/* raxwel Docs — application styles (the site's visual layer; site.css/variable.css are Font Awesome only). */

/* Theme-independent tokens + the raxwel palette DARK theme (the default). The viewer's header toggle stamps
   data-theme="light" / "dark" on <html>; theme-init.js sets it before first paint to avoid a flash. */
:root {
    --rx-radius: 8px;
    --rx-radius-sm: 6px;
    --rx-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --rx-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    --rx-header-h: 70px;
    --rx-bar-h: 48px;

    /* raxwel palette — Bright Blue #1E92CC accent on a navy-tinted dark ground */
    --rx-accent: #1e92cc;
    --rx-accent-strong: #57b0e0;
    --rx-accent-soft: #102a3d;
    --rx-bg: #0a1620;
    --rx-fg: #e8eef4;
    --rx-muted: #94a3b0;
    --rx-border: #223345;
    --rx-surface: #101f2c;
    --rx-surface-2: #1a2c3b;
    --rx-code-bg: #101f2c;
    --rx-danger: #ff7b72;
    /* Lightened for the dark surface, exactly as --rx-danger is: the light theme's green fails contrast on #0d1a24. */
    --rx-success: #3fb950;
    --rx-success-strong: #2ea043;
    --rx-shadow: 0 1px 3px rgba(0, 0, 0, .5);
}

/* raxwel palette — light theme (Blue #016699 for text/link contrast, Navy #004B70 for hover) */
:root[data-theme="light"] {
    --rx-accent: #016699;
    --rx-accent-strong: #004b70;
    --rx-accent-soft: #e6f2fa;
    --rx-bg: #ffffff;
    --rx-fg: #0a2231;
    --rx-muted: #5b6b78;
    --rx-border: #cfdae3;
    --rx-surface: #f2f7fb;
    --rx-surface-2: #e4eef5;
    --rx-code-bg: #f2f7fb;
    --rx-danger: #cf222e;
    --rx-success: #1a7f37;
    --rx-success-strong: #116329;
    --rx-shadow: 0 1px 3px rgba(0, 75, 112, .10), 0 1px 2px rgba(0, 75, 112, .08);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--rx-sans);
    font-size: 16px;
    line-height: 1.6;
    color: var(--rx-fg);
    background: var(--rx-bg);
}

a { color: var(--rx-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code, pre, textarea.editor-code { font-family: var(--rx-mono); }

/* Base defaults formerly inherited from Bootstrap's Reboot (now removed). Contextual rules (.docs-article h*,
   .home-hero h1, .editor-* …) still override size/margins where they set them; these only restore heading weight and
   button font inheritance, matching Bootstrap's reboot so nothing shifts visually. */
h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: .5rem; font-weight: 500; line-height: 1.2; }
button { font-family: inherit; }

.container { width: 100%; max-width: 1600px; margin: 0 auto; padding: 0 24px; }

/* Neutralize the legacy site.css light surfaces (hardcoded for the old light-only theme) so the whole chrome is themed:
   the header <nav> (#ebf0f2), and the main content card (.content: #fff + shadow + grey text). Dark theme is the default
   now, so these must follow the tokens instead of being fixed light. */
.header { background: var(--rx-surface); }
.page_content .content {
    background: transparent;
    color: var(--rx-fg);
    border: 0;
    box-shadow: none;
    border-radius: 0;
    padding: 0;
    height: auto;
}

/* ── Header / footer ─────────────────────────────────────────────────────── */
header { position: sticky; top: 0; z-index: 20; background: var(--rx-surface); border-bottom: 1px solid var(--rx-border); }
/* `justify-content: space-between` is deliberately absent and reverting it re-breaks the layout: it distributes free
   space BETWEEN children, so with the old flat list of four the editor-managed menu was pushed toward the centre by
   whatever was left over rather than by any decision. Two children now — the brand group and the right rail — with
   `margin-inline-start: auto` on the rail (below) putting ALL free space in one gap between them. That keeps the menu
   flush against the logo without any child having to grow. */
.header_content { display: flex; align-items: center; gap: 24px; height: var(--rx-header-h); }
/* The rail does NOT shrink: its three parts are the search (elastic down to its own min-width) plus the AI and auth
   clusters, which are rigid by design. Letting the rail shrink past the sum of those pushed the rigid clusters
   straight out of the viewport — they cannot give way, so the overflow simply moved off-screen instead of being
   absorbed. `flex-shrink: 0` here makes the rail state its true minimum, and the brand group (the only genuinely
   compressible item, since its menu wraps) is then what yields. That is the intended order: search first, brand
   group second, these clusters never. */
.header_content_right_sidebar { margin-inline-start: auto; flex-shrink: 0; }
/* `flex-shrink: 1` + `min-width: 0`, NOT `flex: 0 0 auto`. A non-shrinking group whose links are `white-space: nowrap`
   takes its min-content width from the sum of every label, so a tenant with several long ones (verified with six
   German labels at 1024px) pushed the search to 0px wide and then overflowed the header into a horizontal scrollbar.
   Shrinking is bounded by the logo — .logo is `flex: 0 0 auto` below, so the mark never compresses; only the menu's
   share gives way, and it wraps rather than clipping. Nothing is hidden: capping this group with `overflow: hidden`
   would silently drop editor-configured links, which is the one failure mode worse than a tight search. */
.header-brand-nav { display: flex; align-items: center; gap: 28px; flex: 0 1 auto; min-width: 0; }
.header-brand-nav .logo { flex: 0 0 auto; }
.logo { display: inline-flex; align-items: center; }
/* The header mark's size is set on THIS selector deliberately. The legacy site.css carries
   `.header_content .logo img { height: 44px }`, which out-specifies a bare `.logo img` — so the 30px that used to sit
   here had never applied to anything, and editing it changed nothing. Same selector, same 44px (no visual change),
   later stylesheet: the size is docs.css's to set now. `object-fit` is flipped from site.css's `cover` to `contain`:
   with height + auto width the box always matches the artwork so neither does anything today, but `cover` on a mark
   means "crop it" the moment anything constrains the width — and a cropped logo is the one failure a logo must not
   have. A TENANT's uploaded mark lands in this same box (aspect 0.5–8 by the platform's own rules), so `contain`
   letterboxes an unusual one rather than trimming its edges off. */
.header_content .logo img { height: 44px; width: auto; display: block; object-fit: contain; }
/* A tenant that uploaded BOTH a light and a dark mark gets the one that suits the active theme. Only ever emitted as
   a pair (the layout renders a single <img> when the tenant has one mark), so exactly one is visible at a time and
   the hidden one leaves the accessibility tree with it. */
:root[data-theme="dark"] .logo .logo-light,
:root[data-theme="light"] .logo .logo-dark { display: none; }
.header_content_right_sidebar span { color: var(--rx-muted); }
.header_content_right_sidebar.open {background: var(--rx-surface);}
/* The language/AI/theme cluster, restated here because site.scss only ever declared it as
   `.header_content_right_sidebar_search .header_content_right_sidebar_ai` — nested under the `_search` wrapper that
   used to hold the search box. Removing that wrapper (the search moved up into the rail itself) took the ancestor
   with it, so the group silently fell back to `display: block`: the language <form> is block-level and claimed a line
   of its own, dropping the two icon buttons onto a second row. It was never being squeezed — at 1440px there was
   free space to spare — so this is a missing rule, not a sizing problem.
   `flex-shrink: 0` states the priority explicitly: these three are rigid content (a select and two 34px buttons) and
   must never be what yields. The search is the elastic element and shrinks first; see .header-search. */
.header_content_right_sidebar_ai { display: flex; align-items: center; gap: 16px; font-size: .9rem; flex-shrink: 0; flex-wrap: nowrap; }
.header_content_right_sidebar_ai > * { flex-shrink: 0; white-space: nowrap; }
/* Rigid for the same reason — the auth cluster holds the "Welcome, {name}" greeting, whose width is the user's name.
   Left shrinkable it would wrap mid-greeting before the search gave up any width. This rigidity applies to the
   EXPANDED bar only; inside the drawer it is undone below, where wrapping is the correct behaviour. */
.header_content_right_sidebar_auth { flex-shrink: 0; flex-wrap: nowrap; white-space: nowrap; }
.header-link { font-weight: 600; }
.theme-toggle { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; padding: 0; color: var(--rx-muted); background: transparent; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); cursor: pointer; }
.theme-toggle:hover { color: var(--rx-accent); border-color: var(--rx-accent); }
/* The "Ask AI" header button reuses the icon-button shape but leads with the accent so it invites a click. */
.header-ai { color: var(--rx-accent); }
.header-ai:hover { color: var(--rx-accent-strong); border-color: var(--rx-accent); }
.theme-toggle .icon-moon { display: none; }
:root[data-theme="light"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: inline-flex; }

.lang-switch { margin: 0; }
.lang-switch select {
    height: 34px; padding: 0 26px 0 10px; font-size: .82rem; color: var(--rx-muted); cursor: pointer;
    background: transparent; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm);
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b0' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 8px center;
}
.lang-switch select:hover { color: var(--rx-accent); border-color: var(--rx-accent); }
.lang-switch option { color: var(--rx-fg); background: var(--rx-surface); }

.page { min-height: calc(100vh - var(--rx-header-h) - 60px); padding: 28px 0 48px; background: var(--rx-bg); }
.footer { border-top: 1px solid var(--rx-border); color: var(--rx-muted); font-size: .85rem; padding: 18px 0; background: var(--rx-surface); }
.footer a { color: var(--rx-muted); }
.footer-inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; }
.footer-links a:hover { color: var(--rx-accent); }

/* Editor-managed header links. `flex-wrap` + `min-width: 0` are what let the brand group above honour its
   flex-shrink: the links stay `nowrap` individually (a label should not break mid-word), but the ROW may wrap, so a
   long menu costs height instead of forcing the header wider than the viewport. */
/* `display` is NOT set here — it is owned by the count-driven collapse rules further down, which decide whether this
   menu is inline at all. A `display: flex` at this altitude would be a second source of truth for the same property. */
.header-nav { align-items: center; gap: 18px; flex-wrap: wrap; min-width: 0; }
.header-nav-link { color: var(--rx-fg); font-size: .9rem; font-weight: 500; white-space: nowrap; }
.header-nav-link:hover { color: var(--rx-accent); text-decoration: none; }

/* Editor: nav-link rows */
.navlink-item { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; padding: 10px 0; border-top: 1px dashed var(--rx-border); }
.navlink-item .editor-form-inline { border-top: 0; padding-top: 0; }
.navlink-delete { margin: 0; }
.navlink-add { margin-top: 6px; }
/* ── The menu's collapse threshold is a function of the ITEM COUNT ────────────
   More links need more room, so one hardcoded breakpoint is wrong for every count but the one it was tuned to. There
   is deliberately no overflow dropdown and no scrolling menu: past its threshold the burger simply takes over and
   opens the SAME drawer used on mobile. One menu pattern, not two.

   Queried against .header_content, NOT <header>, and that distinction is load-bearing: .container caps the content at
   1600px, so at a 1920px viewport <header> measures 1905px while the content box is 1600px. Querying the header would
   report ~300px of space that does not exist. Confirmed measured: container width is 1585px @1600 viewport and 1600px
   at BOTH 1920 and 2560 — extra viewport beyond ~1615px gives the menu nothing at all.

   THERE ARE EXACTLY TWO STATES, and every header item switches between them TOGETHER:
     • EXPANDED  — logo + menu inline on the left, search and the right rail on the row. No burger.
     • COLLAPSED — logo far left, burger far right, NOTHING else in the bar. The drawer holds the menu and the rail.
   Earlier cuts gave each item its own breakpoint (menu at one width, rail at 930px, search at 700px). Every reported
   bug since has been a band between two of them: a menu on a second row before the burger arrived, a search sitting
   over the burger, a partially-collapsed bar. One threshold for all of it is the fix, so DO NOT reintroduce a
   per-element breakpoint here.

   Thresholds are MEASURED (binary search on container width, real German labels at .95rem/18px gap, .open state
   cleared) — the width at which the row STOPS FITTING, so the burger appears exactly there and never later. Stated as
   CONTENT-box values because `container-type: inline-size` queries the content box while the measurements are
   border-box: the .container's own 2x24px padding is subtracted, a flat 48px.

     items  border-box  content-box (used below)  fits under the 1600px cap?
       1     642px       594px                    yes
       2      —         1193px                    yes  (calibrated against a real window, not measured here: the
                                                        stored value is 47 less than the viewport at which the
                                                        burger appears — see the rule for the derivation)
       3    1372px      1324px                    yes
       4    1530px      1482px                    only in a ~70px band before the cap — NOT worth a separate state,
                                                  so 4 is drawer-only with 5+ (product decision)
       5    1690px      1642px                    NO — exceeds the 1600px cap
       6    1761px      1713px                    NO

   THE MAXIMUM INLINE ITEM COUNT IS 3. A tenant adding a fourth header link moves the menu to the burger at every
   width, including 4K — the intended consequence of "raise the breakpoint" rather than build a second pattern.

   An earlier set of thresholds was 62px too low at every count, because it was measured BEFORE the right rail was
   pinned `flex-shrink: 0`. While the rail could shrink, the search absorbed pressure toward its 200px min-width and
   the row survived narrower; once the rail became rigid the search holds 340px at every width and the row needs that
   62px back. Re-measure these values if the rail's or the search's flex behaviour ever changes. */
.header_content { container-type: inline-size; container-name: headerbar; }

/* COLLAPSED is the DEFAULT, and each container query below opts a count back into EXPANDED once there is room. That
   direction is deliberate: an unknown or very large count (a tenant with 12 links) must land in the drawer, not spill
   across the bar.

   The BASE rules are deliberately NOT wrapped in :where(): they have to out-specify site.css, which carries
   `.header_content .burger-toggle { display: none }` at (0,2,0). A zero-specificity base rule loses to it and the
   burger disappears entirely — collapsed bar, nothing to open it. They are written at (0,2,0) to match and win on
   order, since docs.css loads after site.css. Only the COUNT selectors below need :where(), and only because their
   attribute would otherwise out-specify `.header-nav.open` and stop the drawer from ever opening. */
.header_content .header-nav { display: none; }
.header_content .header-search { display: none; }
.header_content .header_content_right_sidebar { display: none; }
.header_content .burger-toggle { display: inline-flex; }

/* EXPANDED, per count. Everything comes back at once.
   `.header_content:where(...)` rather than `:where(.header_content[...])`: the class stays OUTSIDE the :where() so
   these keep (0,2,0) and beat site.css, while the data-nav-items attribute sits INSIDE it and contributes nothing.
   That is what keeps `.header-nav.open` (0,2,0, later in the file) able to win when the drawer opens. */
@container headerbar (min-width: 594px) {
    .header_content:where([data-nav-items="0"], [data-nav-items="1"]) .header-nav { display: flex; }
    .header_content:where([data-nav-items="0"], [data-nav-items="1"]) .header-search { display: block; }
    .header_content:where([data-nav-items="0"], [data-nav-items="1"]) .header_content_right_sidebar { display: flex; }
    .header_content:where([data-nav-items="0"], [data-nav-items="1"]) .burger-toggle { display: none; }
}
/* 1193px. CALIBRATED against a real browser window, not computed from this file's container measurements — those
   proved to be offset from a real window by a constant this harness could not see, and computing from them moved the
   switch the wrong way twice.
   Two observations from the target display pinned it: stored 1185 → burger at 1232, stored 1177 → burger at 1224.
   Stored and observed move 1:1 with a constant offset of +47 (1232−1185 = 1224−1177 = 47), so
   stored = desired_viewport − 47, and 1240 − 47 = 1193.
   If this ever needs retuning, take TWO readings from a real window and re-derive the offset; do not adjust the
   stored number by the difference between two viewport figures. */
@container headerbar (min-width: 1193px) {
    .header_content:where([data-nav-items="2"]:not([data-locale="de"])) .header-nav { display: flex; }
    .header_content:where([data-nav-items="2"]:not([data-locale="de"])) .header-search { display: block; }
    .header_content:where([data-nav-items="2"]:not([data-locale="de"])) .header_content_right_sidebar { display: flex; }
    .header_content:where([data-nav-items="2"]:not([data-locale="de"])) .burger-toggle { display: none; }
}
/* GERMAN, 2 items: 1253px = a 1300px flip (1253 + 47). The chrome is wider in German — measured, the right rail is
   624px against English's 594px, all 30px of it the auth cluster ("Anmelden" 65px vs "Login" 35px) — so the same row
   needs more viewport. The English rule above excludes `de` rather than being overridden by this one, because both
   would otherwise match between 1193 and 1253 and the wider locale would expand too early.
   data-locale comes free: _Layout.cshtml already has `uiCulture` in scope (it emits <html lang> and
   data-ui-culture from it), so this is one extra attribute, not new plumbing.
   Counts 1 and 3 are deliberately NOT given German variants yet — measured but unconfirmed, pending the user's own
   readings. */
@container headerbar (min-width: 1253px) {
    .header_content:where([data-locale="de"][data-nav-items="2"]) .header-nav { display: flex; }
    .header_content:where([data-locale="de"][data-nav-items="2"]) .header-search { display: block; }
    .header_content:where([data-locale="de"][data-nav-items="2"]) .header_content_right_sidebar { display: flex; }
    .header_content:where([data-locale="de"][data-nav-items="2"]) .burger-toggle { display: none; }
}
@container headerbar (min-width: 1324px) {
    .header_content:where([data-nav-items="3"]) .header-nav { display: flex; }
    .header_content:where([data-nav-items="3"]) .header-search { display: block; }
    .header_content:where([data-nav-items="3"]) .header_content_right_sidebar { display: flex; }
    .header_content:where([data-nav-items="3"]) .burger-toggle { display: none; }
}

/* Pinned to the right edge in the collapsed state — .header-brand-nav does not grow, so without this the burger sits
   against the logo instead of opposite it (it rendered at x=187 while it lived inside that group). */
.header_content .burger-toggle { margin-inline-start: auto; }

/* Below the rail's own drawer point the search is hidden outright (product decision — not offered on mobile) and the
   burger appears. This is the ONE remaining width-based rule, and it governs the rail, not the menu. It stays a
   @media query because site.scss's matching rules for #header-sidebar-mobile and .burger-toggle are @media at 930px;
   splitting the pair across two mechanisms is what caused the original bug. */
/* The open menu drawer, deliberately NOT scoped to a width: the menu can now collapse at 1468px (4 items) or at every
   width (5+ items), so the drawer must exist wherever the burger does. `.open` beats the collapse rules above on
   specificity — same element, one extra class.

   ABSOLUTE against the same <header> the right rail is positioned against, and that pairing is load-bearing.
   site.scss pins #header-sidebar-mobile at `top: 70px; z-index: 20`; a second absolutely-positioned panel at the same
   offset and layer rendered straight on top of it (both open, one unreadable). Leaving the menu in normal flow fails
   too — .header_content is a fixed 70px via site.scss's `.header { height: 70px }`, so an in-flow row overflows the
   header box instead of growing it. An earlier cut used `fixed`, which pins to the VIEWPORT and so disagreed with the
   rail by the height of anything above the header — caught in dev, where DevExtreme's evaluation banner sits there
   and split the two panels by 142px. Absolute keeps them in one coordinate space.

   max-height + overflow-y are REQUIRED, not defensive. <header> is `position: sticky`, so a drawer taller than the
   viewport cannot be reached by scrolling the page: it scrolls away with its own sticky anchor. Verified before the
   fix — 12 links at 375x500 put the auth cluster's bottom at y=736, i.e. 236px below the fold, leaving the login
   button and user info unreachable. The drawer now scrolls itself. */
.header_content .header-nav.open {
    display: flex; flex-direction: column; align-items: stretch; gap: 12px;
    position: absolute; top: var(--rx-header-h); left: 0; right: 0; z-index: 21;
    /* No box-shadow: the two drawer sections stack edge to edge, and a shadow on this one falls ACROSS the rail below
       it, drawing a hard line that makes the menu read as a raised card sitting on a separate panel. The shadow that
       lifts the whole drawer off the page belongs on the LAST section only — see the rail's rule. Backgrounds and
       padding are kept identical here so the seam between the two is invisible. */
    padding: 1rem 1rem .5rem; background: var(--rx-surface);
    max-height: calc(var(--rx-drawer-space, 100vh) * 0.6); overflow-y: auto;
    /* `nowrap` is REQUIRED here and is not a tidy-up of the inline `wrap` above. In a COLUMN flex container with a
       capped height, `wrap` does not overflow downwards — it starts a SECOND COLUMN alongside the first. Measured
       with 40 links: scrollHeight stayed equal to clientHeight (160px, so the box reported nothing to scroll) while
       the later links were laid out sideways, off the panel and unreachable. One column that actually scrolls is the
       whole point of the drawer. */
    flex-wrap: nowrap;
}
/* The links are the drawer's rows: `flex-shrink: 0` stops the column squeezing 40 items into the capped height
   instead of overflowing it, which would defeat overflow-y entirely. */
/* One per row, centred. `text-align` is what actually centres the label: the link is a block-level flex item stretched
   to the panel width, so `align-items: center` on the column would shrink-wrap it instead and give a ragged column of
   differently-sized hit targets. Full-width rows with centred text keep every row the same size to tap. */
.header_content .header-nav.open .header-nav-link { flex-shrink: 0; width: 100%; text-align: center; }

/* The rail as a DRAWER. Not width-scoped: the collapse point is now the count-driven container query above, so a
   `@media (max-width: 930px)` wrapper here would recreate the partial-collapse band this whole section exists to
   remove — above 930px the rail would stay inline while the menu had already collapsed.

   site.css (compiled from scss/site.scss) still carries its own `@media (max-width: 930px)` block making this element
   an absolute panel. Those rules are NOT deleted at source — site.css is minified build output and this file already
   exists to neutralise the legacy sheet (see the note at the top of the Header section). Everything site.css sets
   there is therefore restated unconditionally here, so the drawer looks identical at every width instead of changing
   character at 930px. */
/* `.header_content` qualifier is required, not decoration: the base collapse rule above is
   `.header_content .header_content_right_sidebar { display: none }` at (0,2,0), so a bare `.open` at (0,1,0) loses
   and the rail stays invisible inside the open drawer. Matching (0,2,0) and sitting later in the file wins. */
.header_content .header_content_right_sidebar.open {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: .75rem;
    position: absolute;
    left: 0;
    right: 0;
    z-index: 20;
    /* Top padding halved where it meets the menu section above: the menu's own bottom padding supplies the other
       half, so the seam reads as one continuous panel rather than two stacked boxes. The box-shadow lives here and
       only here — it is the drawer's LAST section, so this is the edge that should lift off the page. */
    padding: .5rem 1rem 1rem;
    background: var(--rx-surface);
    box-shadow: 0 8px 16px rgba(0,0,0,.1);
    /* Room for the menu drawer above it. --rx-menu-h is MEASURED and published by docs-ui.js on open, because a fixed
       panel cannot push an absolute sibling and the menu's height depends on the tenant's own link count and label
       lengths — any constant here leaves a gap or an overlap the moment an editor adds a link. It falls back to 0px,
       i.e. to the position directly under the header, so the rail still opens if the script has not run. */
    top: calc(var(--rx-header-h) + var(--rx-menu-h, 0px));
    /* The rail starts BELOW the open menu, so its room is whatever the menu left of --rx-drawer-space. The menu is
       capped at 60% of that space (above), which is what guarantees this is never zero: with many links the menu
       scrolls internally instead of consuming the whole drawer and burying the sign-out / Login control. */
    max-height: calc(var(--rx-drawer-space, 100vh) - var(--rx-menu-h, 0px));
    overflow-y: auto;
}
/* While the instant-search dropdown is open the rail must NOT clip it. The panel is absolutely positioned against
   .header-search INSIDE this scroll container, so `overflow-y: auto` cuts it off at the rail's bottom edge —
   measured, the dropdown overhung by 43px and was only reachable by scrolling the drawer.
   `overflow: visible` while `:has()` matches lets the panel escape; the max-height is dropped with it because a
   clipped scroll container and a visible overflow cannot both apply. The rail is short in this state anyway (search
   + two clusters), and the menu drawer above keeps its own independent scrolling, so nothing becomes unreachable.
   `:not([hidden])` is REQUIRED, not belt-and-braces: docs-search.js creates the panel once and toggles the `hidden`
   attribute, so it is in the DOM from first keystroke onwards and a bare `:has(.instant-search-panel)` matched even
   with the dropdown closed. That left the rail permanently unclipped — measured with 40 links at 500x500, the Login
   button sat at y=503, 3px past the fold and unreachable.
   The `.header_content` prefix matches the base rule's own `.header_content .header_content_right_sidebar.open`
   (0,3,0); without it this override sits at (0,2,0) and loses silently. */
.header_content .header_content_right_sidebar.open:has(.instant-search-panel:not([hidden])) {
    overflow: visible;
    max-height: none;
}
/* The search IS offered in the drawer, but only above 400px. With several menu items the burger takes over at a
   fairly wide viewport, and hiding the search there would leave no way to search on what is still a large screen.
   Below 400px space is genuinely tight and it stays hidden.
   No DOM move was needed for this: the form is already the FIRST child of the rail, so the drawer's natural order is
   already menu → search → language/AI/theme → auth. `position: relative` is therefore untouched and the
   instant-search panel still anchors to the input.
   The `.open` qualifier is what beats the `display: block` the expanded-state rules give it. */
.header_content_right_sidebar.open .header-search { display: none; }
@media (min-width: 400px) {
    .header_content_right_sidebar.open .header-search {
        display: block;
        /* Sized, not full-bleed: `width: 340px` from the expanded bar would fight the drawer, so it is relaxed to
           fill the available width up to a comfortable maximum and centred with auto margins. `flex: 0 0 auto` stops
           the column stretching it, and `align-self` is not used because the rail is `align-items: stretch`. */
        width: 100%;
        max-width: 420px;
        flex: 0 0 auto;
        margin-inline: auto;
    }
}

/* Inside the drawer the auth cluster WRAPS, undoing the rigidity it needs in the expanded bar. Signed in with a long
   German name the row is 462px wide (Editor 39 + "Willkommen, {first} {last}" 299 + Abmelden 92 + two 16px gaps); at
   320px `nowrap` did not merely overflow to the right, it pushed the Editor link to left: -158px and the greeting to
   -103px, clipping both off the LEFT edge where nothing can scroll them back into view.
   `white-space: normal` on the greeting is the half that matters — wrapping the flex row alone still leaves one
   unbreakable 299px text node.

   CENTRED AT EVERY WIDTH, in every section, signed in or out. Only the ROW COUNT changes at the breakpoint below —
   never the alignment. There is no right- or left-aligned content anywhere in this drawer. An earlier cut
   right-aligned the signed-in cluster above 400px and tested for it with `:has(span)`; both are gone. */
.header_content_right_sidebar.open .header_content_right_sidebar_auth {
    flex-wrap: wrap;
    justify-content: center;
    white-space: normal;
    gap: 10px 16px;
}
/* BELOW 400px: each control takes a full row of its own, so nothing is ever broken in half. */
.header_content_right_sidebar.open .header_content_right_sidebar_auth > * {
    white-space: normal;
    text-align: center;
    flex: 1 1 100%;
}

/* ABOVE 400px: the three controls share ONE centred row. `flex: 0 1 auto` lets them size to their content and sit
   side by side; `justify-content: center` above is untouched, so this changes the row count and nothing else.
   MEASURED CONSEQUENCE, accepted rather than hidden: the row fits only while the content fits. The greeting is the
   variable part — "Willkommen, {first} {last}" is 185px for a normal name but 299px for a long German one — so the
   single row survives down to ~394px viewport for a normal name and only ~509px for a long one. Between 400 and 509
   a long name therefore wraps to two centred rows rather than one. Nothing clips or overflows either way: the row
   wraps, and every resulting row stays centred. See the header comment above for the full measurements. */
@media (min-width: 400px) {
    .header_content_right_sidebar.open .header_content_right_sidebar_auth > * { flex: 0 1 auto; }
}

/* The language/AI/theme cluster is centred too, so all three drawer sections share one alignment. */
.header_content_right_sidebar.open .header_content_right_sidebar_ai { justify-content: center; flex-wrap: wrap; }

/* .btn / .btn-danger — the header Sign-out button (these were Bootstrap classes; Bootstrap has since been removed, so
   these local rules are now their only definition). */
.btn { display: inline-block; padding: 7px 14px; border-radius: var(--rx-radius-sm); border: 1px solid var(--rx-border); background: var(--rx-bg); color: var(--rx-fg); cursor: pointer; font-size: .85rem; }
.btn-danger { border-color: transparent; background: var(--rx-danger); color: #fff; }
.header_content_right_sidebar form { margin: 0; }

/* ── Home: hero + product directory ──────────────────────────────────────── */
.home-hero { max-width: 760px; margin: 8px 0 36px; }
.home-hero h1 { font-size: 2.4rem; line-height: 1.15; margin: 0 0 10px; }
.home-lead { font-size: 1.1rem; color: var(--rx-muted); margin: 0 0 20px; }
.home-search { position: relative; max-width: 560px; }
.home-search input { width: 100%; padding: 12px 18px; font: inherit; font-size: 1rem; color: var(--rx-fg); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: 999px; box-shadow: var(--rx-shadow); }
.home-search input:focus { outline: none; border-color: var(--rx-accent); }
.home-section-title { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--rx-muted); margin: 0 0 14px; }

/* The group filter (dxSelectBox, mounted by home-directory.js). Razor renders the host `hidden`; the script removes
   that once the widget is up, so this never styles an empty box. */
.home-filter { display: flex; align-items: center; gap: 10px; margin: 0 0 20px; }
.home-filter-label { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--rx-muted); }
.home-filter-select { min-width: 220px; }

.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin: 0 0 28px; }
.product-card {
    display: flex; align-items: flex-start; gap: 14px; padding: 18px 20px; border: 1px solid var(--rx-border);
    border-radius: var(--rx-radius); background: var(--rx-surface); color: inherit;
    transition: border-color .15s, box-shadow .15s, transform .15s;
}
.product-card:hover { text-decoration: none; border-color: var(--rx-accent); box-shadow: var(--rx-shadow); transform: translateY(-1px); }
.product-card-badge {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px;
    border-radius: 10px; background: var(--rx-accent-soft); color: var(--rx-accent-strong); font-weight: 700; font-size: 1.2rem;
}
/* Exactly the badge's box (42×42, 10px radius) so a card with an icon and a card without are the same height and the
   grid never reflows between them. `contain` because a pre-existing IconRef predates the 512² crop on upload and a
   non-square one would otherwise be stretched. */
.product-card-icon {
    flex: 0 0 auto; display: block; width: 42px; height: 42px; object-fit: contain; border-radius: 10px;
}
.product-card-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.product-card-title { font-size: 1.1rem; font-weight: 600; color: var(--rx-fg); }
.product-card-desc { color: var(--rx-muted); font-size: .9rem; }

/* ── Docs reading layout ─────────────────────────────────────────────────── */
.docs-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 40px; align-items: start; }

.docs-nav {
    position: sticky; top: calc(var(--rx-header-h) + var(--rx-bar-h) + 20px); align-self: start;
    max-height: calc(100vh - var(--rx-header-h) - var(--rx-bar-h) - 40px); overflow-y: auto;
    border-right: 1px solid var(--rx-border); padding-right: 16px;
}
.docs-switchers { display: flex; flex-direction: column; gap: 10px; padding-bottom: 16px; margin-bottom: 12px; border-bottom: 1px solid var(--rx-border); }
.docs-switcher { display: flex; flex-direction: column; gap: 4px; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); }
.docs-switcher select { width: 100%; padding: 6px 8px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); color: var(--rx-fg); font-size: .85rem; }

.docs-tree-list { list-style: none; margin: 0; padding: 0; }
.docs-tree-list a { display: block; padding: 5px 10px; border-radius: var(--rx-radius-sm); color: var(--rx-fg); font-size: .9rem; }
.docs-tree-list a:hover { text-decoration: none; background: var(--rx-surface); }
.docs-tree-group { margin-top: 14px; }
.docs-tree-group-title { display: block; padding: 4px 10px; font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); }
.docs-tree-subgroup { list-style: none; margin-top: 6px; }

/* ── Nesting indent ──────────────────────────────────────────────────────────
 *
 * THE INDENT GOES ON THE CHILDREN LIST, NOT ON THE NESTED GROUP. This rule used to read
 * `.docs-tree-subgroup > .docs-tree-list` and matched NOTHING — _DocsNav.cshtml renders
 * `li.docs-tree-subgroup > div.docs-tree-group > ul.docs-tree-list`, so the child combinator skipped over the
 * wrapping div and the intended indent never applied. Every level rendered flush left (measured: category rows and
 * document links all at left:0, levels 1 through 4 alike), which is what made the hierarchy invisible.
 *
 * Targeting the nested group's OWN list is the off-by-one to avoid, and it was made and caught here: that shifts
 * the group's grandchildren while its own row stays flush with its parent, so the first nested level looks
 * un-indented. `.docs-tree-group > .docs-tree-children` is the list a group hands its children, so each level moves
 * with the row it belongs to and the very first subgroup steps in as expected.
 *
 * DocsNavRecursiveRenderTests pins the `li > div.group > ul` chain, so the markup this depends on cannot drift back.
 *
 * INDENT IS ON THE LIST, NOT ON THE <a>. The active highlight is a full-width block with an
 * `inset 2px` left accent bar (see .docs-tree-list a.is-active), so its left edge IS the item's left edge. Padding
 * the anchor would slide the text away from its own accent bar; moving the container carries the whole highlight,
 * bar included, to the indented position.
 *
 * NO MAXIMUM LEVEL. The indent compounds through natural DOM nesting rather than a depth class, so it keeps working
 * if MaxCategoryDepth moves past 4 — nothing here counts levels. */
.docs-tree { --rx-nav-indent: 12px; }
.docs-tree-group > .docs-tree-children { margin-left: var(--rx-nav-indent); }
/* Narrow viewports get a SMALLER step, not the same one. Below 900px the sidebar stops being a 280px column and
 * becomes the full content width, which at a 320px viewport is 256px — and four levels of the desktop 12px would
 * spend 36px of that. 8px keeps four levels inside 24px total (<10% of the nav) while staying a perceptible step
 * against the 22px caret. Measured at 320px with the longest real names, the nav being 256px there: rows step
 * 0/8/16/24, and "Choose Application UI" — the longest real category name, 153px rendered — still sets on one line
 * at level 3 with 188px available. Labels wrap rather than truncate, so a deeper level costs a line, never text.
 *
 * One custom property, overridden once — the selector above is not repeated, so the two values cannot drift. */
@media (max-width: 560px) {
    .docs-tree { --rx-nav-indent: 8px; }
}

.docs-content { min-width: 0; }
.docs-breadcrumb { font-size: .85rem; color: var(--rx-muted); margin-bottom: 14px; }
.docs-breadcrumb a { color: var(--rx-muted); }
.docs-breadcrumb .sep { opacity: .6; }
.docs-meta { color: var(--rx-muted); font-size: .85rem; margin-top: -6px; }

/*.docs-article { max-width: 820px; }*/
/* Title row: the <h1> and, for a docs.write holder only, "Edit document" pushed to the right. `align-items: baseline`
   rather than centre so the button sits on the title's baseline when both are one line; `flex-wrap` is what keeps a
   long title from crushing the button on a narrow viewport — it drops to its own line instead of overlapping, which
   is why the h1's bottom margin moves onto this row (a wrapped button would otherwise sit flush against the meta). */
.docs-article-head { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px 16px;}
.docs-article-head h1 { margin: 0; }
.docs-edit-btn { flex: 0 0 auto; padding: 5px 12px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius); background: var(--rx-surface); color: var(--rx-fg); font-size: .8rem; font-weight: 600; text-decoration: none; white-space: nowrap; }
.docs-edit-btn:hover, .docs-edit-btn:focus-visible { border-color: var(--rx-accent); color: var(--rx-accent); text-decoration: none; }
.docs-article h1 { font-size: 2rem; line-height: 1.25; margin: 0 0 18px; }
.docs-article h2 { font-size: 1.5rem; margin: 36px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--rx-border); }
.docs-article h3 { font-size: 1.2rem; margin: 28px 0 10px; }
.docs-article p, .docs-article ul, .docs-article ol { margin: 0 0 16px; }
.docs-article ul, .docs-article ol { padding-left: 24px; }
.docs-article li { margin: 4px 0; }
/* `height: auto` DELIBERATELY OVERRIDES the height attribute an authored `{width=300 height=200}` emits: the width is
   honoured and the height is recomputed from the intrinsic aspect ratio, so a resized image can never be squashed.
   The consequence is that `height=` in the markdown suffix is decorative on the reader side — it round-trips and
   keeps the editor's own layout honest, but it does not size the published page. `max-width: 100%` is what keeps an
   over-wide authored width from breaking the column on a narrow viewport.
   Image alignment needs no rule here: `{align=…}` renders as `text-align` on the containing <p>, which positions an
   inline <img> on its own. (Float was measured and refused — see docs/DEVEXTREME.md.) */
.docs-article img { max-width: 100%; height: auto; border-radius: var(--rx-radius-sm); }
.docs-article a { font-weight: 500; }
.docs-article blockquote { margin: 0 0 16px; padding: 2px 16px; border-left: 3px solid var(--rx-border); color: var(--rx-muted); }
.docs-article code { padding: .15em .4em; font-size: .88em; background: var(--rx-code-bg); border: 1px solid var(--rx-border); border-radius: 4px; }
.docs-article pre { margin: 0 0 16px; padding: 14px 16px; overflow-x: auto; background: var(--rx-code-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius); }
.docs-article pre code { padding: 0; background: none; border: 0; font-size: .85rem; }
.docs-article table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: .92rem; display: block; overflow-x: auto; }
.docs-article th, .docs-article td { padding: 8px 12px; border: 1px solid var(--rx-border); text-align: left; }
.docs-article th { background: var(--rx-surface); }

/* ── Editor ──────────────────────────────────────────────────────────────── */
/* .editor { max-width: 920px; } */
.editor-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.editor-head-actions { display: inline-flex; align-items: center; gap: 8px; }
.editor-empty-hint { color: var(--rx-muted); font-size: .85rem; margin: -6px 0 14px; }
.editor h1 { margin: 0; font-size: 1.6rem; }
.editor-crumb { font-size: .85rem; color: var(--rx-muted); margin-bottom: 18px; }
.editor-card { padding: 20px 22px; margin-bottom: 20px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius); background: var(--rx-surface); }
.editor-card h2 { margin: 0 0 14px; font-size: 1.15rem; }
.editor-card h3 { margin: 0 0 12px; font-size: 1rem; }
.editor-meta { color: var(--rx-muted); font-size: .85rem; }
.editor-empty { color: var(--rx-muted); font-style: italic; }
.editor-alert { padding: 10px 14px; margin-bottom: 18px; border: 1px solid var(--rx-danger); border-radius: var(--rx-radius-sm); background: var(--rx-accent-soft); color: var(--rx-danger); }

/* Panes adopted into the DevExtreme TabPanel (editor-product-tabs.js). The panel draws its own border and background,
   so the .editor-card chrome is dropped once a section sits inside one — otherwise the two nest visibly. */
.dx-tabpanel .editor-card { border: 0; margin-bottom: 0; background: none; }

/* ── ONE LEFT EDGE ACROSS AN EDITOR PAGE ────────────────────────────────────────────────────────────────────────────
   THE TARGET IS THE BREADCRUMB'S EDGE, x+0. .editor-crumb has no padding of its own, so it sits flush against the
   .editor box and is the first thing on every one of these pages — everything below it should start where it does.

   FOUR SEPARATE INSETS STACKED UP, none of them cancelling out, which is why these pages stepped further right the
   further down you read:
     breadcrumb        x+0    .editor-crumb, no padding
     tab labels        x+10   DevExtreme's `.dx-tab { padding: 12px 10px }`
     pane content      x+22   .editor-card's own `padding: 20px 22px`, which the rule above does NOT strip
     DataGrid text     x+38   that 22, plus `.dx-datagrid .dx-row > td { padding: 14px 16px }`
   The rules below zero the first three against the pane's box. Vertical padding is untouched throughout; only the
   horizontal inset was ever wrong.

   THE FOURTH IS DELIBERATELY LEFT ALONE, and it is the one exception to "everything on one edge". Removing the pane's
   22px already brings the grid's own BOX to x+0; the remaining 16px is interior padding inside a bordered table, and
   zeroing it puts the cell text hard against the table's 1px border. See the note above those cells further down —
   the aligned thing is the table's edge, not its text.

   SCOPED TO .dx-tabpanel, so this is about ADOPTED PANES and nothing else. The grids that live in a plain .editor-card
   — the products list on /admin, and both grids on /admin/report — keep the card's 22px inset and their own cell
   padding, because on those pages there is no tab strip and no breadcrumb-aligned sibling to line up with. */
.dx-tabpanel .editor-card { padding-left: 0; padding-right: 0; }

/* THE TAB LABELS ONTO THAT SAME EDGE. Each .dx-tab is padded 10px, so the first label's TEXT painted at x+10 while the
   pane below it now starts at x+0. The strip is shifted LEFT by that 10px rather than the tab's own padding being
   zeroed: that padding is also the gap BETWEEN tabs and the hit area around each label, so removing it would jam
   "Versions" against "Locales" — and the selected-tab indicator is `.dx-tab::after { left: 0; right: 0 }`, i.e. it
   spans the tab's PADDED box, so shifting the container moves label and underline together while zeroing the padding
   would shrink the underline to the text width.

   ON .dx-tabs, NOT ON .dx-tabs-wrapper, AND THAT IS LOAD-BEARING. Once the strip overflows, DevExtreme wraps the
   wrapper in `.dx-tabs-scrollable { overflow: hidden }`. A negative margin on the WRAPPER shifts it INSIDE that clip,
   so the first label loses its leading 10px at exactly the width where the strip starts scrolling, and the scroll
   offset is then computed against a box that no longer starts where the strip does. `.dx-tabs` is the outer element
   (`padding: 0` in the theme) and sits outside the clip, so the shift there can never cut a label.

   :not(.dx-tabs-nav-buttons) — THE SCROLL-BUTTON CASE INVERTS THE TARGET, and this exclusion is measured, not
   defensive. When the strip is narrow enough for DevExtreme to add its `.dx-tabs-nav-button` arrows, they become flex
   siblings INSIDE .dx-tabs and the LEADING one is then the strip's first painted element, not a label. The button
   carries no 10px tab padding, so it already sits flush: at 320px with 9 tabs it lands at x+0 unshifted, and the
   -10px drags it to x-10, overhanging the breadcrumb while the first label — now pushed right by the 32px button —
   aligns with nothing either way. Verified at 320px: unshifted the left arrow sits exactly on the breadcrumb's edge.
   So the shift applies only while the labels really are the leading element. */
.editor .dx-tabs:not(.dx-tabs-nav-buttons) { padding-left: 0; margin-left: -10px; }

/* THE GRIDS' CELL GUTTER IS KEPT, DELIBERATELY — there is no rule here, and that absence is the decision.
   Everything else on these pages aligns to x+0 (see above), and the obvious next step is to zero the outer columns'
   padding too — `.dx-datagrid .dx-row > td { padding: 14px 16px }` and `.editor-table th/td { padding: 8px 10px }` —
   so the first column's TEXT lands on the breadcrumb edge as well. That was tried and looks wrong: the table draws its
   own 1px left border, and with the padding gone the cell content sits hard against that border with nothing between
   them. A table is not a form field — its box IS visible chrome, so its contents need to sit inside it.
   What matters for the page's alignment is already true without touching the cells: the grid's and the table's own
   BOX starts at x+0, in line with the breadcrumb, the tabs and the forms, because the pane padding above is what was
   inset — not the cells. The remaining 16px (grid) / 10px (fallback table) is interior padding within a bordered box,
   which is the same relationship the .editor-doc-head bar has with its own text.
   So: do not add `td:first-child { padding-left: 0 }` here. It moves the text off the table's border and onto the
   page's edge, which trades a correct-looking table for one number matching in a measurement. */

/* THE ONE DELIBERATE EXCEPTION to the shared default above, and scoped to that panel on purpose.
   /admin/documents is the only editor page with SIBLINGS BRACKETING THE PANEL: the .editor-doc-head bar above it and
   the .editor-doc-filing bar below, both 12px/14px. Its panes therefore align to THOSE BARS' inner edge rather than to
   the breadcrumb — the head bar, the panes and the filing bar read as one frame around the tab panel, and pulling the
   panes out to x+0 would break that framing to fix an alignment that page does not have a problem with.
   15px, NOT 14px, and the extra pixel is the BARS' BORDER. Both bars are `1px solid` on a border-box, so their 14px
   padding starts 1px in and their content lands at x+15; the rule above strips the adopted card's border, so a bare
   14px here left the panes 1px wide of the bars on each side. Measured, not assumed. */
#document-tabs .editor-card { padding-left: 15px; padding-right: 15px; }
/* THE TAB LABELS ONTO THAT SAME EDGE. The .dx-tab padding is 10px and this page's panes sit at x+15, so the strip is
   nudged RIGHT by the 5px difference — the opposite direction to the shared -10px above, and the reason this page
   needs its own rule rather than inheriting one. The reset below cancels the shared `.editor .dx-tabs` margin so the
   two never compound; it is unconditional (no :not()) because it has to win in BOTH the plain and the nav-button
   states, where the shared rule applies in only one.
   Padding, not margin: a negative right margin pushed the strip's own box 5px PAST the pane's right edge, which is
   the same overhang in the other direction. Padding keeps the box inside its container and still moves the labels. */
#document-tabs .dx-tabs { margin-left: 0; }
#document-tabs .dx-tabs-wrapper { padding-left: 5px; }

.editor-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: .9rem; }
.editor-table th, .editor-table td { padding: 8px 10px; border-bottom: 1px solid var(--rx-border); text-align: left; }
.editor-table th { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); }
.editor-table code { background: var(--rx-surface-2); padding: .1em .4em; border-radius: 4px; font-size: .85em; }

.editor-form { display: flex; flex-direction: column; gap: 12px; }
.editor-form-inline { flex-flow: row wrap; align-items: flex-end; gap: 14px; padding-top: 8px; border-top: 1px dashed var(--rx-border); }
.editor-form label { display: flex; flex-direction: column; gap: 4px; font-size: .8rem; font-weight: 600; color: var(--rx-muted); }
.editor-form input, .editor-form select, .editor-form textarea {
    font: inherit; font-weight: 400; color: var(--rx-fg); padding: 8px 10px;
    border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg);
}
.editor-form label.editor-check input[type="checkbox"] { min-width: auto; }
.editor-form-inline input, .editor-form-inline select { min-width: 160px; }
.editor-form textarea { min-height: 120px; resize: vertical; }
.editor-code { font-family: var(--rx-mono); font-size: .85rem; line-height: 1.5; }
.editor-check { flex-direction: row !important; align-items: center; gap: 8px !important; font-weight: 600; }
.editor-check input { padding: 0; }

/* Fields adopted into DevExtreme editors (editor-product-form.js). The caption replaces the original label text, so it
   carries the same type scale as `.editor-form label` above — otherwise the enhanced fields would sit in a different
   rhythm from the native ones still on the page. No .dx-* overrides: theming stays with editor-theme.js. */
.editor-field { display: flex; flex-direction: column; gap: 4px; }
.editor-field-label { font-size: .8rem; font-weight: 600; color: var(--rx-muted); }

/* The category icon picker (editor-product-form.js, data-dx-editor="iconpicker").
   `currentColor` on the glyph is why the sprite was chosen over a webfont: the icon follows the surrounding text
   colour in both themes with no second stylesheet. flex-shrink:0 keeps the fixed 20px box even when a name is long —
   and it is what makes an UNRESOLVABLE name degrade to an empty box of the right size rather than collapsing the row. */
/* The category glyph, wherever it renders: the reader's sidebar (_DocsNav), the admin grid's icon column
   (editor-version-grids.js) and the no-JS fallback table — all via _TablerIcon.cshtml or its JS mirror.
   `flex: 0 0 auto` is what keeps an UNRESOLVABLE name a fixed-size empty box instead of a collapsed one. */
.tabler-icon { color: currentColor; flex: 0 0 auto; vertical-align: -0.15em; }
.editor-icon-glyph { color: currentColor; flex: 0 0 auto; }
.editor-icon-item { display: inline-flex; align-items: center; gap: 8px; }
.editor-icon-field { display: flex; align-items: center; gap: 8px; padding-left: 8px; }
.editor-icon-field > .dx-textbox { flex: 1 1 auto; }

/* ── The category icon picker's popup (editor-product-form.js § "The Tabler icon picker") ──────────────────────────
   A searchable grid of glyphs under sticky category headings, not a dropdown list: the field offers all ~5,130 Tabler
   icons, and a one-column list showed about six of them per screen.

   THE ROW HEIGHTS HERE ARE LOAD-BEARING. The grid is virtualized on a fixed row height — the JS maps scrollTop to a
   row index arithmetically rather than measuring — so .editor-iconpicker-cell's height + the row gap must stay equal
   to ICON_CELL (40px), and .editor-iconpicker-heading's total height equal to ICON_HEADER (28px). Change one without
   the other and rows drift out of the window as you scroll, leaving blank bands. */
.editor-iconpicker { display: flex; flex-direction: column; height: 100%; min-height: 0; }
/* No bottom padding: the chip bar below owns the gap between the two (padding-top there), so setting it in both
   places stacked to 16px against the 11px under the chips. One side owns each gap.
   The bottom border separates the search box from the category strip, matching the strip's own lower border so the
   two rows of chrome above the grid are delimited the same way. */
.editor-iconpicker-search {
    /* NOTE: this element IS the dxTextBox — the widget is mounted directly onto it, so it carries the dx-texteditor
       classes and DevExtreme's own outlined styling. That is why the radius has to be zeroed here rather than on a
       child, and why the padding/border below land on the widget itself.
       8px below, so the border sits clear of the input rather than hard against it. The chip bar's own top padding
       compensates, keeping the total gap between input and chips where it should be. */
    padding: 8px; flex: 0 0 auto;
    border-bottom: 1px solid var(--rx-border);
}
/* Square corners. The radius comes from DevExtreme's own `.dx-texteditor.dx-editor-outlined { border-radius: 4px }`,
   which is TWO classes — a single-class rule here loses on specificity and silently does nothing. Matching its
   shape is what makes this stick; same doubled-class pattern as the overrides in dx-bridge.css. */
.editor-iconpicker-search.dx-texteditor { border-radius: 0; }
/* The category jump bar: one chip per category, scrolling horizontally (41 chips do not fit 360px). Clicking a chip
   scrolls the grid to that category; the active chip TRACKS the grid's position rather than being a filter, so it is
   a "you are here" indicator as much as a control. */
.editor-iconpicker-cats {
    flex: 0 0 auto; display: flex; gap: 4px; align-items: center;
    /* Top padding is 2px, NOT the 11px gap it produces: the search box above contributes 8px of its own bottom
       padding (it needs that to clear its border from the input) plus that 1px border, and the three add up. Change
       any of them and the others have to move — the gap between the input and the chips is the sum.
       The 5px at the bottom is separate from that balance: without it the horizontal scrollbar sits FLUSH against the
       chips' lower edge, which reads as the glyphs resting on the bar. It buys the scrollbar its own breathing room,
       and the gutter beyond it carries the gap down to the divider. */
    padding: 2px 8px 5px; overflow-x: auto; overflow-y: hidden;
    border-bottom: 1px solid var(--rx-border);
    /* Firefox/WebKit thin scrollbar — a full-size one would eat most of a 34px-tall strip. */
    scrollbar-width: thin;
}
.editor-iconpicker-cats::-webkit-scrollbar { height: 4px; }
.editor-iconpicker-cats::-webkit-scrollbar-thumb { background: var(--rx-border); border-radius: 2px; }
.editor-iconpicker-cats[hidden] { display: none; }
.editor-iconpicker-cat {
    flex: 0 0 auto; width: 30px; height: 30px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid transparent; border-radius: var(--rx-radius-sm);
    color: var(--rx-muted); cursor: pointer;
}
.editor-iconpicker-cat:hover { background: var(--rx-surface-2); color: var(--rx-fg); }
.editor-iconpicker-cat:focus-visible { outline: 2px solid var(--rx-accent); outline-offset: 1px; }
.editor-iconpicker-cat.is-active {
    background: var(--rx-accent-soft); border-color: var(--rx-accent); color: var(--rx-accent);
}
/* padding-top matches the 11px above the chip row, so the grid starts the same distance below the divider as the
   chips sit below the search box. At 0 the first row of glyphs butted straight against the divider. */
.editor-iconpicker-scroll { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; padding: 11px 12px 8px; }
/* Holds the FULL scroll height while only the visible rows exist in the DOM. */
.editor-iconpicker-spacer { position: relative; width: 100%; }
.editor-iconpicker-body { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }
.editor-iconpicker-heading {
    height: 28px; display: flex; align-items: center;
    font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
    color: var(--rx-muted);
}
.editor-iconpicker-row { display: flex; height: 40px; align-items: center; }
.editor-iconpicker-cell {
    /* flex: 0 0 auto — WITHOUT it the row's flex layout shrinks cells to fit whatever is in them, so the glyph grid
       stops being a grid: columns drift by a pixel or two per row and the icons no longer line up vertically. The
       column COUNT is computed in JS from the measured width; the cells must then hold exactly that size. */
    flex: 0 0 auto;
    width: 36px; height: 36px; margin-right: 4px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid transparent; border-radius: var(--rx-radius-sm);
    color: var(--rx-fg); cursor: pointer;
}
.editor-iconpicker-cell:hover { background: var(--rx-surface-2); }
.editor-iconpicker-cell:focus-visible { outline: 2px solid var(--rx-accent); outline-offset: 1px; }
.editor-iconpicker-cell.is-selected { border-color: var(--rx-accent); background: var(--rx-accent-soft); }
.editor-iconpicker-empty { padding: 16px 12px; color: var(--rx-muted); font-size: 13px; }
.editor-form-inline .editor-field { min-width: 160px; }

/* ── Editor head bar (Views/Editor/Document.cshtml, Views/Editor/Category.cshtml) ───────────────────────────────────
   AN EDITOR PAGE'S TOP BAR: what the thing being edited is, and a Save for it. Named for the document editor, where it
   started, but it is now SHARED — the category editor's header uses it too, so the two pages' top sections match by
   construction rather than by two copies of the same values drifting apart. Anything added here lands on both; keep
   page-specific rules (the [data-dx-save-document] one below) selector-scoped rather than folded into the base.

   On the document editor it holds a Save for the open translation. DELIBERATELY IDENTICAL CHROME to
   .editor-doc-filing below — same --rx-surface tint, same 1px --rx-border, same --rx-radius-sm, same 12px/14px
   padding. The two are this page's only document-level bars and they bracket the tab panel top and bottom; matching
   them is what makes that pairing read as one frame around the panes rather than as two unrelated boxes.

   The 14px side padding is also load-bearing for the page's vertical alignment: it is the inset the
   `#document-tabs .editor-card` rule above matches, so the head bar, the panes and the filing bar share one pair of
   left/right edges.

   NOT an .editor-card, which is what this was. That gave it 20px/22px padding, an 8px radius and a stacked
   heading/prose/button column — 169px of height for one line of text and one button, the tallest block on a page
   whose actual content is the editor below it. A single centred flex row brings it to ~62px.

   ROW, NOT WRAP, until it has to: the text block flexes and the button holds its width, so a long slug wraps the
   prose rather than pushing Save off the edge. */
.editor-doc-head {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    margin-bottom: 20px; padding: 12px 14px;
    background: var(--rx-surface); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm);
}
/* Takes the slack so the button stays hard right; min-width: 0 lets a long slug wrap inside it instead of forcing
   the row wider than its container. */
.editor-doc-head-text { flex: 1 1 auto; min-width: 0; }
/* The heading and the prose lose .editor-card's margins — they are stacked tight inside a bar now, not spaced out
   in a card, and the bar's own padding provides the room. Type scale is unchanged, so the text reads as it did. */
.editor-doc-head h2 { margin: 0; font-size: 1.15rem; }
.editor-doc-head .editor-meta { margin: 2px 0 0; }
/* The native button is hidden (not removed) once editor-document-save.js swaps a dxButton in beside it — see that
   file. As a flex child a hidden element would still be laid out by some engines' flex pass; this keeps it out. */
.editor-doc-head [data-dx-save-document][hidden] { display: none; }
/* Neither the button nor the adopted widget may shrink: Save is the row's fixed end and its label should never
   ellipsize to give the prose room. */
.editor-doc-head .editor-btn, .editor-doc-head .editor-dx-button { flex: 0 0 auto; }

/* ── Document filing bar (Views/Editor/Document.cshtml + editor-doc-filing.js) ──────────────────────────────────────
   The one DOCUMENT-LEVEL field on a page otherwise made of per-locale translation panes. It sits BELOW the tab panel
   (a sibling of it, never inside a pane — see the view for why the tab adopter forbids that), so the risk is that it
   reads as belonging to whichever translation happens to be open. The surface tint + full outline mark it as chrome
   for the whole document rather than part of a pane, and the top margin separates it from the tab panel above it.
   Deliberately NOT an .editor-card: a card with a heading is what made this read as a third Save.

   1px, not the 2px this carried while it sat above the tabs. Every other control on the page is 1px — .editor-btn,
   .editor-icon-preview, the .editor-revisions rule below — so 2px was the single odd weight in the admin form. It
   was there to shout "not part of a pane" when the bar sat directly against the tab strip; below the panel it is no
   longer adjacent to the panes it could be confused with, so the tint and the outline carry that on their own.

   TWO ROWS, not the single inline row this was: a "Filed in <path>" statement, then the control that changes it.
   As one row the dropdown was the only thing on screen, which stated the destination and never the origin — the
   author could not tell filed-here from about-to-file-here. The state line answers that before the control asks
   anything, and it is server-rendered, so it is also the only filing visible with no JS.

   NOT flex-flow: row wrap (i.e. no .editor-form-inline) — the form is a column of two rows now, and the inline
   modifier's dashed top border belonged to a strip that sat flush under a card. */
.editor-doc-filing {
    margin: 18px 0 0; padding: 12px 14px; gap: 10px;
    background: var(--rx-surface); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm);
}
/* Dirty — the picked category differs from the stored one. The whole outline turns accent so the bar itself signals
   that it holds an uncommitted change, which matters on a tall page where the bar scrolls out of sight above the
   body editor. Class is toggled by editor-doc-filing.js; nothing here depends on it existing. */
.editor-doc-filing.is-dirty { border-color: var(--rx-accent); }

/* The state line. Baseline-aligned rather than centred so the glyph, the label and the path sit on one optical line
   even though the path is the only part at full text size. */
.editor-doc-filing-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; font-size: .85rem; }
/* The folder glyph is the one part that should NOT sit on the text baseline — .tabler-icon's own vertical-align
   handles a normal inline run, but as a baseline-aligned flex child it needs its own nudge. */
.editor-doc-filing-head > .tabler-icon { align-self: center; color: var(--rx-muted); }
/* Same type scale as `.editor-form label` / .editor-field-label — this is a caption, and the page has one rhythm
   for captions. */
.editor-doc-filing-label { font-size: .8rem; font-weight: 600; color: var(--rx-muted); }
/* The path is the CONTENT of this line, so it is the only part in full foreground colour. Monospace would over-weight
   it — this is a title path ("Modules / Ordering"), not a slug. */
.editor-doc-filing-path { color: var(--rx-fg); font-weight: 600; }
/* "Not filed yet" is a state, not a name — italic muted, matching .editor-empty, which is how every other empty
   state on these pages reads. */
.editor-doc-filing-path.is-empty { color: var(--rx-muted); font-weight: 400; font-style: italic; }
/* The pending destination, written by the script as "→ Getting started". Accent-coloured because it is the only
   thing on the bar that is not yet true. */
.editor-doc-filing-next { color: var(--rx-accent); font-weight: 600; }
.editor-doc-filing-next[hidden] { display: none; }

/* The control row. align-items: flex-end so the button's box lines up with the picker's INPUT rather than with the
   top of its caption — the two have different heights because only the picker carries a label above it. */
.editor-doc-filing-row { display: flex; flex-flow: row wrap; align-items: flex-end; gap: 10px; }
.editor-doc-filing-row .editor-field { flex: 1 1 240px; min-width: 160px; }
/* ONE ROW, ONE HEIGHT. The picker is a DevExtreme widget and Move is a plain .editor-btn, so nothing made the two
   agree: measured, the input came out 31px against the button's 34px. The picker is raised to the BUTTON's height
   rather than the button lowered to the picker's, because .editor-btn is global — every admin page uses it, and
   shrinking it here would leave Move inconsistent with the Save/Publish/Delete rows on this same page.
   Scoped to this row so the shared categoryTreePicker on NewDocument.cshtml keeps the stock size. */
.editor-doc-filing-row .dx-dropdownbox .dx-texteditor-input { height: 34px; min-height: 34px; }
/* The stock theme draws this arrow as a 20px DXIcons glyph sized for a taller control; in a 34px field it reads
   oversized (it does not overflow — measured). 16px puts it at roughly half the field height, the proportion the
   rest of the admin form's controls carry. */
.editor-doc-filing-row .dx-dropdowneditor-icon { font-size: 16px; line-height: 16px; }
/* A disabled Move is the RESTING state (nothing changed yet), so it must not read as broken — muted and
   not-allowed, not the greyed-out-and-faded treatment a failed action would get. */
.editor-doc-filing-row .editor-btn[disabled] {
    opacity: .55; cursor: not-allowed;
}

/* Product icon (editor-product-icon.js). The host carries `hidden` in the view and the script removes it once the
   widget is mounted, so a no-JS page shows nothing here rather than a file input that would silently discard the
   author's image — see the script header. `[hidden]` needs the explicit `display: none` because .editor-field sets
   `display: flex`, which otherwise wins. */
.editor-icon[hidden] { display: none; }
/* The positioning context for .editor-icon-uploader below, which is taken out of flow so the hidden widget chrome
   cannot add a stray gap under the tile. Without this it would anchor to the page instead of the field. */
.editor-icon { position: relative; align-items: flex-start; }
.editor-icon-preview {
    position: relative; width: 96px; height: 96px; display: grid; place-items: center;
    border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); overflow: hidden;
}
/* NEVER SHOWN, and deliberately still rendered. The overlay is the prompt in BOTH states now — camera + "Upload"
   when the tile is empty, camera + "Change" over the artwork when it is not — so painting the placeholder's own
   landscape glyph underneath it stacked two different icons on top of each other in the empty state.

   Kept in the markup rather than deleted from the view because it is the NO-JS fallback: the field is `hidden`
   until editor-product-icon.js mounts, and if that script ever fails to run the empty box should still read as an
   image slot. Hiding it here costs nothing and keeps that one honest. */
.editor-icon-placeholder { display: none; }
/* Above the placeholder, and `contain` so a non-square stored icon is never distorted. */
.editor-icon-preview img { position: relative; width: 100%; height: 100%; object-fit: contain; }
/* The dxFileUploader's chrome — its select button and drop zone — is never shown: the tile is the whole affordance.
   `display: none` is NOT usable here, because the widget's own <input type="file"> starts inside this element and a
   display:none subtree cannot be clicked or focused; the script moves the input onto the tile, but the widget still
   needs a laid-out host to initialize against. Collapsed to zero instead, and taken out of flow so it cannot add a
   stray gap under the tile. */
.editor-icon-uploader {
    position: absolute; width: 0; height: 0; overflow: hidden; opacity: 0; pointer-events: none;
}

/* The tile: the change target, and the positioning context for the corner Remove.

   `overflow` MUST stay unset here. .editor-icon-preview above clips its image to the rounded box, and the Remove
   button is pinned to overhang that box's corner (negative offsets below) — putting it inside the clipped element,
   or clipping this one, hides the half of the circle that makes it read as a corner badge.

   width: max-content keeps the tile exactly as wide as the 96px preview: as a flex/grid child it would otherwise
   stretch, and the Remove button — positioned against the TILE — would drift away from the image's corner. */
.editor-icon-tile { position: relative; width: max-content; }

/* A <button> wrapping the preview, so it must not look like one: the visible border and radius belong to
   .editor-icon-preview inside it. */
.editor-icon-change {
    display: block; padding: 0; border: 0; background: none; border-radius: var(--rx-radius-sm);
    cursor: pointer; color: inherit;
}
.editor-icon-change:focus-visible { outline: 2px solid var(--rx-accent); outline-offset: 2px; }

/* The file input that covers the tile is styled in dx-bridge.css, NOT here. The theme sets
   `.dx-fileuploader-input { display: none }` and _Layout.cshtml loads this file BEFORE the theme, so the rule has to
   sit after it to win — see the block at the end of dx-bridge.css. The layering across the two files is:
   overlay 1 < file input 3 < corner Remove 4. */

/* The affordance caption. Two resting states, because with an empty tile this is the only prompt the field has:
   permanently visible when there is no icon, hover/focus-only once there is one (so the icon stays readable). */
.editor-icon-overlay {
    position: absolute; inset: 0; z-index: 1;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    /* Literal rgba, not a theme variable: this is a scrim OVER arbitrary user-uploaded artwork, so it has to darken
       both a white logo and a dark one. --rx-surface tracks the theme and would go pale in light mode, leaving the
       white glyph below unreadable on a light icon.

       .85 is not over-cautious. The scrim's job is to carry white 11px text over an image chosen by someone else —
       a pale logo (the common case for a product icon) sits at roughly the same value as the caption, and at .62
       and even .72 it read straight through the letterforms. The icon is still legible underneath at .85; the
       caption is the thing that must not be ambiguous. */
    background: rgba(4, 12, 20, .85); color: #fff;
    font-size: .72rem; font-weight: 600;
    /* Belt and braces for the pathological case — a near-white logo under the caption. Costs nothing on a dark
       scrim and keeps the text readable if the background ever gets lightened again. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
    transition: opacity .12s ease-in-out;
}
/* Empty tile: the prompt IS the content, so it sits over the placeholder permanently and without a dark scrim —
   there is no artwork underneath for a scrim to separate it from. */
/* Margin on the GLYPH rather than `gap` on the flex column. An inline <svg> sits on the text baseline and carries
   the line-box descender below its own box, so a `gap` is measured from the bottom of that descender and reads
   several px tighter than its number — which is why 5px still looked like the two were touching. `display: block`
   drops the baseline entirely, and the margin below is then the real, visible separation. */
.editor-icon-overlay svg { display: block; margin-bottom: 6px; }
.editor-icon-tile:not(.has-icon) .editor-icon-overlay { opacity: 1; background: none; color: var(--rx-muted); }
/* With an icon: hidden at rest so the artwork reads, revealed on hover or keyboard focus. */
.editor-icon-tile.has-icon .editor-icon-overlay { opacity: 0; }
.editor-icon-tile.has-icon:hover .editor-icon-overlay,
.editor-icon-tile.has-icon:focus-within .editor-icon-overlay { opacity: 1; }
/* Touch has no hover, so an overlay that only appears on it is invisible — the change affordance would simply not
   exist on a tablet. Shown permanently there instead, at a weight that still lets the icon read through. */
@media (hover: none) {
    .editor-icon-tile.has-icon .editor-icon-overlay { opacity: 1; background: rgba(4, 12, 20, .45); }
}
@media (prefers-reduced-motion: reduce) { .editor-icon-overlay { transition: none; } }

/* Corner Remove. Overhangs the preview's top-right, and sits ABOVE BOTH the overlay and the file input that covers
   the tile (z-index 3, in dx-bridge.css). This is the highest layer on purpose: the input spans the whole tile, so
   anything below it is unclickable, and Remove is the one control that must stay reachable. */
.editor-icon-remove {
    position: absolute; top: -7px; right: -7px; z-index: 4;
    display: flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0;
    border: 1px solid var(--rx-border); border-radius: 50%;
    background: var(--rx-bg); color: var(--rx-muted);
    cursor: pointer; line-height: 0;
}
.editor-icon-remove[hidden] { display: none; }
.editor-icon-remove:hover, .editor-icon-remove:focus-visible {
    border-color: var(--rx-danger); background: var(--rx-danger); color: var(--rx-bg);
}
.editor-icon-remove:focus-visible { outline: 2px solid var(--rx-accent); outline-offset: 2px; }
.editor-icon-remove:disabled { opacity: .5; cursor: default; }

/* Product icon in the products list. `contain` so a non-square icon is letterboxed rather than distorted; the empty
   span reserves the same box so rows with and without an icon keep identical heights and the column does not jump. */
.editor-grid-icon {
    display: block; width: 28px; height: 28px; object-fit: contain;
    border-radius: var(--rx-radius-sm);
}
.editor-grid-icon--empty { border: 1px dashed var(--rx-border); opacity: .5; }

/* Crop dialog. The stage is square because the output is a 512² square; the canvas is displayed smaller than its
   backing store, which is why the script scales pointer deltas into stage coordinates. */
.editor-icon-crop { display: flex; flex-direction: column; gap: 12px; }
.editor-icon-crop__hint { margin: 0; color: var(--rx-muted); font-size: .85rem; }
.editor-icon-crop__stage {
    /* The checkerboard makes transparency visible: the output is PNG with alpha preserved, and on a plain background
       a transparent logo and a white one look identical while cropping. */
    width: 100%; aspect-ratio: 1; border-radius: var(--rx-radius-sm); overflow: hidden;
    background-color: var(--rx-bg);
    background-image:
        linear-gradient(45deg, var(--rx-border) 25%, transparent 25%, transparent 75%, var(--rx-border) 75%),
        linear-gradient(45deg, var(--rx-border) 25%, transparent 25%, transparent 75%, var(--rx-border) 75%);
    background-size: 16px 16px;
    background-position: 0 0, 8px 8px;
}
.editor-icon-crop__stage canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
.editor-icon-crop__stage canvas:active { cursor: grabbing; }
.editor-icon-crop__zoom { display: flex; flex-direction: column; gap: 4px; }
.editor-icon-crop__zoom span { font-size: .8rem; font-weight: 600; color: var(--rx-muted); }
.editor-details { margin-top: 14px; }
.editor-details summary { cursor: pointer; color: var(--rx-accent); font-size: .85rem; }

.editor-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; min-height: 55px;}
.editor-actions form { margin: 0; }
.editor-btn {
    display: inline-block; padding: 7px 14px; font-size: .85rem; font-weight: 500; cursor: pointer;
    color: var(--rx-fg); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm);
}
.editor-btn:hover { text-decoration: none; border-color: var(--rx-accent); color: var(--rx-accent); }
.editor-btn-primary { background: var(--rx-accent); border-color: var(--rx-accent); color: #fff; }
.editor-btn-primary:hover { background: var(--rx-accent-strong); border-color: var(--rx-accent-strong); color: #fff; }
.editor-tag { display: inline-block; padding: 2px 8px; font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; vertical-align: middle; color: var(--rx-muted); background: var(--rx-surface-2); border-radius: 999px; }
.editor-tag-ai { color: var(--rx-accent-strong); background: var(--rx-accent-soft); }

/* ── Revision history ───────────────────────────────────────────────────────── */
.editor-revisions { margin-top: 14px; border-top: 1px solid var(--rx-border); padding-top: 10px; }
.editor-revisions > summary { cursor: pointer; font-weight: 600; font-size: .9rem; color: var(--rx-fg); }
.editor-revisions[open] > summary { margin-bottom: 10px; }
.revision-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.revision-item { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px; background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); font-size: .85rem; }
.revision-seq { font-weight: 700; color: var(--rx-accent); font-variant-numeric: tabular-nums; }
.revision-when { color: var(--rx-muted); font-variant-numeric: tabular-nums; }
.revision-title { flex: 1 1 160px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--rx-fg); }
.revision-restore { margin: 0 0 0 auto; }

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
    .docs-layout { grid-template-columns: 1fr; gap: 20px; }
    .docs-nav { position: static; max-height: none; overflow: visible; border-right: 0; border-bottom: 1px solid var(--rx-border); padding: 0 0 16px; }
    .docs-switchers { flex-direction: row; flex-wrap: wrap; }
    .docs-switcher { flex: 1 1 140px; }
}

@media (max-width: 600px){
    .editor-head { flex-direction: column; align-items: flex-start;}
    .editor-head .editor-btn, .editor-head-actions { margin-top: 15px; }
}

@media (max-width: 560px) {
    .container { padding: 0 16px; }
    .header_content_right_sidebar { gap: 10px; font-size: .82rem; }
    .docs-article h1 { font-size: 1.6rem; }
    .editor-table thead { display: none; }
    .editor-table, .editor-table tbody,
    .editor-table tr, .editor-table td { display: block; width: 100%; }
    .editor-table tr { margin-bottom: 1rem;padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
    .editor-table td { padding: .4rem 0; border: none; }
    .editor-table td::before { content: attr(data-label); display: block; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; font-weight: bold; color: var(--rx-muted); margin-bottom: .2rem; }
}

/* ── Search ──────────────────────────────────────────────────────────────── */
/* Sized, NOT fluid. `flex-grow: 0` is the point: the search sits WITH the language/AI/theme controls as one more
   right-hand item, and a growing box would swallow the header's free space and become its dominant element. The basis
   is a deliberate width — comfortably larger than the 420px-capped original read, nowhere near filling the bar — and
   `flex-shrink: 1` down to `min-width` lets it give way on narrow screens instead of forcing a horizontal scrollbar.
   `position: relative` is load-bearing: .instant-search-panel below is absolutely positioned against THIS box, so
   dropping it sends the dropdown to the nearest positioned ancestor and detaches it from the input.
   `margin` is 0 because the rail's own `gap` owns the spacing; the old `0 24px` would double it. */
.header-search { position: relative; flex: 0 1 340px; width: 340px; min-width: 200px; margin: 0; }
.header-search input { width: 100%; padding: 10px 18px; border: 1px solid var(--rx-border); border-radius: 999px; background: var(--rx-bg); color: var(--rx-fg); font: inherit; font-size: .95rem; }
.header-search input:focus { outline: none; border-color: var(--rx-accent); }

.search-page { max-width: 820px; }
.search-form { position: relative; display: flex; gap: 10px; margin: 16px 0 24px; }
.search-form input { flex: 1 1 auto; padding: 10px 14px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); color: var(--rx-fg); font: inherit; }
.search-meta { color: var(--rx-muted); font-size: .9rem; }
.search-scope { display: inline-flex; align-items: center; gap: 8px; margin: -8px 0 20px; padding: 5px 6px 5px 14px; font-size: .85rem; color: var(--rx-fg); background: var(--rx-accent-soft); border: 1px solid var(--rx-border); border-radius: 999px; }
.search-scope-name { font-weight: 600; }
.search-scope-clear { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; font-size: .75rem; line-height: 1; color: var(--rx-muted); background: transparent; border-radius: 999px; text-decoration: none; }
.search-scope-clear:hover { color: var(--rx-fg); background: var(--rx-surface-2); text-decoration: none; }
.search-facet { display: flex; }
.search-facet select { padding: 10px 12px; font: inherit; color: var(--rx-fg); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); cursor: pointer; }
.search-facet select:focus { outline: none; border-color: var(--rx-accent); }
.search-group-title { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--rx-muted); margin: 22px 0 6px; }
.search-results { list-style: none; margin: 0; padding: 0; }
.search-result { padding: 14px 0; border-bottom: 1px solid var(--rx-border); }
.search-result-title { font-size: 1.05rem; font-weight: 600; }
.search-result a:hover { text-decoration: none; }
.search-result a:hover .search-result-title { text-decoration: underline; }
.search-result-crumb { display: block; color: var(--rx-muted); font-size: .8rem; margin-top: 2px; }
.search-result-snippet { margin: 6px 0 0; color: var(--rx-fg); font-size: .9rem; }

.editor-notice { padding: 10px 14px; margin-bottom: 18px; border: 1px solid var(--rx-accent); border-radius: var(--rx-radius-sm); background: var(--rx-accent-soft); color: var(--rx-accent-strong); }

/* instant-search dropdown (shared by the header box and the /search box) */
.instant-search-panel {
    position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
    background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
    box-shadow: var(--rx-shadow); max-height: 70vh; overflow-y: auto; padding: 6px;
}
.instant-search-item { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: var(--rx-radius-sm); color: var(--rx-fg); }
.instant-search-item:hover, .instant-search-item.is-active { background: var(--rx-surface); text-decoration: none; }
.instant-search-item-title { font-weight: 600; font-size: .9rem; }
.instant-search-item-meta { color: var(--rx-muted); font-size: .75rem; }
.instant-search-empty { padding: 10px; color: var(--rx-muted); font-size: .85rem; }
.instant-search-scope { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; padding: 6px 8px 8px; margin-bottom: 4px; border-bottom: 1px solid var(--rx-border); }
.instant-search-facet { display: inline-flex; align-items: center; gap: 6px; font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); }
.instant-search-facet select { font: inherit; font-size: .78rem; font-weight: 400; text-transform: none; letter-spacing: 0; padding: 3px 6px; color: var(--rx-fg); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); cursor: pointer; }

/* The header search's own hide rule moved up into the single 930px header block — see the note there. */

/* ── AI assistant (/ask) ─────────────────────────────────────────────────── */
.assistant-answer { margin-top: 20px; padding: 18px 20px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius); background: var(--rx-surface); }
.assistant-answer-body { white-space: pre-wrap; line-height: 1.65; }
.assistant-sources { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rx-border); }
.assistant-sources-label { display: block; font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); margin-bottom: 8px; }
.assistant-sources ul { list-style: none; margin: 0; padding: 0; }
.assistant-sources li { padding: 3px 0; font-size: .9rem; }
.assistant-source-meta { color: var(--rx-muted); font-size: .8rem; margin-left: 8px; }

/* ── Right-rail "In this article" TOC + nav filter (P2-2) ─────────────────── */
/* 280px, MATCHING .docs-layout — this was 260px, and the 20px difference was not deliberate: the document page
   (this layout) and the product page (.docs-layout) render the SAME sidebar, so a narrower column here meant the
   nav's category names wrapped one page and not the other. Widened to give the deeper levels room now that the
   tree indents per level and can reach four. The 20px comes out of the content column (minmax(0, 1fr)), not the
   right rail, which stays 220px. */
.docs-layout-with-toc { grid-template-columns: 280px minmax(0, 1fr) 220px; }
.docs-toc { position: sticky; top: calc(var(--rx-header-h) + var(--rx-bar-h) + 20px); align-self: start; max-height: calc(100vh - var(--rx-header-h) - var(--rx-bar-h) - 40px); overflow-y: auto; font-size: .85rem; }
.docs-toc-title { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); margin-bottom: 8px; }
.docs-toc-list { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--rx-border); }
.docs-toc-list a { display: block; padding: 4px 12px; margin-left: -1px; color: var(--rx-muted); border-left: 2px solid transparent; }
.docs-toc-list a:hover { color: var(--rx-fg); text-decoration: none; border-left-color: var(--rx-accent); }
.docs-toc-l3 a { padding-left: 24px; font-size: .82rem; }

.docs-tree-filter { width: 100%; padding: 6px 10px; margin-bottom: 12px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); color: var(--rx-fg); font: inherit; font-size: .85rem; }
.docs-tree-filter:focus { outline: none; border-color: var(--rx-accent); }

/* 280px here too — the sidebar must not narrow just because the right rail was dropped. Kept in step with the
   3-column rule above deliberately: two values that must match are the usual way this drifts. */
@media (max-width: 1100px) { .docs-layout-with-toc { grid-template-columns: 280px minmax(0, 1fr); } .docs-toc { display: none; } }
@media (max-width: 900px) { .docs-layout-with-toc { grid-template-columns: 1fr; } }

/* ── Markdown alert callouts (NOTE/TIP/IMPORTANT/WARNING/CAUTION) ─────────── */
.markdown-alert { margin: 0 0 16px; padding: 10px 16px; border-left: 4px solid var(--rx-border); border-radius: 0 var(--rx-radius-sm) var(--rx-radius-sm) 0; background: var(--rx-surface); }
.markdown-alert > :last-child { margin-bottom: 0; }
.markdown-alert-title { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: .85rem; text-transform: uppercase; letter-spacing: .03em; margin-bottom: 6px; }
.markdown-alert-note { border-left-color: #4f83ff; } .markdown-alert-note .markdown-alert-title { color: #4f83ff; }
.markdown-alert-tip { border-left-color: #2ea043; } .markdown-alert-tip .markdown-alert-title { color: #2ea043; }
.markdown-alert-important { border-left-color: #8957e5; } .markdown-alert-important .markdown-alert-title { color: #8957e5; }
.markdown-alert-warning { border-left-color: #d29922; } .markdown-alert-warning .markdown-alert-title { color: #d29922; }
.markdown-alert-caution { border-left-color: #cf222e; } .markdown-alert-caution .markdown-alert-title { color: #cf222e; }

/* ── Editor live preview (P1-B2) ──────────────────────────────────────────── */
.editor-preview { margin-top: 10px; padding: 14px 16px; border: 1px dashed var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); max-height: 420px; overflow-y: auto; }
.editor-preview:empty { display: none; }

/* Upload failure notice for the raw-source editor (docs-search.js). It sits between the textarea and its preview,
   so an author who pastes an unsupported or oversized file sees why nothing was inserted. */
.editor-upload-error { margin: 8px 0 0; padding: 8px 12px; font-size: .85rem; color: var(--rx-danger); border: 1px solid var(--rx-danger); border-radius: var(--rx-radius-sm); background: var(--rx-bg); }

/* Destructive actions. On the document editor Delete now sits directly beside Save in a shared .editor-actions row —
   that row is already `display: flex` with a gap, and Save is lifted out of its form (see Views/Editor/Document.cshtml)
   so both buttons are real siblings on one line. No positioning rules are needed here, and deliberately so: two earlier
   drafts tried to pull a separate Delete block onto the save row with a negative margin and failed, because a
   negatively-pulled block still starts at x=0 and landed Delete on top of Save, and because clearing Save with a fixed
   `padding-left` is label-width dependent ("Save document" measures 125px, the German "Dokument speichern" 158px, so
   any fixed offset breaks in one locale or the other).

   THE CONFIRM DIALOG IS THE ONLY GUARD. An earlier layout kept Delete in a separated row specifically so it was never
   next to Save; that separation is gone by request, which means the dxConfirm wired up by editor-confirm.js via
   [data-confirm] is now the sole thing between a stray click and a deleted document. Do not remove it, and do not add
   a path that submits that form without going through the submit event.

   .editor-actions-danger is NO LONGER USED BY ANY VIEW — the category editor was the last holdout and now shares its
   save row too. The rule is kept as the documented shape for a separated destructive row should one be wanted again;
   the top border is the visual break there. Delete it if you are confident nothing will want that layout back. */
.editor-actions-danger { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--rx-border); }
.editor-btn-danger { border-color: var(--rx-danger); color: var(--rx-danger); background: transparent; }
.editor-btn-danger:hover { background: var(--rx-danger); color: #fff; }

/* Publish. FILLED, not outlined like .editor-btn-danger — it sits beside the filled .editor-btn-primary Save and an
   outlined button there reads as the secondary of the pair, which is backwards: publishing is the affirmative end of
   the row. Same shape as .editor-btn-primary, different hue, so the two are siblings rather than lookalikes. */
.editor-btn-success { background: var(--rx-success); border-color: var(--rx-success); color: #fff; }
.editor-btn-success:hover { background: var(--rx-success-strong); border-color: var(--rx-success-strong); color: #fff; }

/* The `hidden` attribute MUST win over .editor-btn's `display: inline-block`. A `display` set in a class beats the UA
   stylesheet's `[hidden] { display: none }`, so a button an adopter hides (editor-danger-button.js sets
   `button.hidden = true` once its dxButton is mounted) stayed on screen and the author saw TWO Delete buttons — the
   server-rendered fallback and the widget, side by side. Same trap as .editor-icon[hidden] above and .mdx-link[hidden]
   in editor-body.css; this re-asserts none at the specificity the class sets inline-block. */
.editor-btn[hidden] { display: none; }

/* Grid action links. The icon+text form (built by iconTextLink in the grid adopters) needs a gap between glyph and
   word; DevExtreme styles its own `dx-link-icon` but not a hand-built link.

   Only the LAYOUT of these links lives here. Their colour and hover fill are in dx-bridge.css, because the Fluent cut
   ships `.dx-datagrid .dx-link` — two classes, and loaded after this file — so a colour set here loses the cascade
   and silently paints stock Fluent blue. That was the original bug; do not move it back. */
.editor-link-action { white-space: nowrap; }
.editor-link-action .dx-icon { margin-right: 4px; vertical-align: middle; }

/* A soft-deleted document in the version editor's list: still readable, but unmistakably not live. Applied to the
   DataGrid row (onRowPrepared) and to the fallback table row, so both views agree. */
.editor-row-deleted { opacity: .6; }
.editor-row-deleted code { text-decoration: line-through; }

.editor-issues { list-style: none; margin: 0; padding: 0; }
.editor-issue { padding: 6px 0; border-bottom: 1px solid var(--rx-border); font-size: .9rem; }
.editor-issue:last-child { border-bottom: 0; }
.editor-issue-kind { display: inline-block; margin-right: 8px; padding: 1px 7px; font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--rx-danger); background: var(--rx-accent-soft); border-radius: 999px; }
.editor-preview::before { content: "Preview"; display: block; font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); margin-bottom: 8px; }

/* ── Offline export link + print (P2-2) ──────────────────────────────────── */
.docs-export { display: inline-block; margin-bottom: 12px; font-size: .8rem; color: var(--rx-muted); }
.docs-export:hover { color: var(--rx-accent); }

@media print {
    header, .footer, .docs-nav, .docs-toc, .header-search { display: none !important; }
    .docs-layout, .docs-layout-with-toc { display: block; }
    .docs-content { max-width: none; }
    .docs-article a { color: inherit; text-decoration: underline; }
    .page { padding: 0; }
}

/* "Was this page helpful?" feedback widget */
.docs-feedback { margin: 40px 0 8px; padding: 16px 18px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius); background: var(--rx-surface); display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.docs-feedback-q { font-weight: 600; font-size: .95rem; }
.docs-feedback-actions { display: inline-flex; gap: 8px; }
.docs-feedback-btn { padding: 6px 16px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); color: var(--rx-fg); cursor: pointer; font-size: .85rem; }
.docs-feedback-btn:hover { border-color: var(--rx-accent); color: var(--rx-accent); }
.docs-feedback-detail { flex-basis: 100%; display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.docs-feedback-detail label { font-size: .82rem; color: var(--rx-muted); }
.docs-feedback-detail textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); background: var(--rx-bg); color: var(--rx-fg); font-family: inherit; resize: vertical; }
.docs-feedback-send { align-self: flex-start; }
.docs-feedback-thanks { color: var(--rx-accent); font-weight: 600; font-size: .9rem; }
/* Mount points for the adopted DevExtreme buttons (docs-feedback.js). The native <button> beside each is hidden, not
   removed, so this only has to sit where that button did. */
.docs-feedback-dx { display: inline-flex; }
.docs-feedback-detail .docs-feedback-dx { align-self: flex-start; }

/* Editor analytics report + reader "What's new" list */
.report-grid { display: grid; gap: 22px; overflow-x: auto; }
.report-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.report-table th, .report-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--rx-border); vertical-align: top; }
.report-table th { font-size: .74rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rx-muted); }
.report-pos { color: var(--rx-accent); font-weight: 600; }
.report-neg { color: var(--rx-danger); font-weight: 600; }
.report-comments { margin: 4px 0 0; padding-left: 18px; color: var(--rx-muted); font-size: .85rem; }
.report-count { font-variant-numeric: tabular-nums; font-weight: 600; }
.docs-product-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.docs-product-head h1 { margin: 0; }
.whatsnew-list { list-style: none; padding: 0; margin: 0; }
.whatsnew-item { padding: 14px 0; border-bottom: 1px solid var(--rx-border); }
.whatsnew-item h3 { margin: 0 0 4px; font-size: 1.02rem; }
.whatsnew-date { color: var(--rx-muted); font-size: .82rem; }

/* ── DevExpress-style layout: product bar, collapsible nav, anchors, pager ── */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

.docs-bar { position: sticky; top: var(--rx-header-h); z-index: 15; background: var(--rx-surface); border-bottom: 1px solid var(--rx-border); }
.docs-bar-inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: var(--rx-bar-h); }
.docs-bar-left { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: .9rem; }
.docs-bar-home { color: var(--rx-muted); white-space: nowrap; }
.docs-bar-sep { color: var(--rx-muted); opacity: .6; }
.docs-bar-product { font-weight: 700; color: var(--rx-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-bar-product:hover { color: var(--rx-accent); text-decoration: none; }
.docs-bar-right { display: flex; align-items: center; gap: 10px; }
.docs-bar-select { padding: 5px 10px; font: inherit; font-size: .82rem; color: var(--rx-fg); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); cursor: pointer; }
.docs-bar-select:focus { outline: none; border-color: var(--rx-accent); }
.docs-bar-link { font-size: .85rem; font-weight: 600; white-space: nowrap; }

/* Collapsible left navigation */
/* `flex: 1` rather than the `width: 100%` this carried before .docs-tree-row existed: at 100% the toggle claimed the
   whole row on its own and pushed the "+" onto a second line. It still fills a row that has no "+" (a reader's), so
   the hit area is unchanged for everyone who was here before. */
/* The toggle is now a caret-only button, sibling to the title heading — split from the older "caret wrapping the
   whole row" shape so a click on the arrow expands and a click on the title does not (a non-op today; a landing-doc
   navigation once that slice lands). `flex: 0 0 auto` on the toggle, and `flex: 1 1 auto` on the heading, keeps the
   hit area for expand small and gives the header row to the title. */
.docs-tree-toggle { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 22px; height: 22px; padding: 0; background: none; border: 0; cursor: pointer; color: inherit; }
/* `flex: 0 1 auto` — shrink-to-fit, so the heading never grows past its text and the "+" that follows it is pushed
   only as far as the label actually needs.

   HISTORY, because the obvious "fix" here is a regression: this was `flex: 1 1 auto`, which made the heading claim
   the whole row and drove the trailing "+" hard against the sidebar's right edge. `.docs-nav` sets
   `overflow-y: auto`, which forces `overflow-x` to `auto` too (the axes cannot be independently visible), so that
   edge is a SCROLL BOUNDARY: the 22px button landed in or past the nav's `padding-right` and was revealed on hover
   while being off-screen. Do not restore `1 1 auto`. The "+" is instead pinned to a fixed right slot by
   `margin-left: auto` on .docs-tree-add, with the row reserving room for it — see there.

   `min-width: 0` still allows the title to shrink and wrap rather than forcing the row wider. */
.docs-tree-group-heading { display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 0; padding: 5px 4px; color: inherit; }
.docs-tree-group-heading .docs-tree-group-title { padding: 0; }
/* Hover cue on the whole row (was on the toggle when the title lived inside it) so the header still lights up on
   hover regardless of whether the pointer is over the caret or the title. */
.docs-tree-row:hover .docs-tree-group-title { color: var(--rx-fg); }
.docs-tree-caret { display: inline-block; font-size: .95rem; line-height: 1; color: var(--rx-muted); transition: transform .12s ease; }
/* Was `> .docs-tree-toggle` — the toggle is now wrapped in .docs-tree-row, so a CHILD combinator would silently stop
   matching and leave the caret pointing right on an open group. Descendant, and scoped to the row that is the group's
   own header so a nested group's caret cannot be turned by its parent's state. */
.docs-tree-group.is-open > .docs-tree-row > .docs-tree-toggle .docs-tree-caret { transform: rotate(90deg); }

/* Category header row: the title (or its toggle) and, for a docs.write holder only, the "add document" affordance.
   The margin the toggle used to carry moved here so the two rows — toggled and plain-span — space identically. */
/* `cursor: pointer` on the whole row because the whole row (caret + title) now toggles the group — see the
   delegated handler in docs-ui.js. */
/* `padding-right` RESERVES the lane the "+" is pinned into by `margin-left: auto`. Without it the button sits flush
   on the row's right edge, which is also the nav's horizontal scroll boundary (`overflow-y: auto` forces
   `overflow-x: auto`), so the two longest labels pushed it to exactly the edge with zero clearance. 4px matches the
   row's own `gap`, so the "+" is inset by the same rhythm that separates the caret from the title. */
.docs-tree-row { display: flex; align-items: center; gap: 4px; margin-top: 12px; padding-right: 4px; cursor: pointer; }
/* Shrink-to-fit, matching .docs-tree-group-heading above and for the same reason: a growing title pushes the "+"
   into the nav's clipped right edge. The root row is the exception — it has no heading wrapper, so its title is a
   direct flex child and IS the thing that must hold the row open (see .docs-tree-row-root below). */
.docs-tree-row .docs-tree-group-title { flex: 0 1 auto; min-width: 0; }
/* The root "Top level" row has no .docs-tree-group-heading, so nothing else claims the slack. Left shrink-to-fit,
   its "+" would sit immediately against the label rather than trailing the row as it did before this change. */
.docs-tree-row-root .docs-tree-group-title { flex: 1 1 auto; }
.docs-tree-row .docs-tree-toggle { margin-top: 0; }
/* The root group's header exists only for authors, so it aligns with the document links below it rather than with a
   toggle's caret — there is no caret on this row to line up with. */
.docs-tree-row-root .docs-tree-group-title { padding-left: 8px; }
/* This row has a "+" but no caret and no enclosing .docs-tree-group, so the delegated handler early-returns on it
   and nothing toggles. The inherited `cursor: pointer` would advertise a click that does nothing — a false
   affordance — so it is reset here. */
.docs-tree-row-root { cursor: default; }
/* HOVER-REVEALED, PER ROW — a deliberate product decision, not an oversight. The "+" is an authoring control on a
   reader-facing page, so it stays out of the way until the author is actually pointing at the row it belongs to.
   Note this is only ever half the gate: the anchor is rendered ONLY when the request holds docs.write (see the
   `canManage` branches in _DocsNav.cshtml), so a reader is never served the markup at all. This rule governs
   presentation for an author, never access.

   `opacity` + `visibility` rather than `display: none`, so the button keeps its 22px box and the row does not
   reflow when it appears — a display toggle would shift the title sideways on every hover.
   `visibility: hidden` is what removes it from the tab order and from the accessibility tree while concealed;
   opacity alone would leave an invisible button focusable, which is worse than always showing it. */
/* POSITIONING. `margin-left: auto` pins the "+" to the row's right edge instead of letting it trail the label,
   which is what made it land at a different x on every row (measured before this: 215, 224, 238, 146, 238 — a
   ragged column that also collided with the nav's scroll boundary on the two longest labels).

   THE MARK IS AN SVG, NOT A "+" CHARACTER, and that is what actually centres it in the square. Flex centring
   aligns a text glyph's LINE BOX perfectly, but the painted "+" is not centred inside its own line box: measured
   in the UI font, its ink runs 4.37px above the baseline against 2.63px below, landing 1.5px LOW in the 22px
   square, with a -1px left side bearing on top. That error is font-dependent, so a hardcoded nudge would simply be
   wrong on the next fallback font. Two centred strokes in a symmetric viewBox have no bearings and no baseline, so
   the mark is centred by construction. See RenderAddIcon in _DocsNav.cshtml.

   No `font-size`/`line-height` here any more: they existed only to tune that text glyph and now govern nothing.

   `flex: 0 0 auto` keeps the 22px square from being shrunk by a long adjacent title. */
/* `padding: 0` and `margin: 0 0 0 auto` are stated EXPLICITLY, and the selector is doubled up
   (`.docs-tree-list .docs-tree-add`) to make them STICK. This is a real bug fix, not defensive noise:
   `.docs-tree-list a { padding: 5px 10px }` above is specificity (0,2,0) and beats a bare `.docs-tree-add` (0,1,0),
   and EVERY NESTED category row lives inside a `ul.docs-tree-list` — so the document-link padding was landing on
   the "+" of every sub-category, pushing its icon 7px right and 2px down inside the square. Only the top-level row,
   which has no list ancestor, escaped. Measured: margins 10/-4/5/1 on nested rows against 3/3/3/3 at the top level.

   THE `auto` IS LOAD-BEARING — do not flatten this to `margin: 0`. It is what pins the button to the row's right
   edge; without it the button trails each label and the column scatters (measured: two distinct x-positions, 179
   and 272, instead of one). That is the exact regression e8669ba fixed.

   `display: flex` (not inline-flex): the element is a fixed 22px box positioned by the parent flex row, so it never
   needs inline flow, and block-level avoids any inline baseline participation around it. */
.docs-tree-add,
.docs-tree-list .docs-tree-add { flex: 0 0 auto; margin: 0 0 0 auto; padding: 0; display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: var(--rx-radius); color: var(--rx-muted); text-decoration: none; opacity: 0; visibility: hidden; transition: opacity .12s, color .12s, background .12s; }
/* `display: block` kills the inline baseline gap that would otherwise re-introduce a sub-pixel vertical offset —
   an inline SVG sits on the text baseline like a glyph, which is the very thing this change exists to avoid. */
.docs-tree-plus { display: block; flex: 0 0 auto; }
/* Revealed by hovering ITS OWN row (not the tree), and by KEYBOARD focus.
   `:has(:focus-visible)`, NOT `:focus-within`. `:focus-within` matches any focus including a MOUSE CLICK, and the
   caret button retains focus after a click — so expanding a group (the normal way to use this sidebar) left that
   row's "+" pinned visible until focus moved elsewhere. `:focus-visible` is the browser's own "focus that deserves
   a visible ring", i.e. keyboard/AT navigation and not a pointer click, which is exactly the case that needs the
   reveal. The trailing `.docs-tree-add:focus-visible` covers the button itself once tabbed onto — :hover never
   fires for a keyboard user, so without it the control would be unreachable without a mouse. */
.docs-tree-row:hover > .docs-tree-add,
.docs-tree-row:has(:focus-visible) > .docs-tree-add,
.docs-tree-add:focus-visible { opacity: .6; visibility: visible; }
.docs-tree-add:hover, .docs-tree-add:focus-visible { opacity: 1; color: var(--rx-accent); background: var(--rx-accent-soft); text-decoration: none; }
/* Touch-only devices have no hover state at all, so a hover-only affordance would be unreachable there — the "+"
   is shown unconditionally instead.
   `any-hover: none` — NOT `(hover: none), (pointer: coarse)`. That comma is a media query LIST, i.e. OR, and
   `pointer: coarse` describes only the PRIMARY pointer: a touchscreen laptop, a pen-enabled display or a hybrid
   device reports coarse while still having a working mouse. On those machines the OR matched, this rule won on
   cascade order at equal specificity, and the "+" was pinned permanently visible — hover appeared to do nothing.
   `any-hover: none` is true only when NO available input can hover, which is the condition actually meant here. */
@media (any-hover: none) {
    .docs-tree-add { opacity: .6; visibility: visible; }
}
.docs-tree-children { display: none; }
.docs-tree-group.is-open > .docs-tree-children { display: block; }
.docs-tree-subtitle { padding-left: 8px; }
.docs-tree-list a.is-active { background: var(--rx-accent-soft); color: var(--rx-accent-strong); font-weight: 600; box-shadow: inset 2px 0 0 var(--rx-accent); }

/* Right-rail scroll-spy active state */
.docs-toc-list a.is-active { color: var(--rx-accent); border-left-color: var(--rx-accent); font-weight: 600; }

/* Heading anchor links */
.docs-article h2, .docs-article h3 { scroll-margin-top: calc(var(--rx-header-h) + var(--rx-bar-h) + 16px); }
.heading-anchor { margin-left: 8px; color: var(--rx-muted); font-weight: 400; text-decoration: none; opacity: 0; transition: opacity .12s; }
.docs-article h2:hover .heading-anchor, .docs-article h3:hover .heading-anchor, .heading-anchor:focus { opacity: 1; text-decoration: none; }

/* Code copy button */
.docs-article pre.has-copy { position: relative; }
.code-copy { position: absolute; top: 8px; right: 8px; display: inline-flex; align-items: center; justify-content: center; padding: 5px; color: var(--rx-muted); background: var(--rx-bg); border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); cursor: pointer; opacity: 0; transition: opacity .12s, color .12s, border-color .12s; }
.docs-article pre.has-copy:hover .code-copy, .code-copy:focus { opacity: 1; }
.code-copy:hover { color: var(--rx-accent); border-color: var(--rx-accent); }
.code-copy.is-copied { opacity: 1; color: var(--rx-accent); border-color: var(--rx-accent); }

/* Previous/next article pager */
.docs-pager { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 40px 0 8px; }
.docs-pager-link { display: flex; flex-direction: column; gap: 4px; padding: 14px 16px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius); background: var(--rx-surface); transition: border-color .15s, box-shadow .15s; }
.docs-pager-link:hover { text-decoration: none; border-color: var(--rx-accent); box-shadow: var(--rx-shadow); }
.docs-pager-next { text-align: right; }
.docs-pager-dir { font-size: .75rem; color: var(--rx-muted); text-transform: uppercase; letter-spacing: .04em; }
.docs-pager-title { font-weight: 600; color: var(--rx-fg); }

.docs-product-title { margin: 0 0 8px; }

@media (max-width: 700px) {
    .docs-bar-inner { flex-wrap: wrap; gap: 10px; padding-top: 8px; padding-bottom: 8px; }
    .docs-bar-link { display: none; }
}
@media (max-width: 560px) {
    .docs-pager { grid-template-columns: 1fr; }
}
/* .docs-edit-btn joins these: it is an authoring control, and a printed page is a reader's artifact. (The sidebar's
   "+" needs no rule — .docs-nav is already hidden in print.) */
@media print { .docs-bar, .docs-pager, .code-copy, .heading-anchor, .docs-edit-btn { display: none !important; } }

/* Ask-AI streaming indicator. Rendered hidden by Razor and shown by docs-ask.js only while [data-ask-body] carries
   aria-busy, i.e. only while a stream is actually running. */
.assistant-status { display: flex; align-items: center; gap: 10px; margin-top: 18px; color: var(--rx-muted); font-size: .9rem; }
.assistant-status-spinner { display: inline-flex; }
/* The adopted question box grows to fill the row the native input used to occupy; the button keeps its natural size. */
.assistant-ask-box { flex: 1 1 auto; min-width: 0; }
.assistant-ask-button { flex: 0 0 auto; }

/* Search page: same arrangement — the adopted query box fills the row, facets and button keep their natural size. */
.search-page-box { flex: 1 1 auto; min-width: 0; }
.search-page-button { flex: 0 0 auto; }
.search-facet-select { min-width: 150px; }

/* Docs header bar: the adopted version/language switchers. Narrow by design — they sit in a dense bar beside the
   breadcrumb, and the version labels and locale codes they show are short. */
.docs-bar-switch { min-width: 110px; }

/* The adopted TOC filter fills the sidebar width the native input used to occupy. */
.docs-tree-filter-box { width: 100%; margin-bottom: 12px; }
