/* HIRMS component styles — every value comes from docs/design-tokens.css.
   Design-system rules baked in here rather than per screen:
   · DataGrid collapses to cards ≤ 820px
   · SidePanel becomes a bottom sheet on mobile
   · touch targets ≥ 44px on kiosk/portal density
   · accent green is reserved for financial confirmations */

@import url('tokens.css');
@import url('hsl-brand.css');   /* HSL corporate identity overlay */

*, *::before, *::after { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    font-family: var(--f-ui);
    font-size: var(--t-md);
    line-height: var(--lh-base);
    color: var(--ink);
    background: var(--paper);
    /* NO GRID WASH. The 24px hairline lattice that used to sit here is gone —
       a v1 "instrument" device, absent from Brand Book v2.0, and at the new
       aurora strength it read as noise rather than texture. Client direction,
       19 Aug 2026.

       There were TWO lattices, and both are gone: this page canvas, and
       .hs-welcome-rail-grid on the front door, which was hardcoded to the old
       warm paper and so invisible to both the --grid-line grep and the CI hex
       guard. Verified by measurement, not by reading: computed
       background-image on <body> is `none` across all 192 captured surfaces.

       --grid-line survives as a token but now has NO consumer. It is kept
       because tenant branding may want a wash back one day; note that the
       handoff claimed Aurora.razor paints a sibling lattice, and it does not —
       Aurora renders three radial blobs and nothing else. */
    -webkit-font-smoothing: antialiased;
    /* tnum is not cosmetic. It is what makes the money columns in the claims
       queue and the requisition run line up; without it the workbench looks
       broken at a glance. cv05 and ss03 are the Inter character variants the
       book specifies in §05. */
    font-feature-settings: var(--ff-num);
}

/* dc.html consistently sets every heading to --w-bold (700), not --w-semi
   (600) — confirmed at dc.html:150,159,203,227,283,315,330,369,398,446,482,
   503,528,543,576,685,767 etc. UI-DESIGN-SYSTEM-AUDIT §4.9. */
/* The book's scale is size PLUS line-height PLUS tracking, as a set. Taking
   only the sizes leaves headings smaller but still on the old 1.14 ratio with
   no negative tracking, which reads as generic — the opposite of the point.
   --lh-tight stays as the fallback for h4, which has no step of its own. */
h1, h2, h3, h4 { font-family: var(--f-disp); font-weight: var(--w-bold); line-height: var(--lh-tight); margin: 0; }
h1 { font-size: var(--t-2xl); line-height: var(--lh-2xl); letter-spacing: var(--tr-2xl); }
h2 { font-size: var(--t-xl); line-height: var(--lh-xl); letter-spacing: var(--tr-xl); }
h3 { font-size: var(--t-lg); line-height: var(--lh-lg); letter-spacing: var(--tr-lg); }
a { color: var(--brand); text-decoration: none; }
/* The offset keeps the rule clear of descenders, so an underlined link stays
   legible rather than having its g's and y's struck through (dc.html:84). */
a:hover { text-decoration: underline; text-underline-offset: 3px; }
code, .mono { font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .02em; white-space: nowrap; }

/* ── Ellipsis idiom (V1-V6 fix, docs/WORKBENCH-AUDIT-2026-08-06.md) ─────────
   The design system's own repeated pattern (dc.html:462-464 et al.) — put on
   the INNERMOST text-bearing element, never a table cell wholesale, since
   many cells intentionally stack two lines (a name + a mono id, for
   instance). Pair with a `title="@fullValue"` attribute at the call site so
   the full value stays reachable, not just less-clipped. Requires a bounded
   width from somewhere up the tree (an explicit column Width, a fixed
   sidebar track, a flex child with min-width:0) — this rule alone does
   nothing without one. */
.hs-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* ── Money atom (§3.41) ────────────────────────────────────────────────────
   Tabular numerals are guaranteed only here — the design-system rule for
   stopping KPI figures jittering as they update. Tone maps to the --pos/--neg
   tokens, defined and previously unused. */
.hs-money { font-variant-numeric: tabular-nums; }
.hs-money.pos { color: var(--pos); }
.hs-money.neg { color: var(--neg); }

/* ── The accessibility floor ─────────────────────────────────────────────────
   Four base rules the design system states as non-negotiable (dc.html:1081-84),
   kept together because they are the ones that apply to everything and so
   belong to no component in particular. */

/* Visible focus, everywhere, once (dc.html:81). Before this, the whole codebase
   granted focus in two places — the form fields below and .hs-landing-card — so
   every nav link, button, chip, tab, sortable header and pager control gave a
   keyboard user no sign of where they were. In a workbench an assessor drives
   from the keyboard through a queue of hundreds, that is not a rough edge; it
   is the difference between usable and not. The universal selector is the whole
   point, and the design says so in as many words: focus belongs "on every
   focusable element, not 8 isolated rules" (dc.html:1081). A per-component
   focus rule is a per-component omission waiting to happen.

   Note the border-radius: it applies to the focused element, not just the
   outline, so a pill-shaped .hs-chip squares off to 3px while focused. That is
   the design's own rule copied faithfully; if the corner-popping reads badly on
   the round controls, drop the border-radius line rather than the selector. */
*:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

/* Interactive controls that remove their own border/background need an explicit
   focus ring too — a chip, tab, sort header or nav link that turns invisible on
   focus is a keyboard user's dead end (DESIGN-HANDOFF §P2.9). The global rule
   above already catches most; these restate it for elements that paint their own
   focus target (pill corners pop to 3px, matching the design file's rule). */
.hs-chip:focus-visible,
.hs-tab:focus-visible,
.hs-th-sort:focus-visible,
.hs-nav a:focus-visible,
.hs-appbar a:focus-visible,
.hs-bell-btn:focus-visible,
.hs-pager button:focus-visible,
.hs-btn:focus-visible,
.hs-welcome-row:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

/* Placeholder text. The browser default is the input's colour at reduced
   opacity, which lands somewhere unpredictable and usually too faint; naming
   the colour and forcing opacity to 1 makes it a deliberate value we can
   contrast-check (dc.html:82). */
::placeholder { color: var(--ink-3); opacity: 1; }

/* First tab stop on every shell: a way past the navigation. Without it a
   keyboard or screen-reader user tabs through the whole sidebar on every page
   before reaching the thing they came for. Parked off-screen with a negative
   offset rather than display:none, because a hidden element cannot be focused
   and this one has to be (dc.html:102). */
.hs-skip {
    position: absolute; left: -9999px; top: 8px; z-index: 99;
    padding: 10px 16px;
    background: var(--ink); color: var(--paper);
    border-radius: var(--r-sm); font-weight: var(--w-semi);
}
.hs-skip:focus { left: 12px; }

/* Available to assistive technology, absent from the page. For the heading a
   route needs but the design does not draw — the kiosk's h1, for instance,
   where the visible heading is the step title but a screen-reader user still
   has to be told what they have landed on. Not display:none and not
   visibility:hidden: both remove the element from the accessibility tree, which
   is the one thing this must not do. */
.hs-visually-hidden {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Some people are made ill by movement — vestibular disorders, migraine — and
   say so at the operating system. Honour it: near-zero rather than zero, so
   transitionend and animationend still fire and nothing that waits on them
   hangs (dc.html:91). */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
    }
}

/* ── App shell ─────────────────────────────────────────────────────────────
   The design file's workbench frame (dc.html:549-624): a masthead across the
   top, a sidebar nav on the left, content to the right. Three grid rows — the
   top bar is sticky, the nav and main share the remaining row.

   height (not min-height) + overflow:hidden pins the shell to exactly one
   viewport: the mast's row is auto-sized off the top, and the nav/main row
   gets exactly what's left. Without a definite height there neither the mast
   nor the nav can stay put — the grid just keeps growing and the whole page
   scrolls under a "sticky" mast that has no further ancestor to stick against.
   Restored to the old page-scrolling behaviour below 820px, where the nav
   becomes a horizontal strip and a fixed-height shell would clip it. */
/* background here, not only on body: data-theme lives on THIS element, so
   --paper resolves to the themed value inside it. body sits outside the
   wrapper and would still resolve the light :root value in dark mode. */
/* No box at all. This element exists only to carry data-theme high enough that
   MudBlazor's portalled providers (popover, dialog, snackbar) sit INSIDE the
   themed subtree — custom properties inherit down the DOM, which display:contents
   does not interrupt, while generating no box that could disturb layout,
   stacking, or the fixed positioning dialogs and snackbars rely on. */
.hs-theme-root { display: contents; }

/* Anything Mud renders a container for inherits MUD's text colour, not ours.
   .mud-drawer sets `color` from --mud-palette-drawer-text, so every descendant
   that does not set a colour of its own inherits it — and inside SidePanel that
   is the line descriptions, the amounts, .hs-kv dd and .hs-panel-head h3, while
   .hs-phone-section-title and .hs-kv dt are immune because they set
   var(--ink-2) explicitly.

   Measured at 1.02:1 — dark navy on a dark panel — when data-theme was moved to
   dark WITHOUT MudThemeProvider.IsDarkMode. In shipped code those two are two
   renderings of ONE C# value and always move together, so there is no live path
   to it today. This rule removes the possibility rather than relying on that:
   the panel takes its ink from the HSL token, so the two can never diverge here
   again regardless of what Mud's palette is doing.

   It is the mirror of the failure .hs-theme-root above was added for. That one
   was our tokens resolving light inside Mud's container; this is Mud's colour
   inheriting into ours. Same root — two colour systems in one subtree — and
   both are closed by naming the owner explicitly at the boundary. */
.hs-panel, .hs-panel-body, .hs-panel-head { color: var(--ink); }

/* The lockup swaps to its reversed artwork on dark. Measured: hsl-logo.png's
   wordmark is rgb(0,13,58) -- 1.02:1 on the #0A1230 canvas, invisible --
   while hsl-logo-reversed.png is white at 18.39:1 and exists for this.

   The rebrand already shipped this fix twice and both times too narrowly:
   the welcome rail hardcodes the reversed file in its own markup, and the
   masthead gets a white backing chip. Neither covers .hsl-logo, which is the
   shared lockup on the kiosk home, BOTH sign-in panels and /onboard -- the
   screens a user meets before any other.

   content: url() on an <img> replaces the rendered image while leaving src,
   alt text and layout alone, so no call site needs to know about the theme. */
/* Repointed to the kit's own -white vector (2026-08-21). The mechanism is
   unchanged and still correct: content: url() swaps the rendered image while
   leaving src, alt and layout alone, so no call site needs to know about the
   theme. What changed is that both halves are now vectors from one kit, cut
   from the same master — the PNG pair they replace is the pair that produced
   eight half-a-pair defects, because two independently exported rasters can
   drift and these cannot.

   The PNGs stay on disk deliberately (product-owner: "keep fallback just in
   case"). Nothing references them any more, so they cost only bytes, and
   reverting is one string in each of two places rather than a re-export. */
[data-theme="dark"] .hsl-logo { content: url('../img/hsl-lockup-compact-white.svg'); }
/* 236px, not the design file's 194px (dc.html:566-571), since 2026-09-07.
   The 194 was measured against six FLAT rows; the grouped rail indents a
   row under its header by --s3 + --s2 + 1px, and at 194px the longest labels
   ("Ex-gratia requests", "Dashboard & Reports") clipped to an ellipsis on the
   live workbench (owner screenshot). 42px is the indent back, plus the
   widest label's margin at 13px Inter; the row's title attribute still
   carries the full text for anything a tenant brand name outgrows. */
.hs-shell { display: grid; grid-template-rows: auto 1fr; grid-template-columns: 236px 1fr; height: 100vh; overflow: hidden; background: var(--paper); color: var(--ink); }
.hs-mast { grid-column: 1 / -1; }

/* ── Masthead ──────────────────────────────────────────────────────────────
   The design file's sticky masthead (dc.html:105): glass, with the hamburger,
   the H+ mark + HSLOnline, a search box, then the system controls. The
   notification bell and theme toggle live here, at the top.

   Classes match AppMasthead.razor's actual markup (hs-mast-*). This block
   used to style hs-topbar-* — a class family nothing in the codebase renders
   (grep over every .razor file under src turns up zero hits) — so the masthead rendered
   fully unstyled, and without grid-column:1/-1 the shell's grid auto-placed
   the nav beside it in row 1 and shoved main content into the 248px column. */
.hs-mast {
    position: sticky; top: 0; z-index: 40;
    display: flex; align-items: center; gap: var(--s4);
    /* Three independent right-pushes, so no single failure can bunch the
       cluster against the brand: the spacer grows (flex:1 below), the bell
       carries margin-left:auto, and space-between spreads first/last as a
       final fallback. The live-VM "header bunched left" reports all traced to
       a browser holding an older stylesheet; these make the layout correct
       even against one. */
    justify-content: space-between;
    padding: var(--s3) var(--s5);
    /* Two product-owner asks, 2026-08-09, both landing on this one property.
       "Make background white": the old value was the glass treatment —
       `color-mix(in srgb, var(--paper) 55%, transparent)` over whatever sat
       behind it (the aurora, the page) — which read as a dull grey scrim,
       not white, on the screenshot that prompted the ask. --paper at full
       strength IS this design's white (`--i-25` #FBFBF9 in light mode, the
       same token every opaque card/panel calls "paper"), so this stays a
       token reference rather than a hand-picked #fff, and it is opaque in
       BOTH themes — dark mode's --paper is a near-black opaque, not a
       translucent one either, and the dark-mode logo backing-chip rule two
       blocks down already assumed a non-white ground, so it is unaffected.
       backdrop-filter is dropped with it: nothing shows through an opaque
       bar for it to blur any more.
       "Add a splash of teal", pointing at the top-right corner near the
       language/theme/bell/account cluster: a second background-image layer
       UNDER the opaque colour, using the multi-layer form of the `background`
       shorthand (comma-separated layers paint image-over-color, and only the
       LAST, colour-only layer may set background-color) — so --paper stays
       the trailing solid layer and the bar stays opaque, while the gradient
       shows through only where its own stops are not fully transparent.
       Uses --brand — the app's one teal token (tokens.css: "le teal 600
       porte les actions") — at low alpha via color-mix, so it re-derives
       correctly in dark mode from this one declaration instead of needing a
       second, hand-tuned dark value.
       Widened 2026-08-10: a plain `circle` sized to the corner's own
       farthest-corner radius faded out well before reaching the bar's own
       middle (the empty flex-spacer between the wordmark and the settings
       cluster), so that whole stretch read as plain white against a splash
       that only ever touched the far-right icons. A wide, short `ellipse`
       hugging the top edge reaches across the middle without growing tall
       enough to compete with the logo/wordmark on the left; alpha nudged
       down slightly (16%→13%) since it now covers more of the bar. */
    background: radial-gradient(ellipse 820px 150px at 100% 0%,
        color-mix(in srgb, var(--brand) 13%, transparent), transparent 100%),
        var(--paper);
    border-bottom: 1px solid var(--rule);
}
.hs-mast-icon {
    display: grid; place-items: center;
    width: var(--ctl); height: var(--ctl); flex: none;
    border: none; background: transparent; color: var(--ink-2);
    border-radius: var(--r-sm); cursor: pointer;
}
.hs-mast-icon:hover { background: var(--surface-2); }
/* dc.html draws the hamburger with no onClick and no collapsed/rail sidebar
   in any frame — it only has real work below 820px, where the sidebar
   becomes a drawer. Shown again in the 820px media query below. */
.hs-mast-toggle { display: none; }
.hs-mast-brand { display: flex; align-items: center; gap: var(--s3); flex: none; }
.hs-mast-logo-wrap { display: flex; align-items: center; flex: none; }
/* 28px → 35px (product-owner ask, 2026-08-09: "increase logo size by 25%" —
   28 * 1.25 = 35 exactly). This is a few px taller than every other control
   in the bar (.hs-mast-icon/.hs-mast-avatar are --ctl 32px / 28px), so it is
   now what sets the bar's own height — the bar has no explicit height of its
   own, only `align-items:center` + padding around its tallest child, and
   that child changed. It grows the measured ~54-57px bar dc.html:553-564
   specs by roughly the same few px, not shrinks it — the alternative was
   capping the increase at 32px (level with the icon buttons) to hold the bar
   height exactly, but that stops short of the literal "25%" ask for a
   difference too small to read as a broken spec. `.hs-nav`'s sticky
   `top`/`max-height` below are measured against the bar's real height, not
   this literal number, and were re-measured and updated alongside this. */
/* 42px, not 35. The kit's floor is a WIDTH (148px for a horizontal lockup) and
   this asset is 710.3x200, so height drives it: 148 / (710.3/200) = 41.7px is
   the smallest height that clears the floor. 42px, rounded up — 35px rendered
   about 124px wide and was under it. */
.hs-mast-logo { display: block; height: 42px; width: auto; }
/* THE CHIP IS GONE, and removing it is the other half of adopting the kit.
   
   It existed because the dark masthead carried the FULL-COLOUR lockup, which the
   CI manual forbids recolouring — so the manual's own fix is a white backing
   chip to give it a light ground. 3267dd3 repointed the dark swap to the kit's
   -white lockup and left this rule in place, which put white artwork on a white
   chip: the masthead logo rendered as a blank white rectangle in dark mode.
   
   Caught in a screenshot, not by a guard, and it is the same half-a-pair shape
   this rebrand has now hit nine times — I changed the artwork and left its
   compensating rule behind. The white lockup needs no ground; that is what
   being the reversed cut MEANS. */
.hs-mast-word { font-family: var(--f-disp); font-weight: var(--w-bold); font-size: var(--t-sm); letter-spacing: -.3px; }
.hs-mast-spacer { flex: 1; }
/* Language + Theme: an inline toolbar row down to 481px, a gear-icon dropdown
   below that. The gear stays hidden until the row itself would be cramped —
   see the ≤480px override at the bottom of this file, which is the ONLY
   thing that changes; .hs-mast-inline-settings holds the exact same
   LangToggle/ThemeToggle markup at every width. */
.hs-mast-settings { position: relative; }
.hs-mast-settings-toggle { display: none; }
.hs-mast-inline-settings { display: flex; align-items: center; gap: var(--s3); }
.hs-mast-account { position: relative; }
.hs-mast-avatar {
    width: 28px; height: 28px; border-radius: var(--r-full);
    background: var(--brand-soft); color: var(--brand);
    display: grid; place-items: center; font-weight: var(--w-semi); font-size: 10.5px;
    flex: none; border: 1px solid transparent; cursor: pointer;
}
.hs-mast-avatar:hover, .hs-mast-avatar.active { border-color: var(--brand-line); }
.hs-mast-initials { pointer-events: none; }
/* Same anchored-dropdown shape as .hs-bell-panel — the account menu is the
   masthead's other overlay and should read as the same pattern.

   position/right/top/z-index deleted 2026-08-16 — same dead-CSS collapse bug
   as .hs-bell-panel (see its history note): this lands on the list inside
   MudMenu's own popover now, and a second position:absolute here pulled it
   out of flow, collapsing the popover's outer box to 0×0. */
.hs-mast-panel {
    width: min(280px, calc(100vw - var(--s6)));
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-lg); box-shadow: var(--e-3);
    padding: var(--s4); display: flex; flex-direction: column; gap: var(--s3);
}
.hs-mast-who { font-size: var(--t-sm); font-weight: var(--w-semi); }
.hs-mast-detail { font-size: var(--t-xs); color: var(--ink-2); margin-top: 2px; }
.hs-mast-panel-section { padding-top: var(--s3); border-top: 1px solid var(--rule); display: flex; flex-direction: column; gap: var(--s2); }

/* ── Sidebar nav ───────────────────────────────────────────────────────────
   The design file's nav (dc.html:566-571): a 194px column, items with a 3px
   tick on the left, an active item filled with brand-soft + brand tick, and a
   badge riding the right edge. Glass, like the masthead.

   Two layers, not one, since 2026-08-10. `.hs-nav` is a transparent sticky/
   scroll SHELL only — it owns the positioning math (sticky offset, max-
   height, the top gap) but paints nothing. `.hs-nav-inner` (added in
   MainLayout.razor, wrapping the Capture button + nav rows) is the actual
   visible glass panel — background, border, the row list. Splitting them is
   what makes the top gap read as EMPTY PAGE rather than a blank rectangle of
   the sidebar's own surface colour: before this split, `.hs-nav` painted its
   background across the whole padded box including the gap, so the "move
   the sidebar down" fix from the day before looked like an unstyled box
   floating above the real content instead of the panel hanging mid-page —
   direct product-owner correction on a live screenshot. */
.hs-nav {
    /* Reverted to --s3 (2026-08-11, product-owner, direct live-screenshot
       feedback during the MudBlazor migration — supersedes the --s10 bump
       below, kept for history). The --s10 top offset was added 2026-08-09 to
       align the nav's top (the Capture button) with the main column's
       heading row, since .hs-main's own padding + eyebrow/h1 stack sits
       lower than an unpadded nav would. Seen live, that gap reads as dead
       space floating above the nav card — nothing in the nav occupies it the
       way the main column's eyebrow text does — so the alignment it bought
       cost more than it was worth. Flush-under-the-masthead, with a small
       --s3 breathing gap, is the current call. Reset to 0 in the ≤820px row
       below either way — the nav is a horizontal strip under the mast there,
       not a column beside main, so there is nothing to align against. */
    padding-top: var(--s3);
    overflow-y: auto;
    /* Pinned under the masthead instead of scrolling away with the page —
       the masthead is sticky at top:0, so the nav sticks just below it, not
       at 0, or the two would overlap. Bounded to the viewport height minus
       that offset so a nav taller than the window still scrolls internally
       rather than spilling under the fold.
       60px, not the 54px dc.html:553-564 specs and AppMasthead's own header
       comment cite: raising .hs-mast-logo 28px→35px (25% bigger, product-
       owner ask 2026-08-09) made the logo the bar's tallest child in place
       of the 32px icon buttons, which grows the bar's own auto height by
       the same few px. Re-measured in headless Chrome after that change
       rather than computed by hand, since the bar's height was never an
       authored constant to begin with — only padding + tallest-child, same
       as before. Keep this in step with .hs-mast-logo's height; a future
       change to either without the other reintroduces the overlap this
       exists to prevent. */
    position: sticky; top: 60px; align-self: start;
    max-height: calc(100vh - 60px);
}
.hs-nav-inner {
    background: var(--surface);
    border-right: 1px solid var(--rule);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    padding: 0 var(--s2) var(--s3);
    display: flex; flex-direction: column; gap: 2px;
}
.hs-brand { font-family: var(--f-disp); font-size: var(--t-lg); font-weight: var(--w-bold); color: var(--brand-lo); margin-bottom: var(--s4); padding: 0 var(--s2); }
.hs-brand small { display: block; font-family: var(--f-ui); font-size: var(--t-2xs); font-weight: var(--w-reg); color: var(--ink-3); letter-spacing: .08em; text-transform: uppercase; }
/* ── The collapsible group header and its rows ────────────────────────────
   .hs-nav-group used to be a plain uppercase <div> label, and stayed in this
   file unused for the month the rail was flat. It is a <button> now (owner ask
   2026-09-07, argued in MainLayout.razor §2): same typographic treatment, plus
   a hit area, a hover state and a chevron.

   All the button resets are needed. A <button> brings its own font, its own
   1px border, `background: buttonface` and centred text; without the resets a
   section header renders as an actual grey push-button in the middle of a list
   of links, which is the opposite of the "these are containers, not
   destinations" distinction the header exists to draw. */
.hs-nav-group {
    display: flex; align-items: center; gap: var(--s2);
    width: 100%; min-height: 30px;
    margin: var(--s4) 0 2px; padding: 0 var(--s3) 0 var(--s2);
    background: none; border: 0; border-radius: var(--r-sm);
    font-family: inherit; font-size: var(--t-2xs); font-weight: var(--w-semi);
    text-transform: uppercase; letter-spacing: .1em; text-align: left;
    color: var(--ink-3); cursor: pointer;
}
/* The first header sits directly under the capture CTA, where the --s4 top
   margin above reads as a gap nobody asked for. */
.hs-nav-group:first-of-type { margin-top: var(--s2); }
.hs-nav-group:hover { background: var(--surface-2); color: var(--ink-2); }
.hs-nav-group-label { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* Two borders on a square, rotated: right when collapsed, down when open. The
   transform is animated and nothing else is — the rows appear and disappear
   with no height transition, because animating to `auto` needs either a fixed
   height or a JS measure, and the house rules have no room for the second. The
   global prefers-reduced-motion rule at the top of this file already zeroes
   this along with everything else. */
.hs-nav-chevron {
    width: 7px; height: 7px; flex: none; margin-left: 2px;
    border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg); transition: transform 120ms ease;
}
.hs-nav-group[aria-expanded="true"] .hs-nav-chevron { transform: rotate(45deg); }
/* The rail down the left of an open group's rows is what makes the indent read
   as containment rather than as a stray margin. It stops at --rule-2 so it
   never competes with the 3px brand tick sitting 8px to its right. */
.hs-nav-rows {
    display: flex; flex-direction: column; gap: 2px;
    margin-left: var(--s2); padding-left: var(--s2);
    border-left: 1px solid var(--rule-2);
}
.hs-nav-rows.is-collapsed { display: none; }
.hs-nav a {
    display: flex; align-items: center; gap: var(--s3);
    min-height: 36px; padding: 0 var(--s3);
    border-radius: var(--r-sm);
    color: var(--ink-2); font-size: var(--t-sm); font-weight: var(--w-med);
    text-decoration: none;
}
.hs-nav a:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.hs-nav a.active { background: var(--brand-soft); color: var(--brand); font-weight: var(--w-semi); }
/* MainLayout.razor documents this rule as already existing for "Capturer une
   visite" — it wasn't; the class+element+class selector is needed (0,2,1) to
   outrank .hs-nav a's (0,1,1), a bare .hs-nav-action rule would lose. Without
   it the button rendered with --ink-2 body text on the brand background
   instead of --on-brand, reading as washed-out on every masthead. */
/* FLAT --brand, not the Pulse gradient this inherits from .hs-btn-primary, and
   the reason is book rule 2: ONE Pulse primary action per screen. This CTA is
   persistent NAVIGATION — it sits in the rail on all twenty workbench routes —
   so with Pulse on it, every page that has its own primary ("New member",
   "Upload invoices") rendered two identical gradient buttons at once, and
   /admin/notifications rendered four. Pulse marks the action on THIS screen;
   a standing nav CTA is a different thing and should not compete with it.

   It also fixes a contrast bug that arrived with the theme-flipping --on-brand.
   These selectors are (0,2,1) and beat .hs-btn-primary's (0,1,0), so they
   override the button's colour but NOT its background — which left Midnight
   text on the theme-invariant Pulse gradient in dark, at 3.98:1. A flat --brand
   fill pairs correctly with --on-brand in both themes: 4.51:1 light (white on
   #2D6BFF) and 5.07:1 dark (Midnight on #4C82FF). Setting background here is
   what makes the pair coherent, not just the colour. */
.hs-nav a.hs-nav-action { background: var(--brand); border-color: var(--brand); color: var(--on-brand); font-weight: var(--w-semi); }
.hs-nav a.hs-nav-action:hover { background: var(--brand); color: var(--on-brand); filter: brightness(1.06); }
/* The 3px tick — the design file's status edge on the active nav item.
   Idle rows carry a dim brand tint rather than flat --rule-2 grey: the tick
   is the row's identity mark even at rest, and a flat grey column of eight
   reads as decoration nobody finished, not eight distinct destinations. */
.hs-nav-tick {
    width: 3px; height: 14px; border-radius: var(--r-full);
    background: var(--brand-soft); flex: none;
}
.hs-nav a:hover .hs-nav-tick { background: var(--brand-line); }
.hs-nav a.active .hs-nav-tick { background: var(--brand); }

/* The label and the count badge — dc.html:1209-12 builds the row as
   [3px tick] label [badge] where the badge is mono, 9px, pill-shaped and
   pushed to the row's end with margin-left:auto. The design's active-row badge
   fills brand/on-brand; the idle badge sits on --surface-3 in --ink-2. The
   component renders the badge span only when a count is positive, so no
   display:none workaround is needed for empty badges. */
/* min-width:0 lets the label actually shrink inside the flex row instead of
   pushing the badge (flex:none) past the sidebar's edge — the default
   flex-shrink basis is the text's own intrinsic width, which is wider than
   the sidebar on items like "Console anti-fraude". */
.hs-nav-label { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.hs-nav-badge {
    margin-left: auto;
    font-family: var(--f-mono); font-size: 9px; font-weight: var(--w-semi);
    padding: 2px 6px; border-radius: var(--r-full);
    background: var(--surface-3); color: var(--ink-2);
    flex: none;
}
.hs-nav a.active .hs-nav-badge { background: var(--brand); color: var(--on-brand); }

/* ── Aurora backdrop ────────────────────────────────────────────────────────
   The design file's signature wash (dc.html:96-100): three fixed radial blobs,
   blurred hard, drifting on 28–40s loops behind every surface. Deliberately
   almost invisible — it gives the glass something to refract. `--grid-line`
   (tokens.css:55) is the graph-paper wash that sits on the same layer; the
   aurora adds the colour. prefers-reduced-motion already zeroes the drift via
   the global rule at the top of this file. */
.hs-aurora {
    position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
    /* 85%, matching the design passes: both set auroraIntensity to 85 by default
       and apply it as (intensity / 100) on this layer. Measured out of
       `HSL Apps Rebrand.dc.html` rather than guessed, because the instruction
       reads "raise the layer opacity to 85%" while this layer had no opacity
       declaration at all — i.e. it was already at 100%, and 85% is a reduction.

       The blob alphas are deliberately NOT touched. The three hues are balanced
       against each other in --aurora-1/2/3 and they already carry the design's
       own values, so the layer is the only correct dial. If the aurora ever
       needs to be stronger, raise these three tokens together, never one. */
    opacity: .85;
}
/* The brand mark, watermarked behind every surface.

   Sits INSIDE .hs-aurora, so it is already fixed, already pointer-events:none,
   already aria-hidden, and already reaches all five shells through the one
   component. It is also therefore multiplied by this layer's own opacity:.85 —
   the effective figure is --wm-opacity x .85, which is why the token reads
   higher than the value you see on screen.

   Anchored right and allowed to bleed off the edge, rather than centred. Text
   columns start at the left, so the densest part of the mark sits where the
   fewest glyphs are; and a large centred mark on a finished page reads as a
   stalled loading state, which is the wrong association entirely.

   background-image on a div rather than an <img>: this is decoration, and a
   background cannot be dragged, saved, or picked up by an image-context menu.

   Not animated. The three blobs behind it already drift, and a second moving
   element on the same layer would draw the eye to the background — the opposite
   of what a watermark is for. */
.hs-aurora-mark {
    position: absolute;
    top: 50%; right: -8vw; transform: translateY(-50%);
    width: var(--wm-size); height: var(--wm-size);
    background-image: url('../img/hsl-icon.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    opacity: var(--wm-opacity);
}

/* ── Filter rows ─────────────────────────────────────────────────────────────
   .hs-form-row had NO rule at all. The markup in Audit.razor:41 has been asking
   for one since it was written, so the div computed to display:block and its
   fields each took the full card width and stacked — the "three search boxes
   should be on one row" the product owner ringed. Not a regression; a rule that
   was never authored.

   auto-fit with a minmax track, NOT `repeat(3, 1fr)`, and that is the whole
   design decision here. The row is not always three fields: Audit.razor:46
   renders a fourth (Filter.User) only when _scope.CanSeeEveryone, so a
   platform-ops operator sees four and everyone else sees three. A hardcoded
   three-column grid would wrap that fourth field onto a lonely second row for
   exactly the people who use this screen most. auto-fit lets the track count
   follow the field count.

   240px is the floor because the two date inputs render a native calendar
   picker; below roughly that, Chromium clips the widget rather than the text,
   which is invisible in a screenshot and obvious the moment you click one.
   Under ~740px the fields stack again, which is correct on a phone. */
.hs-form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--s3) var(--s4);
    align-items: end;
}

/* ── Per-SURFACE watermarks ──────────────────────────────────────────────────
   The layer above only reaches the page canvas. The nav rail, the welcome rail
   and the welcome list are OPAQUE and sit over it, so they showed no mark at
   all — which is exactly what the product owner ringed in red on three
   screenshots. These put one on each of those surfaces.

   Painted as ::after over the content rather than behind it, which looks wrong
   written down and is right here: at 4-6% this is a wash, not an occluder, and
   pointer-events:none means it can never take a click. It is also the pattern
   .hs-welcome-rail-blob already uses — those blobs are position:absolute with
   no z-index and therefore already paint above the rail's static content. A
   z-index:-1 alternative would drop the mark BEHIND each surface's own
   background-color and vanish entirely.

   One declaration block for all four so they cannot drift apart; only the
   geometry differs per surface below. */
.hs-nav::after,
.hs-shell::after,
.hs-welcome-rail::after,
.hs-welcome-rows::after {
    content: "";
    position: absolute;
    pointer-events: none;
    background-image: url('../img/hsl-icon.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    opacity: var(--wm-opacity);
}

/* Each surface needs to be a positioning context for its own mark.
   .hs-welcome-rail already declares position:relative and overflow:hidden — not
   repeated here.

   .hs-nav IS DELIBERATELY ABSENT, and that absence is the fix for a defect this
   block shipped.

   It already declares `position: sticky; top: 60px; align-self: start` at :365.
   The first version of this rule added `.hs-nav { position: relative; }` here,
   later in the file at equal specificity — so it won, and `sticky` became
   `relative`. Under sticky, `top: 60px` is the threshold at which the nav
   starts sticking. Under relative, the SAME declaration is an offset, so the
   whole sidebar was pushed 60px down the page: .hs-main sat at y=61 and .hs-nav
   at y=121, which is the gap the product owner spotted. It also silently
   removed the sticky behaviour, so the nav would have scrolled away with the
   page.

   No override is needed at all: sticky is a positioned value and already
   establishes the containing block that .hs-nav::after anchors to.

   I wrote the comment warning about later-wins-per-property in this very block,
   then hit it on a different property in the same rule — avoiding `overflow`
   and clobbering `position`. The lesson is not "watch overflow", it is: when a
   selector already exists elsewhere in the file, READ ITS DECLARATIONS before
   adding a second rule for it. */
.hs-welcome-rows { position: relative; overflow: hidden; }
.hs-shell { position: relative; }

/* The sidebar: low and right, under the nav list's tail rather than behind the
   items. Held fully INSIDE the box — no negative offsets — precisely because
   the rule above deliberately leaves overflow alone: .hs-nav's overflow-y:auto
   makes it a scroll container, so a mark hanging off the edge would produce a
   real scrollbar rather than being clipped. */
.hs-nav::after {
    width: 168px; height: 168px;
    right: 0; top: 50%; transform: translateY(-50%);
}

/* The workbench content column, sitting over the page's standfirst — moved here
   from the far right on direct product-owner instruction ("move the smaller
   watermark to exactly above the TOB text"). It now reads behind the masthead
   rule and the description line rather than behind the right-hand KPI card,
   where it competed with the money figure.

   On .hs-shell and NOT .hs-main on purpose: .hs-main is the shell's one
   scrolling region (overflow-y:auto), so a mark anchored inside it would slide
   away on the first scroll. .hs-shell is height:100vh, overflow:hidden, so this
   holds its place.

   left as a PERCENTAGE, not a pixel offset: .hs-shell's first grid column is a
   fixed 194px nav and the second is 1fr, so the content area's midpoint moves
   with the viewport. A fixed left would drift off the standfirst on a wider
   screen. */
.hs-shell::after {
    width: 224px; height: 224px;
    left: 45%; top: 22px;
}

/* The welcome rail is deep --b-900. Bottom-left, tucked under the standfirst and
   away from the display type. 260 -> 416px, the +60% asked for after seeing it
   live; the offsets grow with it so the mark keeps the same amount of itself on
   screen rather than sliding into the "Health Systems Link" footer link. */
.hs-welcome-rail::after {
    width: 416px; height: 416px;
    left: -90px; bottom: -74px;
}

/* The welcome list column: large and right, bled off the edge, behind the
   numbered rows the same way the canvas mark sits behind the claims grid.
   420 -> 756px, the +80% asked for. At this size it is wider than the column's
   right margin, which is the intent — it reads as a crop of the mark rather
   than a logo placed in a corner. */
.hs-welcome-rows::after {
    width: 756px; height: 756px;
    right: -200px; top: 50%; transform: translateY(-50%);
}

@media (max-width: 700px) {
    /* Two columns become one; a 420px mark behind a single-column list stops
       being texture. The nav is a drawer at this width and is busy enough. */
    .hs-nav::after { display: none; }
    .hs-welcome-rows::after { width: 260px; height: 260px; right: -80px; }
    .hs-shell::after { width: 200px; height: 200px; }
}

@media print {
    .hs-nav::after, .hs-shell::after,
    .hs-welcome-rail::after, .hs-welcome-rows::after { display: none; }
}

/* On a phone the mark would occupy most of the viewport behind a single-column
   list, which is where a watermark stops being texture and starts being a
   backdrop. Smaller, and pushed further off the edge. */
@media (max-width: 560px) {
    .hs-aurora-mark { width: 74vw; height: 74vw; right: -22vw; }
}

/* Print: no watermark behind a claim document. Toner, and the fact that a
   printed claim is evidence rather than a brand surface. */
@media print {
    .hs-aurora-mark { display: none; }
}

.hs-aurora .blob {
    position: absolute; border-radius: var(--r-full); filter: blur(70px);
}
.hs-aurora .blob-1 {
    top: -240px; left: -180px; width: 840px; height: 840px;
    background: radial-gradient(circle at 35% 35%, var(--aurora-1), transparent 62%);
    animation: hxAur1 28s var(--ez-io) infinite;
}
.hs-aurora .blob-2 {
    top: 8%; right: -260px; width: 780px; height: 780px;
    background: radial-gradient(circle at 60% 40%, var(--aurora-2), transparent 60%);
    animation: hxAur2 34s var(--ez-io) infinite;
}
.hs-aurora .blob-3 {
    bottom: -280px; left: 28%; width: 920px; height: 720px;
    background: radial-gradient(ellipse at 50% 50%, var(--aurora-3), transparent 62%);
    animation: hxAur1 40s var(--ez-io) infinite reverse;
}
@keyframes hxAur1 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(70px, -50px) scale(1.14); } }
@keyframes hxAur2 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-80px, 60px) scale(1.08); } }

/* ── Glassmorphism ──────────────────────────────────────────────────────────
   `--glass-blur` was defined in tokens and never consumed (DESIGN-HANDOFF
   §P3.13). The shells and overlays that float over content — the sticky
   appbar, the workbench nav, the side panel, the bell, the picker — get the
   backdrop blur + the inset specular highlight, per dc.html:105. The surface
   tokens are already translucent; the blur is what makes them read as glass
   rather than washed-out. */
/* .hs-confirm/.hs-modal dropped 2026-08-17: both are opaque --paper surfaces, so
   the blur was never visible — it only forced a compositing layer and an extra
   stacking context onto MudBlazor's `.mud-dialog`. That extra context is exactly
   what would re-create the grey-dialog bug fixed below (see .mud-dialog.hs-modal)
   the moment --paper ever goes translucent. */
.hs-appbar, .hs-nav-inner, .hs-panel, .hs-bell-panel {
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
}

/* ── Notched cards ──────────────────────────────────────────────────────────
   The design file's signature clipped corner on KPI tiles and the member card
   (dc.html:403, 848). `--notch` is the kill switch: 0px flattens every notch
   at once, which is what a printer or a tenant wanting a softer corner sets.
   The notch clips the hover shadow too, so the shadow sits on an unclipped
   wrapper instead — .hs-kpi-link already provides that wrapper. */
.hs-kpi, .hs-membercard {
    clip-path: polygon(0 0, calc(100% - var(--notch)) 0, 100% var(--notch), 100% 100%, 0 100%);
}

/* ── Status edge ticks ──────────────────────────────────────────────────────
   The 3px coloured edge that marks a row's status before the pill is read
   (dc.html:459). Applied to DataGrid rows via RowStatus (hs-row-*), nav items
   and claim cards. A tick is the pill's colour standing at the row's edge. */
.hs-tick {
    align-self: stretch; width: 3px; border-radius: var(--r-full); flex: none;
    background: var(--rule-2); opacity: .55;
}
.hs-tick.is-pend, .hs-tick.is-held { background: var(--st-pend-dot); }
.hs-tick.is-eval { background: var(--st-eval-dot); }
.hs-tick.is-redy { background: var(--st-redy-dot); }
.hs-tick.is-rejt, .hs-tick.is-flagged { background: var(--st-rejt-dot); }
.hs-tick.is-paid { background: var(--st-paid-dot); }
.hs-tick.is-brand { background: var(--brand); opacity: 1; }
/* A statused row's rail is the design's first track (dc.html:459,611) — full
   colour, not the faded neutral a no-status row shows. held/flagged are the
   semantic aliases several pages' RowStatus already passes (Finance,
   FraudConsole, Invoices, OcrJobs, PreAuthQueue, TobBrowser) — without them
   here this rule only ever matched pend/eval/redy/rejt/paid, so every one of
   those pages' tick was rendering as the faded default above, not the
   coloured rail its row-level inset shadow already showed. */
.hs-tick.is-pend, .hs-tick.is-held, .hs-tick.is-eval, .hs-tick.is-redy,
.hs-tick.is-rejt, .hs-tick.is-flagged, .hs-tick.is-paid { opacity: 1; }
/* The one scrolling region in the shell: .hs-mast's row is auto-sized and
   .hs-nav scrolls its own overflow (hirms.css .hs-nav), so main is what's left
   to carry page-length content within the shell's fixed viewport height. */
.hs-main {
    padding: var(--s6) var(--s8); max-width: 1600px; overflow-y: auto;
    /* Nothing inside the shell may widen the page. Vertical stays `auto` —
       this is the shell's one scrolling region — and horizontal is clipped
       because a workbench page is a column of cards that reflow, never a
       canvas you pan. Safe for the grids that genuinely do scroll sideways:
       those live in .hs-grid-wrap, which carries its own overflow-x:auto
       (with MudBlazor's .mud-table-container inside it doing the actual
       scrolling), so a too-wide table scrolls within itself rather than
       pushing .hs-main's box wider — verified live by forcing a 3000px table
       into the /members grid: .mud-table-container scrolled the full 1820px
       while .hs-main's scrollWidth stayed at its client width.
       NOTE this is a guard, not the fix for the closed-side-panel horizontal
       scrollbar (2026-08-16 UAT D-3) — see .mud-drawer.hs-panel-glass near
       .hs-panel below for that one and for why clipping here could not have
       caught it.

       AND NOTE what clipping here costs when something upstream IS too wide:
       this line is what silently ate the audit trail's "· 170.00 $ acros"
       (2026-08-17/20/21). "Grids that genuinely scroll live in .hs-grid-wrap"
       held for MudDataGrid but not for the 19 bare `<table class="hs-table">`
       in plain cards, which the min-width near .hs-table used to blow out to
       720px with no wrap to scroll them. Overflowing INTO this clip produces no
       scrollbar, no wrap and no error — the text is just gone, which is the
       most expensive failure mode a layout can have and the reason
       scripts/ui-check-table-overflow.py measures rendered geometry rather than
       trusting this comment. Keep the clip; keep the guard that proves nothing
       is hitting it. */
    overflow-x: hidden;
    /* "Add a splash of teal around this region of space for all background"
       (product-owner ask, 2026-08-09), a second, larger annotation on the
       same screenshot covering the KPI-tile row and reaching down over the
       content beneath it.
       MainLayout.razor already renders <Aurora /> here — three fixed, blurred
       teal/lime blobs behind the whole shell (hirms.css .hs-aurora) — and
       .hs-main itself was, and stays, transparent so that wash can show
       through its gutters. It is not what this ask is pointing at, though:
       measured in headless Chrome, .hs-aurora's blobs sit correctly and
       .hs-main really is transparent over them, but between .hs-aurora's own
       low alpha (.22–.32) and its 70px blur further softening it, plus the
       KPI tiles' and panels' own opaque/near-opaque card surfaces sitting on
       top, nothing of it reads as a "splash" over precisely the region
       circled — it is closer to imperceptible there than subtle. Rather than
       retune .hs-aurora's shared blob geometry — Kiosk and Member portal
       both key their own washes off the same three blobs, and a change big
       enough to read as a splash on THIS surface would very likely be too
       strong on theirs — this adds a second, purpose-built wash scoped to
       .hs-main alone.
       Two soft pools, not one centred blob: a single wash reading as a
       circle behind a rectangular KPI row looks like a stray spotlight,
       where two loosely bracketing the row's left and right ends reads as
       "the top of this page has some colour" without a shape anyone would
       describe. Positioned in px, not %, and background-attachment:local —
       CSS resolves a percentage `at` position against the FULL scrollable
       content box, not the viewport, and .hs-main routinely holds page-
       length content, so a percentage position would drift toward the
       bottom of a long page; `local` (rather than the default `scroll`,
       which is fixed to the viewport of the element and would instead
       follow the user down the page like a stuck highlight) scrolls the
       wash away with the KPI row once that row scrolls past — it belongs to
       the top of the page, not to whatever happens to be on screen.
       color-mix over --brand, same token the masthead's own corner wash
       uses (hirms.css .hs-mast), at low single-digit alpha — a back-office
       queue screen, not a marketing hero; low enough that it never competes
       with the KPI values or the status colours sitting on top of it, and it
       re-derives correctly in dark mode from this one declaration. */
    background:
        radial-gradient(620px 420px at 60px 0px, color-mix(in srgb, var(--brand) 9%, transparent), transparent 70%),
        radial-gradient(560px 380px at calc(100% - 60px) 40px, color-mix(in srgb, var(--brand) 6%, transparent), transparent 70%),
        transparent;
    background-repeat: no-repeat;
    background-attachment: local;
}
/* Teal, not near-black — the page-head rule reads as an accent line under the
   title now, matching the brand colour every primary action already uses,
   not a plain structural divider. Product-owner ask, 2026-08-06. */
/* The Signal RULE, not a solid blue border.

   The brand book names this at p35 — "4 pt Signal rule bleeding the full top
   edge" — and uses it as its own page furniture: every page footer in the book
   carries one, measured 48x2.5pt with stops sampling #9784F8 -> #288DF6 ->
   #34D7AE. We had the right device in the right place drawn the wrong way.

   border-bottom cannot carry a gradient, so the rule becomes a ::after strip and
   the border is dropped. Same 2px, same position, same padding-bottom — nothing
   about the layout moves.

   Scoped to the page head and nowhere else, deliberately. p21 caps accent plus
   gradient at ~6% of any surface and p23 forbids two gradients on one surface;
   2px across one edge cannot breach either, but sprinkling it would. The book's
   own product mock (p36) carries no gradient rule at all, which is the argument
   against going further than this. */
.hs-page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4); margin-bottom: var(--s5); flex-wrap: wrap; padding-bottom: var(--s4); position: relative; }
.hs-page-head::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: var(--g-signal);
}
/* .hs-page-head-norule kills the border it no longer has; kill the strip too, or
   the modifier silently stops working — the exact half-a-pair this rebrand keeps
   producing. */
.hs-page-head-norule::after { content: none; }
.hs-page-head p { margin: var(--s1) 0 0; color: var(--ink-2); font-size: var(--t-sm); max-width: 68ch; }
.hs-page-head-norule { border-bottom: none; padding-bottom: 0; }
.hs-page-head-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.hs-page-head-crumbs { margin-top: var(--s3); }
/* The V1 eyebrow — mono, tracked, uppercase, brand. */
.hs-eyebrow {
    font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: 2px;
    text-transform: uppercase; color: var(--brand);
    margin-bottom: var(--s1);
}
.hs-page-head h1 { max-width: 22ch; }

/* ── Wave 8 · Domain components ──────────────────────────────────────────── */

/* Timeline — date+status rail on the left, headline+detail on the right, a
   hairline between rows. */
.hs-spine-row {
    display: flex; gap: var(--s4); padding: var(--s3) 0; border-bottom: 1px solid var(--rule);
}
.hs-spine-row:last-child { border-bottom: none; }
.hs-spine-index { min-width: 132px; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.hs-spine-stage { font-size: var(--t-xs); color: var(--ink-2); }
.hs-spine-body { flex: 1; font-size: var(--t-sm); }
.hs-spine-detail { white-space: pre-wrap; margin: 4px 0 0; font-size: var(--t-2xs); }
.hs-spine.dense .hs-spine-row { padding: var(--s2) 0; }

/* HsKv — semantic dl/dt/dd pairs. */
.hs-kv { margin: 0; }
.hs-kv-row { display: flex; justify-content: space-between; gap: var(--s4); padding: var(--s2) 0; border-bottom: 1px solid var(--rule); }
.hs-kv-row:last-child { border-bottom: none; }
.hs-kv dt { color: var(--ink-2); font-size: var(--t-xs); }
.hs-kv dd { margin: 0; font-size: var(--t-sm); text-align: right; }
.hs-kv.right .hs-kv-row { justify-content: flex-start; }
.hs-kv.right dd { margin-left: auto; }

/* HsDisclosure — summary button with caret, aria-expanded. */
.hs-disclosure-summary {
    display: inline-flex; align-items: center; gap: var(--s2);
    background: none; border: none; padding: 0; cursor: pointer;
    font-family: var(--f-ui); font-size: var(--t-sm); font-weight: var(--w-med); color: var(--ink);
}
.hs-disclosure-caret { transition: transform var(--d-fast) var(--ez-out); color: var(--ink-2); }
.hs-disclosure.open .hs-disclosure-caret { transform: rotate(90deg); }
.hs-disclosure-body { margin-top: var(--s2); }

/* ResultList — a real list surface (not .hs-picker-item, which is a floating
   dropdown style). */
.hs-resultlist-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s1); }
.hs-resultlist-item {
    width: 100%; text-align: left; background: none; border: none; padding: 0; cursor: pointer;
    display: block;
}
.hs-resultlist-item:disabled { cursor: not-allowed; }
.hs-resultlist-item:focus-visible { outline: 2px solid var(--brand-line); border-radius: var(--r-sm); }
.hs-resultlist-item-main { display: flex; flex-direction: column; gap: 2px; padding: var(--s2) var(--s3); border: 1px solid var(--rule); border-radius: var(--r-md); }
.hs-resultlist li.disabled { opacity: .55; }

/* LineRepeater — repeating rows with add/remove. */
.hs-linerepeater-row { display: flex; align-items: flex-end; gap: var(--s2); margin-bottom: var(--s2); }
.hs-linerepeater-row > :first-child { flex: 1; }
.hs-linerepeater-remove { flex: none; }
.hs-linerepeater-add { margin-top: var(--s1); }

/* Invoices.razor review — one card per line instead of a 7-column table
   (2026-08-09): a code search whose dropdown is only as wide as a 220px
   table cell showed 4-5 characters of every match, and Description had to
   fit in whatever width five other columns left it. */
.hs-review-lines { display: flex; flex-direction: column; gap: var(--s3); }
.hs-review-line {
    border: 1px solid var(--rule); border-radius: var(--r-md); padding: var(--s1) var(--s3);
    background: var(--surface-2);
}
.hs-review-line.hs-row-held { box-shadow: inset 3px 0 0 var(--st-pend); }
.hs-review-line-top { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s1); }
/* Compact pass (2026-08-23, product owner on the live panel): a ten-line
   invoice was ten postcards. Inputs inside a review line drop to the dense
   scale — the values here are short (a qty, a price, a code) and the height
   was all padding. Scoped to the card so the rest of the product keeps the
   comfortable scale. */
.hs-review-line .hs-input { padding: 4px 8px; font-size: 13px; min-height: 0; }
.hs-review-line .hs-label { font-size: 10px; letter-spacing: .06em; }
.hs-review-line .hs-btn-sm { padding: 2px 10px; }
.hs-review-line-page {
    font-family: var(--f-mono, ui-monospace, monospace); font-size: 11px;
    color: var(--ink-3); border: 1px solid var(--rule);
    border-radius: 999px; padding: 1px 8px; flex: none;
}
.hs-review-line-grid {
    display: grid; align-items: start; gap: var(--s3);
    /* Code and Resolved-to get the most room — a search result and a
       resolved benefit name are both prose, not a number. Qty/Unit/Total
       are fixed enough to not need to flex, and are sized as one coherent
       trio rather than three unrelated widths (76/96/108px, previously —
       Qty read as cramped next to a visibly wider Unit for no reason tied
       to their content: both are the same shape of control, an
       .hs-input.hs-num). Qty and Unit match exactly; Total gets a little
       more room for its second "plan pays" line, the same
       narrow-numeric-beside-wider-field balance .hs-filters strikes with
       its own fixed-width fields. */
    grid-template-columns: minmax(180px, 1.4fr) minmax(180px, 1.4fr) 64px 78px 96px;
}
.hs-review-line-code, .hs-review-line-resolved, .hs-review-line-num { display: flex; flex-direction: column; gap: var(--s1); }
/* A2: the source snippet wraps inside its cell instead of running off the card
   — a source nobody can read is a source nobody can check. Capped so one long
   snippet cannot make the value column unreadable. */
.hs-source-cell { white-space: pre-wrap; word-break: break-word; max-width: 340px; }

.hs-review-line-evidence {
    margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--rule);
}
/* Same breakpoint the panel itself switches to a bottom sheet at (§1281) —
   below it, five columns racing for space is the same crowding this whole
   redesign exists to fix. Two columns: Code+Resolved (the prose fields)
   keep a full row each; Qty/Unit/Total share the second row, which is
   still plenty of room for a 2-3 digit number. */
@media (max-width: 820px) {
    .hs-review-line-grid { grid-template-columns: 1fr 1fr 1fr; }
    .hs-review-line-code, .hs-review-line-resolved { grid-column: 1 / -1; }
}

/* ── Surfaces ────────────────────────────────────────────────────────────── */
/* Cards rest at --e-1 and rise to --e-2 on hover (dc.html:49-50) — the build
   rested them at --e-2, so the hover affordance had nowhere to go. */
.hs-card {
    position: relative;
    /* var(..., fallback), not a --card-bg/--card-blur declared in every theme
       block: the tokens exist only in the light block (tokens.css), so dark
       mode falls straight through to the plain --surface/no-blur it always
       had, guaranteed by the fallback rather than a second override to keep
       in step. */
    background: var(--card-bg, var(--surface)); border: 1px solid var(--rule); border-radius: var(--r-lg);
    box-shadow: var(--e-1); padding: var(--pad-card);
    backdrop-filter: var(--card-blur, none);
    -webkit-backdrop-filter: var(--card-blur, none);
    transition: box-shadow var(--d-fast) var(--ez-out);
}
.hs-card:hover { box-shadow: var(--e-2); }
/* The KPI tile's coloured 3px top-accent border (.hs-kpi-top, TopColor-driven)
   generalized to every card sitewide, not just KpiTile. Plain cards get the
   brand colour; a page can still request a status colour per-instance with
   style="--card-accent:var(--st-rejt)" the same way KpiTile passes TopColor. */
.hs-card::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    background: var(--card-accent, var(--brand));
}
.hs-card-flush::before { left: 1px; right: 1px; }
.hs-card + .hs-card { margin-top: var(--s4); }
/* Undo that stacking margin when the cards are actually side-by-side grid
   cells, not a vertical stack — grid's own `gap` already spaces them, and
   the leftover top margin on every card past the first column pushed that
   card's box (and its coloured top-bar, .hs-card::before) down relative to
   its row-mates. Concretely: Home's Overview "Rule base loaded" /
   "Latest audit trail" pair (align-items:start, meant to read as a matched
   two-column row) had "Latest audit trail" sitting 16px lower than
   "Rule base loaded", because it is the second .hs-card in DOM order and
   .hs-card + .hs-card matched it regardless of grid vs. stacked layout. */
.hs-grid-2 > .hs-card, .hs-grid-3 > .hs-card, .hs-grid-4 > .hs-card { margin-top: 0; }
.hs-card-glass {
    background: color-mix(in srgb, var(--surface) 66%, transparent);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
}
.hs-card-flush { padding: 0; }
.hs-card-aside { display: flex; align-items: center; gap: var(--s2); margin-left: auto; }
.hs-card-foot { padding-top: var(--s3); margin-top: var(--s3); border-top: 1px solid var(--rule); }
/* flex-wrap so a long right-hand status readout (e.g. "N held for missing
   pre-auth") drops to its own line instead of clipping at the card edge —
   it's a number worth reading in full, not an identifier worth truncating. */
.hs-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s2) var(--s3); margin-bottom: var(--s3); flex-wrap: wrap; }
.hs-card-title { font-family: var(--f-ui); font-weight: var(--w-semi); font-size: var(--t-md); margin: 0; }
.hs-muted { color: var(--ink-2); font-size: var(--t-sm); }
.hs-dim { color: var(--ink-3); font-size: var(--t-xs); }

/* Karine's rendered Markdown. Tight margins: a chat turn is not an article, and
   default paragraph spacing inside a 440px panel wastes most of it. */
.hs-karine-turn-text > p { margin: 0; }
.hs-karine-turn-text > p + p,
.hs-karine-turn-text > ul,
.hs-karine-turn-text > .hs-karine-tablewrap { margin-top: var(--s2); }
.hs-karine-turn-text ul { margin: 0; padding-left: var(--s4); }
.hs-karine-turn-text li { margin: 0; }
.hs-karine-turn-text strong { font-weight: var(--w-semi); }
.hs-karine-turn-text code {
    font-size: var(--t-xs); background: var(--surface-2);
    border-radius: var(--r-sm); padding: 0 var(--s1);
}

/* A claims-by-stage table is wider than the panel. It scrolls INSIDE its own
   wrapper so the panel keeps its width and the page body never scrolls
   sideways.

   That comment described an intention the stylesheet did not deliver, and the
   wrapper below is only half of what makes it true. See .hs-karine-turn's
   min-width:0 — without it this overflow-x never engages, because the wrapper
   is never the thing being squeezed. */
.hs-karine-tablewrap { overflow-x: auto; }
.hs-karine-table { border-collapse: collapse; font-size: var(--t-xs); width: 100%; }
.hs-karine-table th,
.hs-karine-table td {
    border-bottom: 1px solid var(--rule-2);
    padding: var(--s1) var(--s2); text-align: left; white-space: nowrap;
}

/* The FIRST column wraps; every other column keeps nowrap.

   nowrap on every cell was the second half of the overflow. Column one is prose
   — a benefit-line name, a provider, a lifecycle stage — and holding it on one
   line is what pushed a 4-column French table past the panel. The figure
   columns genuinely need nowrap: a money amount broken across two lines is
   unreadable, and these are claim values.

   break-word, deliberately NOT anywhere. hirms.css:1109-1118 already records
   what `anywhere` did here once: "a provider name then breaks one word per line
   and the row grows eight lines tall". break-word breaks BETWEEN words and only
   splits a single word when that word alone cannot fit. */
.hs-karine-table td:first-child,
.hs-karine-table th:first-child {
    white-space: normal;
    overflow-wrap: break-word;
    min-width: 8ch;
}
.hs-karine-table th { color: var(--ink-3); font-weight: var(--w-med); }
/* Every column after the first is a figure — amounts and counts line up only
   with tabular figures, and a money column that does not line up is harder to
   read than no table at all. */
.hs-karine-table td:not(:first-child) {
    text-align: right; font-variant-numeric: tabular-nums;
}

/* The reconciliation delta on the invoice-group queue — red when non-zero
   (docs/08 §6: "shown red before anything else"). */
.hs-delta-bad { color: var(--neg); font-weight: var(--w-semi); }

/* ── Buttons ─────────────────────────────────────────────────────────────── */
/* Weight --w-semi per dc.html:1281-1296 (the build used --w-med). Height is
   --ctl (32px, the compact control grid dc.html:1038 builds btn() on) — the
   build hardcoded a 38px comfort height instead. UI-DESIGN-SYSTEM-AUDIT §4.4. */
.hs-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
    min-height: var(--ctl); padding: 0 var(--s4);
    border: 1px solid var(--rule-2); border-radius: var(--r-md);
    background: var(--surface); color: var(--ink); font-family: var(--f-ui); font-size: var(--t-sm); font-weight: var(--w-semi);
    /* nowrap on the CLASS, not per instance: a button's label is a verb, and a
       verb broken across lines reads as two ("Detai / ls" in the 90px queue
       column, owner screenshot 2026-08-23). Any button too narrow for its
       label should grow or its container should — never wrap. */
    white-space: nowrap;
    cursor: pointer; transition: background var(--d-fast) var(--ez-out), border-color var(--d-fast) var(--ez-out);
}
/* Hover is one step along the surface ladder, not a fixed grey. --i-50 was a
   literal step off the light ink ramp, so it stayed near-white in dark and
   hovering a button lit a hole in the page. --surface-2 moves the right way in
   both themes: darker than --surface on a light ground, lighter on a dark one. */
.hs-btn:hover:not(:disabled) { background: var(--surface-2); }
.hs-btn:disabled { opacity: .5; cursor: not-allowed; }
/* Pulse gradient, per the book: ONE primary action per screen, everything else
   outlined or text. That is a design discipline this rule cannot enforce — it
   only guarantees that when a primary IS used it reads as the primary.
   The gradient stops at #1E8FD4 rather than full Cyan so the white label clears
   3:1 across the whole fill; do not "correct" it back to the book's stop. */
.hs-btn-primary { background: var(--g-pulse); border-color: transparent; color: var(--on-pulse); box-shadow: var(--e-1); }
/* The background MUST be re-declared here, not just the filter.
   .hs-btn:hover:not(:disabled) sets background: var(--surface-2) — a near-white
   translucent — and it has EXACTLY the same specificity as this rule (0,3,0
   each: one class, :hover, and :not(:disabled) which takes its argument's
   weight). Same specificity means the later rule wins PER PROPERTY, so
   declaring only `filter` here left the base rule's background standing: the
   Pulse gradient was replaced by near-white on hover and the button visibly
   vanished. Re-declaring background is what beats it. */
.hs-btn-primary:hover:not(:disabled) { background: var(--g-pulse); filter: brightness(1.06); }
/* accent green = financial confirmations only */
.hs-btn-accent { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
/* Same trap as .hs-btn-primary above, and this one predates the rebrand: with
   no hover rule at all, an accent button fell straight through to
   .hs-btn:hover's near-white background and washed out. These are the EFT and
   approve actions, so the button that disappears under the cursor is the one
   carrying money. */
.hs-btn-accent:hover:not(:disabled) { background: var(--accent); filter: brightness(1.06); }
/* Danger is FILLED per dc.html:1285 (btnDanger = --st-rejt bg, #fff text) —
   the build had it as an outline, which read as a secondary action for the
   one button that destroys state. */
.hs-btn-danger { background: var(--st-rejt); border-color: var(--st-rejt); color: var(--on-danger); }
.hs-btn-danger:hover:not(:disabled) { background: var(--st-rejt); filter: brightness(.92); }
/* Soft — the design's row action ("Évaluer"/Assess), dc.html:1283. */
.hs-btn-soft { background: var(--brand-soft); border-color: var(--brand-line); color: var(--brand); }
.hs-btn-soft:hover:not(:disabled) { background: var(--brand-soft); filter: brightness(.97); }
.hs-btn-ghost { background: transparent; border-color: transparent; color: var(--brand); }
.hs-btn-sm { min-height: 30px; padding: 0 var(--s3); font-size: var(--t-xs); }
/* Icon-only control (refresh, theme toggle) — square, not the text button's
   horizontal padding. 2026-08-06: "Refresh" text buttons across the workbench
   became icon+tooltip on product-owner ask; this is the one rule both share. */
/* min-height alone isn't enough: a text .hs-btn's line-height content pushes
   it a few px past --ctl's floor (measured 36px, not 32, in the same button
   row) — an icon-only button has no text content to do that, so it settled
   at exactly the --ctl floor and sat visibly shorter than its siblings
   (product-owner screenshot, 2026-08-06: the refresh icon on /invoices'
   action row). Explicit height matches what its siblings actually render at. */
.hs-btn-icon-only { min-width: var(--ctl); height: 36px; padding: 0; gap: 0; }
.hs-btn-icon-only.hs-btn-sm { min-width: 30px; height: 30px; }
/* Raised, not flat-ghost: a card resting at --e-1, lifting to --e-2 on
   hover — the same elevation language cards already use, so the refresh
   control reads as a distinct pressable surface rather than blending into
   the header text beside it. 2026-08-06, product-owner ask. */
.hs-btn-raised { box-shadow: var(--e-1); }
.hs-btn-raised:hover:not(:disabled) { box-shadow: var(--e-2); }
@keyframes hxSpin { to { transform: rotate(360deg); } }
.hs-btn-icon-spin svg { animation: hxSpin 0.8s linear infinite; }
@media (prefers-reduced-motion: reduce) { .hs-btn-icon-spin svg { animation: none; } }
.hs-btn-row { display: flex; gap: var(--s2); flex-wrap: wrap; }

/* ── Accent colour: teal checkboxes/radios per dc.html:358 (the design's
   non-negotiable). Without this every control shipped browser-default blue. */
.hs-app input[type="checkbox"], .hs-app input[type="radio"],
input[type="checkbox"], input[type="radio"] { accent-color: var(--brand); width: 16px; height: 16px; cursor: pointer; }

/* ── Forms ───────────────────────────────────────────────────────────────── */
/* Field label signature per the design system §4.2: mono + uppercase + tracked,
   not the semibold sans it used to be. gap s2 (6px) per the design's field
   rhythm, not s1. */
.hs-field { display: flex; flex-direction: column; gap: var(--s2); margin-bottom: var(--s3); }
.hs-label {
    font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .8px;
    text-transform: uppercase; color: var(--ink-2);
}
.hs-label-wrap { display: flex; flex-direction: column; gap: var(--s1); }
.hs-input, .hs-select, .hs-textarea {
    /* --ctl (32px), matching dc.html's selectStyle/inputMono/inputError
       (dc.html:1297-1299) — the build hardcoded 38px. */
    min-height: var(--ctl); padding: var(--s2) var(--s3);
    border: 1px solid var(--rule-2); border-radius: var(--r-md);
    background: var(--surface); font-family: var(--f-ui); font-size: var(--t-sm); color: var(--ink); width: 100%;
}
/* A field gets more than the global focus ring, and gets it on :focus rather
   than :focus-visible — a text box you have clicked into should look active to
   a mouse user too, because "where am I typing" is a question everyone asks. */
.hs-input:focus, .hs-select:focus, .hs-textarea:focus { outline: 2px solid var(--brand-line); border-color: var(--brand); }
.hs-textarea { min-height: 84px; resize: vertical; }
/* Select wrapper — the design file draws the chevron as an overlay (dc.html:347),
   so the native arrow is suppressed and replaced with a themed one. The native
   control stays, keeping the listbox keyboard behaviour. */
.hs-select-wrap { position: relative; display: inline-flex; align-items: center; }
.hs-select-wrap .hs-select { appearance: none; -webkit-appearance: none; padding-right: var(--s8); }
.hs-select-block { display: flex; }
.hs-select-block .hs-select { width: 100%; }
.hs-select-chevron { position: absolute; right: 12px; color: var(--ink-2); pointer-events: none; }
.hs-select-wrap:has(.hs-select:disabled) .hs-select-chevron { opacity: .5; }

/* Input (MudTextField-backed since the D2 pass, MUDBLAZOR-MAUI-MIGRATION-
   TRACKER.md M10 Group D) — MudTextField, unlike MudAutocomplete, exposes no
   InputClass to tag the rendered <input> directly, so Input.razor's Mono
   parameter hangs off this wrapper class + a descendant selector instead;
   values match the .mono rule above (`code, .mono`) so identifiers keep the
   same face they had as a plain .hs-input. */
.hs-input-mono input { font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .02em; }

/* ── Wave 4 · Combobox (Select) ─────────────────────────────────────────── */
.hs-combo { position: relative; }
.hs-combo .hs-input { width: 100%; padding-right: var(--s8); }
/* MudAutocomplete's internals default to MudBlazor's own Normal-density
   typography and a 46.5px icon-adornment button regardless of Margin=Dense
   on the component itself (verified live — Dense alone only trims a few
   px off the 63px total), which visibly mismatched the plain .hs-input
   fields beside it in a filter bar (reported 2026-08-11, Claims filter
   row: "large unproportional wrong font"). min-height alone does not fix
   it — MudBlazor never sets an explicit height here, so it's the CONTENT
   (the oversized icon button, subtitle1-scale line-height) driving the
   box, not a competing height declaration to out-specify; an explicit
   height is what actually constrains it. 40px, not the --ctl 32px token
   itself: .hs-input's own min-height IS 32px, but its real rendered
   height (padding + line-height + border, verified live) settles at
   ~40px — pinning this to the literal token first undershot that and
   still read as visibly shorter than its neighbours (reported again,
   2026-08-12). Match the neighbour's actual box, not its nominal one. */
.hs-combo .mud-input { height: 40px; min-height: 40px; font-size: var(--t-sm); }
.hs-combo .mud-input-adornment-icon-button { width: 28px; height: 28px; padding: 6px; }
.hs-combo .mud-input-adornment-icon-button .mud-icon-root { width: 16px; height: 16px; }
/* .hs-combo-list/.hs-combo-item(.active/.selected)/.hs-combo-error — deleted
   2026-08-16. These styled the hand-rolled <ul> Select.razor rendered itself
   before M1 (commit 1252356, "rebuild pickers on MudAutocomplete, delete
   floating.js"): a position:absolute list, positioned first by plain CSS
   (top:calc(100% + 4px)) and later by wwwroot/js/floating.js (position:fixed,
   measured off the anchor's own getBoundingClientRect — deleted in the same
   commit). Select.razor's markup has rendered a <MudAutocomplete> exclusively
   since then; MudBlazor portals its own popover to <MudPopoverProvider/> in
   MainLayout, a sibling of the anchor's whole subtree, not a descendant of
   `.hs-combo` — so none of these selectors have matched anything live for a
   while (verified: zero `hs-combo-list`/`hs-combo-item`/`hs-combo-error`
   occurrences anywhere under src/). Left in place this long, they read as
   the dropdown's positioning rules to anyone investigating a misaligned
   popover — the exact trap a 2026-08-14 alignment report (dropdown opening
   ~40-50px off its input) risked leading a fix into: editing dead CSS that
   was never in the render path, while the actual popover is positioned by
   MudAutocomplete's own AnchorOrigin/TransformOrigin/RelativeWidth (now
   explicit below, not left to MudBlazor's defaults). `.hs-combo-state` stays
   — Select.razor's NoItemsTemplate still renders it. */
.hs-combo-state { padding: var(--s3); font-size: var(--t-sm); color: var(--ink-2); }
.hs-picker-item-main { display: flex; flex-direction: column; gap: 2px; }
.hs-picker-why { font-size: var(--t-2xs); color: var(--ink-2); }

/* ── Wave 4 · Checkbox / Radio / Switch ─────────────────────────────────── */
.hs-check { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; }
.hs-check-input { width: 17px; height: 17px; accent-color: var(--brand); cursor: pointer; }
.hs-check-label { font-size: var(--t-sm); color: var(--ink); }
.hs-switch { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; }
.hs-switch-input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.hs-switch-track {
    position: relative; width: 36px; height: 20px; border-radius: var(--r-full);
    background: var(--rule-2); transition: background var(--d-fast) var(--ez-out); flex: none;
}
.hs-switch-track::after {
    content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
    border-radius: var(--r-full); background: var(--paper);
    box-shadow: var(--e-1); transition: transform var(--d-fast) var(--ez-out);
}
.hs-switch-input:checked + .hs-switch-track { background: var(--brand); }
.hs-switch-input:checked + .hs-switch-track::after { transform: translateX(16px); }
.hs-switch-input:focus-visible + .hs-switch-track { outline: 2px solid var(--brand-line); }
.hs-switch-input:disabled + .hs-switch-track { opacity: .5; }
.hs-switch-label { font-size: var(--t-sm); color: var(--ink); }

/* ── Wave 4 · OTP (single input over segmented cells) ───────────────────── */
.hs-otp-field { display: flex; flex-direction: column; gap: var(--s2); }
.hs-otp { position: relative; }
.hs-otp-input {
    position: relative; z-index: 1;
    width: 100%; min-height: 56px;
    background: transparent; border: 1px solid var(--rule-2); border-radius: var(--r-md);
    font-family: var(--f-mono); font-size: 16px; letter-spacing: .6em; text-align: center;
    /* The typed digits render in the backdrop's cells, one per box, not here —
       centring a whole growing string over fixed-width cells would drift out
       of alignment with every keystroke. This input stays invisible on
       purpose; only its caret (the real typing position) shows. */
    color: transparent; caret-color: var(--brand);
}
.hs-otp-input:focus { outline: 2px solid var(--brand-line); border-color: var(--brand); }
.hs-otp-cells {
    position: absolute; inset: 0; z-index: 0;
    display: grid; gap: 2px;
    pointer-events: none;
}
.hs-otp-cell {
    margin: 2px 0;
    border: 1.5px solid var(--rule-2); border-radius: var(--r-sm);
    background: var(--paper);
    display: grid; place-items: center;
    font-family: var(--f-mono); font-weight: var(--w-semi); font-size: var(--t-xl); color: var(--ink);
}
.hs-otp-cell.filled { border-color: var(--brand-line); background: var(--brand-soft); }
.hs-otp-cell.current {
    border-color: var(--brand); background: var(--paper);
    box-shadow: var(--ring);
}
.hs-otp-timer { font-size: var(--t-xs); color: var(--ink-2); }
.hs-otp-expired { color: var(--st-pend); font-weight: var(--w-semi); }
.hs-otp-attempts { font-size: var(--t-xs); color: var(--neg); }
.hs-otp-locked { display: flex; flex-direction: column; gap: var(--s2); font-size: var(--t-sm); color: var(--neg); }
.hs-otp-field.expired .hs-otp-cell { border-color: var(--st-pend-line); background: var(--st-pend-bg); }
@keyframes hs-shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-6px); }
    75% { transform: translateX(6px); }
}
.hs-otp-field.shake { animation: hs-shake 200ms var(--ez-out); }
.hs-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
.hs-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s3); }
.hs-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s4); }
/* .hs-field's own margin-bottom (below, "Forms") is for a field standing alone
   in normal flow. Inside a grid or a filter bar the row/column gap already
   provides that spacing, so the field's own margin would just add a second,
   redundant gap under the last row/at the field's own edge — harmless on its
   own, but exactly the kind of "field 3 taller than field 4 in the same row"
   drift this file has chased before (see .hs-filters .hs-filter-field's own
   comment). Zeroed here, not by not emitting the margin in the component,
   because a lone <HsField> outside any grid still needs it. */
.hs-grid-2 .hs-field, .hs-grid-3 .hs-field, .hs-grid-4 .hs-field,
.hs-filter-field .hs-field { margin-bottom: 0; }

/* ── StatusPill ──────────────────────────────────────────────────────────── */
.hs-pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 10px; border-radius: var(--r-full);
    font-size: var(--t-xs); font-weight: var(--w-semi); letter-spacing: .02em; white-space: nowrap;
    border: 1px solid transparent;
}
/* "Ready to pay (partial)" cannot wrap under the global nowrap in a narrow
   card cell — this modifier opts a pill out of nowrap for card mode. */
.hs-pill-wrap { white-space: normal; }
/* No leading dot, and that still satisfies the 2026 book's rule 3 ("never
   colour alone for status"), because the rule accepts a dot, an icon OR a word
   — and every pill renders its label. The 6px ::before an earlier build added
   widened every pill ~12px in the status column and was removed per
   UI-DESIGN-SYSTEM-AUDIT §4.5.

   The --st-*-dot tokens are NOT unused: they are the graphic half of the
   pairing and feed the 3px row edge ticks (.hs-tick), where the text-weight
   --st-* colours were nearly invisible at that width. */
.hs-pill-pend { color: var(--st-pend); background: var(--st-pend-bg); border-color: var(--st-pend-line); }
.hs-pill-eval { color: var(--st-eval); background: var(--st-eval-bg); border-color: var(--st-eval-line); }
.hs-pill-redy { color: var(--st-redy); background: var(--st-redy-bg); border-color: var(--st-redy-line); }
.hs-pill-rejt { color: var(--st-rejt); background: var(--st-rejt-bg); border-color: var(--st-rejt-line); }
.hs-pill-paid { color: var(--st-paid); background: var(--st-paid-bg); border-color: var(--st-paid-line); }
/* --surface-3, not the --i-100 literal that was here. Two reasons, and the
   second is the surprise: the ramp step never moved in dark, and --ink-2 on
   --i-100 measured 4.44:1 — under AA in the *light* theme it was written for.
   --surface-3 carries the same weight by eye and lands at 4.54:1 light,
   6.39:1 dark. Same fix, same pair, as .hs-split-member below. */
.hs-pill-neutral { color: var(--ink-2); background: var(--surface-3); border-color: var(--rule); }

/* ── DataGrid ────────────────────────────────────────────────────────────── */
/* overflow-x:auto, not hidden: a fixed-layout table whose columns sum past
   the card's width (a narrow host, a long localized label) now scrolls
   instead of silently clipping trailing columns — including action columns
   like Requisitions' Next step, which is exactly what hidden did (2026-08-16).
   overflow-y stays hidden so the header/footer still take the wrap's
   border-radius corners. */
.hs-grid-wrap { border: 1px solid var(--rule); border-radius: var(--r-lg); background: var(--surface); overflow-x: auto; overflow-y: hidden; }
/* Fixed layout is what makes DataGridColumn's Width authoritative instead of
   a hint content can still widen past — without it a nowrap identifier
   column just grows the table, and the crowded neighbour wraps messily
   instead of the design's single-line cells (WORKBENCH-AUDIT-2026-08-06
   V1-V4). Columns with no Width share the remainder per the browser's own
   fixed-layout default, which is fine for the common case (one flexible
   description column beside a couple of narrow ones). */
.hs-table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); table-layout: fixed; }
/* min-width is what lets .hs-grid-wrap's overflow-x:auto actually engage. With
   width:100% alone the table can never exceed its wrap, so the wrap has nothing
   to scroll and table-layout:fixed squeezes the unsized columns instead —
   combined with the overflow-wrap:anywhere below, a provider name then breaks
   one word per line and the row grows eight lines tall ("Grid is not
   responsive", docs/ui_issues_16_08_26/invoices.png). Below this width the grid
   scrolls sideways inside its own card, which .hs-main's overflow-x:hidden
   comment already accounts for.

   Above the 820px card breakpoint only: narrower than that MudDataGrid switches
   to cards and there is no table left to size.

   SCOPED TO .hs-grid-wrap — 2026-08-21, and this scoping IS the fix for the
   table-text-clipping defect reported three times (2026-08-17, 2026-08-20,
   2026-08-21). min-width and the thing that scrolls it are one pair, and only
   half of it was ever selected for: `.hs-grid-wrap` is rendered in exactly one
   place (the wrap div in DataGrid.razor), while `.hs-table` is also used bare
   inside an .hs-card in 19 places (Home's RuleBase + Audit cards, Capture,
   Claims, ChannelInbox,
   FraudConsole, InvoiceBatch, Invoices, OcrJobs, Reports, TobBrowser,
   UploadInvoices, Kiosk Home). Those 19 have no wrap, therefore nothing that
   can scroll — so the 720px was not a scroll floor for them, it was 720px of
   table inside a card that MEASURES 553px at 1440, 473px at 1280 and 383px at
   1100 (Playwright, /, 2026-08-21). The overflow does not wrap and does not
   scroll: .hs-main carries overflow-x:hidden (hirms.css .hs-main), so it is
   silently CLIPPED, which is why the audit trail read "· 170.00 $ acros" and
   why the rule-base counts vanished out of their own 72px column entirely.

   Note what this was NOT, because three fixes went at the wrong thing: the
   72px/28px cell widths in Home.razor are innocent. Measured on the live page,
   those cells report scrollWidth == clientWidth == 72 and 28 — they contain
   their own padding fine, and --s4 is still 16px, exactly what that arithmetic
   always assumed. The cell was never too small; the TABLE was too big.

   THE ESCAPE HATCH, so the next person does not re-widen this selector to get
   it: a bare table that genuinely needs more room than its card has should be
   wrapped in `<div class="hs-grid-wrap">`, which is what buys the min-width AND
   the overflow-x:auto that makes it reachable. Taking the floor without the
   wrap is the defect; taking both is the supported shape.

   Guarded by scripts/check-table-overflow.py (static, in CI) and measured by
   scripts/ui-check-table-overflow.py (real geometry, needs a live workbench).
   If you widen this selector again, both will tell you. */
@media (min-width: 821px) { .hs-grid-wrap .hs-table { min-width: 720px; } }
/* Opt-in per column (DataGridColumn.Truncate) — never the table default: a
   narrative column (the audit trail's "What happened") is meant to read as
   a sentence, and blanket ellipsis would hide the point of it. */
.hs-table .hs-truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hs-table thead th {
    text-align: left; font-size: 9.5px; text-transform: uppercase; letter-spacing: 1px;
    color: var(--ink-2); font-weight: var(--w-semi); font-family: var(--f-mono);
    padding: var(--s3) var(--s4); background: var(--surface-2); border-bottom: 1px solid var(--rule);
    /* WRAPS, and the reason is translation. nowrap here survived the table
       min-width fix as its last 4px: at 1100px the audit card's narration column
       is 133px, and the French header "Ce qui s'est passé" needs 137. English
       "What happened" fits, so this was invisible in the language the header was
       written in and clipped in the one the workbench actually defaults to.

       A header is two or three words; wrapping it costs one line of height on a
       narrow viewport and nothing at all on a wide one. Losing the last letter of
       a column heading costs the reader the heading.

       The BODY keeps nowrap where it needs it (figures, codes) — this rule is
       thead only. Guarded: scripts/ui-check-table-overflow.py measures header
       cells too, which is how the 4px was found rather than eyeballed. */
    white-space: normal; overflow-wrap: break-word;
}
/* table-layout:fixed sizes columns from the header, not the content — a
   long unbroken token (an invoice number, a mono code) in a cell that never
   opted into .hs-truncate then overflowed the column instead of wrapping.
   overflow-wrap:anywhere breaks it at any point once it has nowhere left to
   wrap at a normal word boundary. */
.hs-table tbody td { padding: var(--cell-y) var(--s4); border-bottom: 1px solid var(--rule); vertical-align: middle; overflow-wrap: anywhere; }
.hs-table tbody tr:last-child td { border-bottom: none; }
.hs-table tbody tr { cursor: pointer; }
/* Colour-role fix (UI-DESIGN-SYSTEM-AUDIT §4.1): the design sets hover =
   --surface-2, selected = --brand-soft. Both were --brand-soft, so hovering a
   row made it look selected. */
.hs-table tbody tr:hover { background: var(--surface-2); }
.hs-table tbody tr.selected { background: var(--brand-soft); box-shadow: inset 3px 0 0 var(--brand); }
.hs-table.compact tbody td { padding: 8px var(--s4); }
/* Owner 2026-09-06 (UI-4): an eleven-column table inside a drawer split its
   fixed layout into slivers and `overflow-wrap: anywhere` then broke words
   mid-letter ("CLINIQU / E NGALIE / MA"). A wide table keeps whole words and
   scrolls sideways inside its wrapper instead. */
.hs-table.wide { table-layout: auto; min-width: 100%; }
.hs-table.wide tbody td, .hs-table.wide thead th { overflow-wrap: normal; white-space: nowrap; }
/* Scoped to the scroll boxes on purpose (check-table-overflow.py): a pinned
   min-width is only safe where the container scrolls. */
.hs-grid-scroll .hs-table.wide thead th, .hs-grid-wrap .hs-table.wide thead th { white-space: normal; min-width: 5.5rem; }
/* Owner 2026-09-06 (UI-7/UI-8): the finance grids scroll INSIDE a bounded
   box so the column headers stay put while the rows go by, and the totals
   row stays in view at the foot. Sticky works against the nearest scrolling
   ancestor, which is exactly this wrapper. Backgrounds are opaque so rows
   never show through the pinned head/foot. */
.hs-grid-scroll { overflow: auto; max-height: calc(100vh - 17rem); border-radius: var(--r-lg); }
.hs-grid-scroll .hs-table thead th { position: sticky; top: 0; z-index: 2; background: var(--surface-2); }
.hs-grid-scroll .hs-table tfoot th { position: sticky; bottom: 0; z-index: 2; background: var(--surface-2); border-top: 1px solid var(--rule); }
/* The name column (company / provider) gets room before the numerics do. */
.hs-grid-scroll .hs-table .hs-col-name, .hs-grid-wrap .hs-table .hs-col-name { min-width: 14rem; }
.hs-table .hs-col-ref { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A filter bar whose fields share the row EVENLY (owner 2026-09-06: "well
   balanced and spaced") — the default gives the first field the slack. */
.hs-filters.balanced .hs-filter-field, .hs-filters.balanced .hs-filter-field:first-child { flex: 1 1 200px; min-width: 200px; }
/* UI-13 (owner 2026-09-06): the DataGrid component (MudDataGrid inside
   .hs-grid-wrap) scrolls its rows in a bounded box with the header pinned
   at the top and the foot at the bottom — the same behaviour the finance
   grids got by hand. max-height, not height: a short list stays short. A
   page with several grids gets several boxes, so a foot releases as soon
   as its own box is scrolled past. */
.hs-grid-wrap .mud-table-container { max-height: calc(100vh - 17rem); overflow: auto; }
.hs-grid-wrap .mud-table-root thead .mud-table-cell,
.hs-grid-wrap .mud-table-root thead th { position: sticky; top: 0; z-index: 2; background: var(--surface-2); }
.hs-grid-wrap .mud-table-root tfoot .mud-table-cell,
.hs-grid-wrap .mud-table-root tfoot th { position: sticky; bottom: 0; z-index: 2; background: var(--surface-2); border-top: 1px solid var(--rule); }
.hs-grid-wrap .mud-table-root tbody td { overflow-wrap: normal; }
/* A plain table that keeps whole words and lets the browser size the columns
   (auto layout) — for lists whose long text should still wrap at spaces. */
.hs-table.roomy { table-layout: auto; min-width: 100%; }
.hs-table.roomy tbody td { overflow-wrap: normal; }
.hs-num { text-align: right; font-variant-numeric: tabular-nums; }
/* th.hs-num must repeat the alignment: `.hs-table thead th` above sets
   text-align:left at higher specificity, so numeric HEADERS silently
   left-aligned over right-aligned values on every table in the app
   (owner, 2026-09-06 — seen on Company lens and invoice details). */
.hs-table thead th.hs-num { text-align: right; }
.hs-empty { padding: var(--s10) var(--s4); text-align: center; color: var(--ink-3); font-size: var(--t-sm); }

/* ── EmptyState (dc.html:484-489) ────────────────────────────────────────── */
.hs-empty-state {
    display: flex; flex-direction: column; align-items: center; gap: var(--s2);
    border: 1px dashed var(--rule-2); border-radius: var(--r-md);
    padding: var(--s5); text-align: center;
}
.hs-empty-badge {
    width: 36px; height: 36px; border-radius: var(--r-full);
    background: var(--brand-soft); color: var(--brand);
    display: grid; place-items: center; margin-bottom: var(--s2);
}
.hs-empty-title { font-size: var(--t-sm); font-weight: var(--w-semi); }
.hs-empty-body { font-size: var(--t-xs); color: var(--ink-2); margin: 4px 0 var(--s3); }

/* ── Skeleton (dc.html:490-493) — shimmer bars matching the real layout ──── */
@keyframes hxShim { 0% { background-position: -360px 0; } 100% { background-position: 360px 0; } }
.hs-skeleton { display: flex; flex-direction: column; gap: var(--s2); padding: var(--s4); }
.hs-skeleton-bar {
    height: 13px; border-radius: var(--r-xs);
    background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-2) 37%, var(--surface-3) 63%);
    background-size: 360px 100%; animation: hxShim 1.3s linear infinite;
}

/* Bulk bar (S3.1) */
.hs-bulkbar {
    display: flex; align-items: center; gap: var(--s3);
    padding: var(--s2) var(--s4); background: var(--brand-soft);
    border-bottom: 1px solid var(--brand-line); font-size: var(--t-sm);
}

/* Filter chips */
/* ── OIDC transition screens ───────────────────────────────────────────────
   Deliberately the same shape as the boot loader in each host's index.html —
   logo, a moving track, one line of text — because these two screens appear
   back to back during a sign-in and a visible change of visual language between
   them reads as something having gone wrong. */
.hs-authwait {
    min-height: 60vh; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: var(--s4);
    padding: var(--s8) var(--s5); text-align: center;
}
/* Square box for a square mark. This was height:40px;width:auto, sized for the
   2.845:1 lockup that used to be here; leaving width:auto with an SVG lets the
   intrinsic 512x512 viewBox decide, and the element jumps once the file decodes.
   Both axes are pinned, matching .hs-loading-mark. */
.hs-authwait-logo { width: 44px; height: 44px; flex: none; }

/* Same three dots, same keyframe, as the loading overlay and the reconnect
   dialog — one definition of "working" across every waiting surface. */
.hs-authwait-dots { display: flex; gap: 6px; }
.hs-authwait-dots span {
    width: 8px; height: 8px; border-radius: var(--r-full); background: var(--brand);
    animation: hs-loading-pulse 1.1s var(--ez-io) infinite;
}
.hs-authwait-dots span:nth-child(2) { animation-delay: .15s; }
.hs-authwait-dots span:nth-child(3) { animation-delay: .3s; }
@media (prefers-reduced-motion: reduce) {
    .hs-authwait-dots span { animation: none; opacity: .7; }
}
.hs-authwait-track {
    width: min(260px, 70vw); height: 3px; border-radius: var(--r-full);
    background: var(--rule); overflow: hidden;
}
/* Indeterminate on purpose. A redirect has no measurable progress, and a bar
   that pretends otherwise is inventing a number. */
.hs-authwait-bar {
    width: 40%; height: 100%; border-radius: var(--r-full); background: var(--brand);
    animation: hs-authwait-slide 1.4s var(--ez-io) infinite;
}
@keyframes hs-authwait-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(250%); }
}
.hs-authwait-msg { margin: 0; color: var(--ink-2); font-size: var(--t-md); }
.hs-authwait-failed { color: var(--ink); font-weight: var(--w-semi); }
.hs-authwait-detail {
    margin: 0; max-width: 46ch; color: var(--ink-3);
    font-size: var(--t-sm); font-family: var(--f-mono); word-break: break-word;
}

.hs-chips { display: flex; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s3); }

/* A hairline between two groups of chips that mean different things. The
   invoice queue's status chips are mutually exclusive (one workflow state at a
   time); the chips after this rule are independent toggles layered on top of
   whichever status is selected. Same row because they are all "narrow the
   queue", but the eye should not read them as one set of radio buttons. */
.hs-chips-sep {
    width: 1px; align-self: stretch; margin: 2px var(--s2);
    background: var(--rule); flex: none;
}

/* The declared member name beside the match count. Deliberately quiet and
   clipped: it is corroboration for a filtered row — "this is why this one came
   back" — not data anyone reads down the column. */
.hs-member-preview {
    display: block; margin-top: 2px;
    font-size: var(--t-xs); color: var(--ink-3);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
/* .hs-chip is not exclusive to PillGroup — LangToggle's/ThemeToggle's plain
   chip-pill mode and at least one page's own hand-rolled status-filter tabs
   use the bare class directly, with no MudChip involved at all. A prior pass
   narrowed the base rule to a .mud-chip.hs-chip compound (matching
   .mud-drawer.hs-panel-glass's reasoning for SidePanel) to make sure it beat
   MudBlazor's own .mud-chip defaults regardless of stylesheet order — but
   that same change silently un-styled every non-MudChip .hs-chip consumer
   app-wide, since the base rule no longer matched them at all (found live,
   2026-08-11: Claims' status tabs rendered as plain unstyled text).
   Plain .hs-chip again — App.razor's load order is now fixed (MudBlazor.min.css
   before hirms.css, see App.razor's own comment), so a same-specificity tie
   against MudBlazor's .mud-chip resolves in hirms.css's favour by source
   order alone; PillGroup's MudChip still carries Class="hs-chip" (see
   PillGroup.razor), so it's still covered by this same rule. margin:0
   cancels MudChip's own 4px chip margin for that case — spacing is
   .hs-chips' flex gap, not per-chip margin, and a plain button never had
   that margin to cancel in the first place. */
.hs-chip {
    margin: 0; min-height: 32px; padding: 0 var(--s3); display: inline-flex; align-items: center; gap: 6px;
    border: 1px solid var(--rule-2); border-radius: var(--r-full); background: var(--surface);
    font-size: var(--t-xs); font-weight: var(--w-med); color: var(--ink-2); cursor: pointer;
    transition: background var(--d-fast) var(--ez-out), border-color var(--d-fast) var(--ez-out);
}
.hs-chip:hover:not(.active) { background: var(--surface-2); border-color: var(--rule); }
.hs-chip.active { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
/* A filter chip with zero results must not look identical to one with hundreds
   — de-emphasis, not removal, so the row layout does not jump. */
.hs-chip[data-count="0"] { opacity: .5; }

/* Masthead presentation — ink/mono/uppercase for the shell's segmented
   controls (FR/EN, CONFORT/COMPACT), per §4.1's two chip styles. */
.hs-chips-masthead .hs-chip {
    min-height: 28px; padding: 0 var(--s2);
    background: transparent; border-color: transparent;
    font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .08em;
    text-transform: uppercase; color: var(--ink-2);
}
.hs-chips-masthead .hs-chip:hover:not(.active) { background: var(--surface-2); border-color: transparent; }
.hs-chips-masthead .hs-chip.active { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }

/* The FR|EN switch in the masthead: the shared .hs-chips brings a bottom margin
   meant for in-page chip groups; the masthead bar is taller than the chips
   (60px vs. 32px — see .hs-mast-logo), so the margin must go and the row must
   centre regardless of the bar's exact height. The design's masthead
   language control (dc.html:563) is the same segmented pair as the theme
   chips, only compacted to two letters. */
.hs-mast .hs-lang-toggle { margin-bottom: 0; align-items: center; }
.hs-mast .hs-lang-toggle .hs-chip { min-height: 28px; padding: 0 var(--s2); }

/* Language dropdown — topbar-cluster recommendation 1b (2026-08-10): one
   button (globe + current code + chevron) instead of a segmented pill, so
   the control doesn't keep growing as CultureSwitcher.Choices does (it
   already anticipates a third, Portuguese, entry). Sized and shaped like
   the theme/bell icon buttons either side of it (36px tall, r-full) rather
   than introducing a new button language; the panel reuses .hs-mast-panel's
   own anchored-dropdown shape for the same reason. */
.hs-lang-dd { position: relative; }
/* .hs-lang-dd-toggle is a <button> — see LangToggle.razor. It was a <div>
   until 2026-08-17, which is why [aria-expanded="true"] below never matched
   anything: nothing set the attribute. */
.hs-lang-dd-toggle {
    display: flex; align-items: center; gap: 6px;
    height: 36px; padding: 0 var(--s3);
    border: 1px solid var(--rule); border-radius: var(--r-full);
    background: var(--paper); color: var(--ink-2);
    font-family: var(--f-ui); font-size: var(--t-xs); font-weight: var(--w-semi);
    cursor: pointer;
}
.hs-lang-dd-toggle:hover, .hs-lang-dd-toggle.active,
.hs-lang-dd-toggle[aria-expanded="true"] { background: var(--surface-2); color: var(--ink); }
/* position/right/top/z-index deleted 2026-08-17 — the third and last instance
   of the dead-CSS collapse bug aa34616 fixed on .hs-bell-panel and
   .hs-mast-panel (see .hs-bell-panel's history note for the full account).
   Since MudMenu took this over (M2 Wave 0, b154cd8) this class lands on the
   <mud-list> INSIDE MudMenu's portalled popover, whose outer wrapper already
   sets position:absolute + measured left/top; a second position:absolute on
   the inner element pulled it out of that wrapper's flow and collapsed the
   popover box to 0×0. Width/background/border/shadow/padding stay — they size
   and paint the panel, they do not place it. */
.hs-lang-dd-panel {
    min-width: 160px;
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-lg); box-shadow: var(--e-3);
    padding: var(--s2); display: flex; flex-direction: column; gap: 2px;
}
.hs-lang-dd-item {
    text-align: left; border: none; background: transparent;
    padding: var(--s2) var(--s3); border-radius: var(--r-sm);
    font-family: var(--f-ui); font-size: var(--t-sm); color: var(--ink);
    cursor: pointer;
}
.hs-lang-dd-item:hover { background: var(--surface-2); }
.hs-lang-dd-item.active { background: var(--brand-soft); color: var(--brand); font-weight: var(--w-semi); }

/* Theme dropdown — same 1b call, same shape as .hs-lang-dd* right above.
   Icon-only toggle (36px circle, not a labelled pill like language) since
   the active theme is a glyph, not a two-letter code — matches the plain
   icon buttons (bell etc.) either side of it on the bar. */
.hs-theme-dd { position: relative; }
.hs-theme-dd-toggle {
    display: flex; align-items: center; gap: 4px;
    width: 36px; height: 36px; padding: 0;
    border: 1px solid var(--rule); border-radius: var(--r-full);
    background: var(--paper); color: var(--ink-2);
    cursor: pointer; justify-content: center;
}
.hs-theme-dd-toggle:hover, .hs-theme-dd-toggle[aria-expanded="true"] { background: var(--surface-2); color: var(--ink); }
.hs-theme-dd-panel {
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
    min-width: 160px;
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-lg); box-shadow: var(--e-3);
    padding: var(--s2); display: flex; flex-direction: column; gap: 2px;
}
.hs-theme-dd-item {
    display: flex; align-items: center; gap: var(--s2);
    text-align: left; border: none; background: transparent;
    padding: var(--s2) var(--s3); border-radius: var(--r-sm);
    font-family: var(--f-ui); font-size: var(--t-sm); color: var(--ink);
    cursor: pointer;
}
.hs-theme-dd-item:hover { background: var(--surface-2); }
.hs-theme-dd-item.active { background: var(--brand-soft); color: var(--brand); font-weight: var(--w-semi); }

/* Theme toggle's compact masthead presentation — icon-only chips, sized like
   the language switch beside it. .hs-chip-icon needs no margin of its own
   here since there is no label sibling to space from. */
.hs-theme-toggle { margin-bottom: 0; align-items: center; }
.hs-mast .hs-theme-toggle .hs-chip { min-height: 28px; padding: 0 var(--s2); }

/* ── DocumentViewer (S8.2) ───────────────────────────────────────────────── */
.hs-docview { margin: 0; }
.hs-docview img { max-width: 100%; border: 1px solid var(--rule); border-radius: var(--r-md); }
.hs-docview figcaption { margin-top: var(--s2); }
.hs-docview-empty, .hs-docview-link { border: 1px dashed var(--rule-2); border-radius: var(--r-md); padding: var(--s4); display: flex; flex-direction: column; gap: var(--s3); align-items: flex-start; }
/* Fills whatever height its container gives it (hs-review-split-doc-body,
   below) rather than the fixed/intrinsic sizing the image and empty-state
   variants use — a PDF viewed in place needs the available height, not a
   caption-sized box. */
.hs-docview-frame { height: 100%; display: flex; }
.hs-docview-frame iframe { flex: 1; width: 100%; height: 100%; border: 0; background: var(--paper); }
.hs-docview-image { height: 100%; overflow: auto; display: flex; flex-direction: column; align-items: center; background: var(--surface-2); padding: var(--s3); }

/* ── Invoice review split (Invoices.razor's Review panel, hs-panel-full) ──
   Equal halves: the form to review on the left, the source document on the
   right, both independently scrollable — the point is comparing one
   against the other without losing either out of view. */
.hs-review-split { display: flex; height: 100%; gap: var(--s5); }
.hs-review-split-form {
    flex: 1 1 0; min-width: 0; overflow-y: auto;
    /* Slightly denser than the panel default — this column now shares the
       screen with the document instead of owning the full width, and the
       smaller size keeps line/field rows from wrapping awkwardly. */
    font-size: var(--t-sm);
}
.hs-review-split-doc {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
    border: 1px solid var(--rule); border-radius: var(--r-md); overflow: hidden;
    background: var(--surface-2);
}
.hs-review-split-doc-head {
    padding: var(--s3) var(--s4); border-bottom: 1px solid var(--rule);
    display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
    flex-wrap: wrap; background: var(--paper);
}
.hs-review-split-doc-body { flex: 1; min-height: 0; }
/* Standalone document pane, for a SidePanel that shows ONLY a document
   (invoice receipts, pre-auth paperwork opened outside the split). The
   split's doc-body sizes by flex:1 inside .hs-review-split's height:100%
   column; dropped bare into a panel it has no flex parent, gets intrinsic
   height, and renders the PDF in a caption-sized strip. This gives the
   pane the viewport's height minus the panel chrome, explicitly. */
.hs-doc-pane { height: calc(100dvh - 150px); min-height: 320px; display: flex; flex-direction: column; }
.hs-doc-pane .hs-review-split-doc-body { flex: 1; min-height: 0; }
@media (max-width: 960px) {
    /* Below the width a genuine side-by-side is legible, stack instead —
       same threshold logic as .hs-panel's own bottom-sheet breakpoint. */
    .hs-review-split { flex-direction: column; overflow-y: auto; }
    .hs-review-split-form { overflow-y: visible; }
    .hs-review-split-doc { min-height: 480px; }
}

/* ── SidePanel → bottom sheet ≤ 820px ────────────────────────────────────── */
/* Black, not rgba(20,23,25,…). The design system specifies no scrim at all —
   it has no modal overlay, so its dark block has nothing to say here and this
   is HIRMS's own value. It could not stay as it was: (20,23,25) is *lighter*
   than dark --paper #0C0E10, so the scrim would have faintly lit the page it
   is there to dim, which is the exact opposite of a scrim's job. Black at the
   same .35 darkens in both themes and is imperceptibly different from the old
   value on a light ground. Alpha deliberately unchanged. */
.hs-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.35); backdrop-filter: blur(2px); z-index: 40; }
.hs-panel {
    position: fixed; top: 0; right: 0; bottom: 0; width: min(760px, 96vw);
    /* Glass per design §5.2 — the panel is chrome, not a solid surface; the
       queue shows through the blur so the user keeps their place in it. */
    background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border-left: 1px solid var(--rule); box-shadow: var(--e-4);
    z-index: 41; display: flex; flex-direction: column;
    animation: hs-slide var(--d-base) var(--ez-out);
}
@keyframes hs-slide { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
.hs-panel-sm { width: min(420px, 96vw); }
.hs-panel-lg { width: min(1040px, 96vw); }
/* SidePanel is a MudDrawer since M1 — position/width/z-index/slide-in are
   MudDrawer's own job now (Anchor/Width/Variant props), so this rule only
   layers the glass visual treatment on top of it, deliberately NOT
   redeclaring position/width/animation the way .hs-panel above does (that
   rule is dead now — kept for history, nothing applies it). Two classes so
   it reliably outranks MudBlazor's own .mud-drawer background/shadow rules
   regardless of their own specificity. */
.mud-drawer.hs-panel-glass {
    /* The one exception to "position is MudDrawer's job", and the fix for the
       horizontal scrollbar every SidePanel-hosting page carried (2026-08-16
       UAT D-3: at an 820px viewport /invoices measured scrollWidth 1607,
       /members and /claims 1580; at 1440 /invoices measured 2200 — it was
       never only a narrow-width problem).

       MudBlazor's own base rules are `position: fixed` for .mud-drawer and
       `position: absolute` for `.mud-drawer:not(.mud-drawer-fixed)`, and it
       only emits .mud-drawer-fixed for a drawer that sits inside a MudLayout /
       MudDrawerContainer. SidePanel deliberately does not — it renders its
       MudDrawer inline in the page (see SidePanel.razor) — so every panel fell
       to position:absolute. A CLOSED temporary drawer is not unmounted, it is
       parked off-canvas at right:-<width>, and an absolutely positioned box
       whose nearest positioned ancestor is nothing at all resolves against the
       INITIAL containing block. That is why no overflow rule inside the shell
       could catch it: .hs-main's overflow-x:hidden (and .hs-shell's, and
       body's) only clips descendants whose containing block is inside them,
       and this one's was the page itself — measured live, .hs-main's own
       scrollWidth was already clamped to its client width while
       <html> still reported 1580.

       position:fixed is both the fix and the correct answer for a Temporary
       (modal) drawer: fixed boxes are laid out against the viewport and
       contribute nothing to the document's scrollable overflow, so the parked
       panel stops widening the page. Geometry is otherwise identical — closed
       and open bounding boxes measured byte-for-byte the same before and
       after at 820px and 1440px — because body does not scroll in this shell
       (.hs-main is the scrolling region), which is the only case where fixed
       and absolute-against-the-ICB would differ. */
    position: fixed;
    background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border-left: 1px solid var(--rule);
    box-shadow: var(--e-4);
    display: flex; flex-direction: column;
}
/* True edge-to-edge — no 96vw cap, no left border (nothing beside it to
   separate from once it's the whole viewport). Used only when the panel
   actually has a document to show side-by-side (Invoices.razor's Review
   panel, Size="full") — everything else keeps its normal drawer width.
   Ported from main's pre-MudDrawer .hs-panel-full onto the MudDrawer
   selector shape above — the old rule targeted a plain .hs-panel div that
   SidePanel stopped rendering in M1, so it was silently unreachable by the
   time this merged; SidePanel.razor's PanelWidth switch grew a "full" case
   (Width="100vw") to match, this rule only needs to add the border-left
   override .hs-panel-glass otherwise carries. */
.mud-drawer.hs-panel-glass.hs-panel-full { border-left: none; }
/* The bottom-sheet grab handle and the ≤820 .hs-panel sheet rules were
   deleted on 2026-09-07 (M-1): SidePanel is a MudDrawer (.hs-panel-glass) and
   its phone width is SidePanel.PanelWidth — nothing rendered .hs-panel. */
.hs-panel-head {
    padding: var(--s5) var(--s6) var(--s4); border-bottom: 1px solid var(--rule);
    display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4);
    background: var(--surface-2);
}
/* A flex item's default min-width is auto (its content's intrinsic width),
   not 0 — a long unbroken title (an IntakeRef, an invoice number) forced the
   whole head to overflow the panel instead of wrapping. min-width:0 lets it
   shrink to the row's actual space; overflow-wrap breaks the token itself
   once it has nowhere left to shrink to. */
.hs-panel-head > div:first-child { min-width: 0; overflow-wrap: break-word; }
.hs-panel-head-aside { display: flex; align-items: center; gap: var(--s2); }
.hs-panel-body { padding: var(--s5) var(--s6); overflow-y: auto; flex: 1; }
.hs-panel-foot {
    padding: var(--s4) var(--s6); border-top: 1px solid var(--rule); background: var(--surface-2);
    display: flex; justify-content: space-between; gap: var(--s3); flex-wrap: wrap;
}

/* ── Modal (general overlay; HsConfirmDialog is its specialisation) ──────── */
/* position/top/left/transform/z-index/animation deleted 2026-08-17 — the
   MudDialog instance of the same bug already fixed on .hs-bell-panel,
   .hs-mast-panel and .hs-lang-dd-panel (see their history notes): HIRMS CSS
   still claiming placement and stacking that a MudBlazor overlay component owns
   now.

   Modal.razor has been a MudDialog since M1 and passes this class straight
   through to MudBlazor's own `.mud-dialog`. That element lives inside
   `.mud-dialog-container`, which is `position: fixed; z-index:
   var(--mud-zindex-dialog)` (1400) and therefore its OWN stacking context; the
   dialog's scrim, `.mud-overlay.mud-overlay-dialog`, sits in that context at
   calc(var(--mud-zindex-dialog) + 1) and MudBlazor puts `.mud-dialog` at +2
   precisely so a dialog paints ABOVE its own scrim. `z-index: 41` here won the
   cascade (hirms.css is linked after MudBlazor.min.css, App.razor:27 vs :35, and
   both selectors are one class) and put the dialog BELOW it — so the 0.4-alpha
   overlay painted over the dialog surface, greying it AND swallowing every click
   on it (with BackdropClick on, a click on Confirm dismissed the dialog instead
   of running it). `position: fixed` did not rescue it: fixed positioning does
   not escape an ancestor stacking context.

   `animation: hs-fade` went for the same reason — it overrode MudBlazor's own
   `mud-open-dialog-center`, using keyframes written for a
   translate(-50%,-50%)-positioned box. The centring itself is
   `.mud-dialog-container.mud-dialog-center`'s flexbox job now (and the translate
   was already dead: mud-open-dialog-center runs with `animation-fill-mode: both`
   and ends on `transform: scale(1)`, which outranks a normal transform
   declaration). @keyframes hs-fade STAYS — .hs-loading-overlay still uses it.

   Width/max-height/background/border/shadow stay: they size and paint the
   dialog, they do not place or stack it. Two classes in the selector for the
   same reason `.mud-drawer.hs-panel-glass` uses two — it must outrank
   MudBlazor's own single-class `.mud-dialog` rules on specificity rather than on
   stylesheet order. */
.mud-dialog.hs-modal {
    width: min(640px, 94vw); max-height: 90vh;
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-lg); box-shadow: var(--e-4);
    display: flex; flex-direction: column;
}
/* Also two classes, and NOT optional: `.mud-dialog.hs-modal` above is (0,2,0)
   and a single-class `.hs-modal-sm` would lose to it, pinning every Modal to the
   md width regardless of Size. */
.mud-dialog.hs-modal-sm { width: min(460px, 94vw); }
.mud-dialog.hs-modal-lg { width: min(880px, 94vw); }
.hs-modal-head {
    padding: var(--s4) var(--s5); border-bottom: 1px solid var(--rule);
    display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
}
.hs-modal-body { padding: var(--s5); overflow-y: auto; }
.hs-modal-foot {
    padding: var(--s4) var(--s5); border-top: 1px solid var(--rule);
    display: flex; justify-content: flex-end; gap: var(--s2);
}
@keyframes hs-fade { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } }

/* ── Loading overlay (LoadingOverlayService / LoadingOverlayHost) ─────────── */
/* Glass in BOTH themes, not light-only like .hs-card (2026-08-08): this is
   chrome sitting over the page, the same category as .hs-panel/.hs-scrim
   above, which already go glass unconditionally — the light-only rule was
   specific to page-content cards, not overlays. */
/* z-index 2000/2001, not the 60/61 every other hs-* overlay uses: SidePanel
   became a MudDrawer in M1 (see its own note above), and MudBlazor's drawer/
   dialog/popover stack (--mud-zindex-*) all sit well above 1000 by default.
   A page can legitimately call Loading.TrackAsync while a MudDrawer-based
   SidePanel is open (e.g. FraudConsole's RebuildGraph from the signal detail
   panel) — this overlay is the single blocking indicator for the whole app,
   so it must always paint above every other layer, MudBlazor's included, not
   just above HIRMS's own hs-* chrome. */
.hs-loading-scrim { backdrop-filter: blur(4px); z-index: 2000; }
.hs-loading-overlay {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    z-index: 2001; width: min(420px, 90vw);
    background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--rule); border-radius: var(--r-lg); box-shadow: var(--e-4);
    padding: var(--s6) var(--s5);
    display: flex; flex-direction: column; align-items: center; gap: var(--s4);
    text-align: center;
    animation: hs-fade var(--d-base) var(--ez-out);
}
/* ── The Blazor reconnect dialog ─────────────────────────────────────────────
   See ReconnectModal.razor for why we can style this at all (short version: by
   supplying the element ourselves we stop the framework building its shadow-DOM
   default, so this is ordinary light DOM and the cascade works normally).

   HIDDEN BY DEFAULT AND THAT IS LOAD-BEARING. The element is always in the page
   — it has to be, or the framework builds its own when the circuit drops — so
   without this it would sit over the workbench permanently. The framework adds
   exactly one state class at a time (it calls removeClasses() first), so these
   four rules are mutually exclusive by contract, not by accident.

   z-index sits ABOVE .hs-loading-scrim (2000/2001). A dropped circuit outranks
   any in-app operation: if a 4s overlay is up when the connection dies, the
   overlay is now describing something that can no longer finish. */
.hs-reconnect { display: none; }
.hs-reconnect.components-reconnect-show,
.hs-reconnect.components-reconnect-failed,
.hs-reconnect.components-reconnect-rejected {
    display: flex; align-items: center; justify-content: center;
    position: fixed; inset: 0; z-index: 2100;
    background: color-mix(in srgb, var(--ink) 38%, transparent);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.hs-reconnect.components-reconnect-hide { display: none; }

/* Same glass plate as .hs-loading-overlay, deliberately: one visual language for
   "something is happening", whether it is our own operation or the framework's
   circuit retry. */
.hs-reconnect-card {
    width: min(420px, 90vw);
    background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--rule); border-radius: var(--r-lg); box-shadow: var(--e-4);
    padding: var(--s6) var(--s5);
    display: flex; flex-direction: column; align-items: center; gap: var(--s4);
    text-align: center;
    animation: hs-fade var(--d-base) var(--ez-out);
}
.hs-reconnect-mark { width: 44px; height: 44px; flex: none; }

/* The three dots reuse the overlay's keyframes rather than declaring their own —
   one definition, so the two surfaces cannot drift out of step. */
.hs-reconnect-dots { display: flex; gap: 6px; }
.hs-reconnect-dots span {
    width: 8px; height: 8px; border-radius: var(--r-full); background: var(--brand);
    animation: hs-loading-pulse 1.1s var(--ez-io) infinite;
}
.hs-reconnect-dots span:nth-child(2) { animation-delay: .15s; }
.hs-reconnect-dots span:nth-child(3) { animation-delay: .3s; }

.hs-reconnect-title {
    font-family: var(--f-ui); font-size: var(--t-sm); font-weight: var(--w-med);
    color: var(--ink); margin: 0;
}
.hs-reconnect-detail {
    font-family: var(--f-ui); font-size: var(--t-xs); color: var(--ink-3);
    margin: 0; font-variant-numeric: tabular-nums;
}

/* Which line shows in which state. All three titles live in the markup and CSS
   picks one, rather than the component branching — there is no circuit when this
   is on screen, so Blazor cannot re-render to swap the text. Everything visible
   here has to be decided by CSS alone. That is also why the reload control is an
   <a href> and not a @@onclick button. */
.hs-reconnect-when-show,
.hs-reconnect-when-failed,
.hs-reconnect-when-rejected,
.hs-reconnect-when-ended { display: none; }
.components-reconnect-show .hs-reconnect-when-show { display: block; }
.components-reconnect-failed .hs-reconnect-when-failed { display: block; }
.components-reconnect-rejected .hs-reconnect-when-rejected { display: block; }
.components-reconnect-failed .hs-reconnect-when-ended,
.components-reconnect-rejected .hs-reconnect-when-ended { display: inline-flex; }

/* Retrying is the only live state, so it is the only one that animates. Dots
   still pulsing after the retries are exhausted would contradict the sentence
   directly above them. */
.components-reconnect-failed .hs-reconnect-dots,
.components-reconnect-rejected .hs-reconnect-dots { display: none; }

@media (prefers-reduced-motion: reduce) {
    .hs-reconnect-dots span { animation: none; opacity: .7; }
    .hs-reconnect-card { animation: none; }
}

/* The mark above the dots. No positioning needed and none wanted: the overlay
   is already a centred flex column with a gap, so being first child is the
   whole layout. width/height are set on the element AND here so the box is
   reserved before the SVG decodes — the overlay is centred with translate(-50%),
   so a late-arriving intrinsic size would shift the whole panel, not just push
   the dots down.

   flex: none because the overlay's align-items:center does not stop a flex item
   shrinking along the main axis; without it a tall enough panel squashes the
   mark rather than the text.

   Deliberately NOT animated. The three dots already carry the motion, and a
   second animation on the same 44px of screen competes with them — it reads as
   busy rather than alive. It also stays still under prefers-reduced-motion for
   free, where the block below has to switch the other two off. */
.hs-loading-mark { width: 44px; height: 44px; flex: none; }
.hs-loading-spinner { display: flex; gap: 6px; }
.hs-loading-spinner span {
    width: 8px; height: 8px; border-radius: var(--r-full); background: var(--brand);
    animation: hs-loading-pulse 1.1s var(--ez-io) infinite;
}
.hs-loading-spinner span:nth-child(2) { animation-delay: .15s; }
.hs-loading-spinner span:nth-child(3) { animation-delay: .3s; }
@keyframes hs-loading-pulse {
    0%, 80%, 100% { opacity: .3; transform: scale(.8); }
    40% { opacity: 1; transform: scale(1); }
}
/* The "streaming" read: each line clips in from zero width as if typed. @key
   on the line in LoadingOverlayHost forces a fresh element per line, which is
   what re-triggers this animation on every advance rather than playing once. */
.hs-loading-line {
    font-family: var(--f-ui); font-size: var(--t-sm); color: var(--ink-2);
    min-height: 1.4em; max-width: 100%;
    overflow: hidden; white-space: nowrap;
    border-right: 2px solid transparent;
    animation: hs-loading-type .6s steps(30, end), hs-loading-caret .8s step-end 4;
}
@keyframes hs-loading-type { from { max-width: 0; } to { max-width: 100%; } }
@keyframes hs-loading-caret { 50% { border-color: var(--brand); } }
@media (prefers-reduced-motion: reduce) {
    .hs-loading-line { animation: none; }
    .hs-loading-spinner span { animation: none; opacity: .7; }
}

/* ── Meter (member balances) ─────────────────────────────────────────────── */
.hs-meter + .hs-meter { margin-top: var(--s4); }
.hs-meter-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s3); margin-bottom: 6px; }
.hs-meter-label { font-size: var(--t-sm); font-weight: var(--w-med); }
/* Mono, per dc.html:385 ({{m.value}} is rendered in --f-mono with tnum) — the
   build left this in --f-ui. UI-DESIGN-SYSTEM-AUDIT §3.4. */
.hs-meter-value { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--ink-2); font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; }
/* Segmented tick-track, per dc.html:385-388 — twelve segments with 2px gaps,
   not a smooth bar. The design file renders the meter as discrete ticks and
   fills them left to right; the last filled tick is the same brand/accent
   colour the smooth bar used. Each segment is a 1px-radius span on the grid. */
.hs-meter-track {
    display: flex; gap: 2px; height: 10px;
}
.hs-meter-track .tick {
    flex: 1; border-radius: 1px; background: var(--surface-3);
    transition: background var(--d-slow) var(--ez-out);
}
.hs-meter-track .tick.on { background: var(--brand); }
.hs-meter-track .tick.warn { background: var(--st-pend); }
.hs-meter-track .tick.over { background: var(--st-rejt); }
.hs-meter-note { font-size: var(--t-2xs); color: var(--st-pend); margin-top: 4px; }

/* A KPI that answers a question should take you to the answer. */
.hs-kpi-link { display: block; text-decoration: none; color: inherit; }
.hs-kpi-link:hover { text-decoration: none; }
.hs-kpi-link:hover .hs-kpi {
    border-color: var(--brand-line);
    box-shadow: var(--e-2);
    transform: translateY(-1px);
}
.hs-kpi-link .hs-kpi { transition: border-color var(--d-fast) var(--ez-out), box-shadow var(--d-fast) var(--ez-out), transform var(--d-fast) var(--ez-out); }

/* ── KpiTile / ChartCard ─────────────────────────────────────────────────── */
/* Label per dc.html:584 — mono 9px uppercase tracked --ink-2, not --f-ui.
   The notch (--notch, 18px) belongs on KpiTile + MemberCard only (audit §1). */
.hs-kpi {
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-lg);
    padding: var(--s4); box-shadow: var(--e-1);
    clip-path: polygon(0 0, calc(100% - var(--notch)) 0, 100% var(--notch), 100% 100%, 0 100%);
}
.hs-kpi-label { font-family: var(--f-mono); font-size: 9px; letter-spacing: .8px; text-transform: uppercase; color: var(--ink-2); }
.hs-kpi-value { font-family: var(--f-disp); font-size: var(--t-2xl); font-weight: var(--w-bold); line-height: var(--lh-1); margin: var(--s2) 0 4px; display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s2); font-feature-settings: 'tnum'; }
/* --g-700, not --accent. --accent is #12B27E, a FILL colour: as 28px type on
   white it measures 2.73:1, under even the 3:1 large-text floor. --g-700
   (#0A7A56) is the ramp's text-safe green at 5.34:1. The same distinction
   the book draws for Cyan and Mint applies one step further up this ramp. */
.hs-kpi-value.accent { color: var(--g-700); }
.hs-kpi-sub { font-size: var(--t-xs); color: var(--ink-2); }
/* The design file's finance KPI — a 3px coloured top border (dc.html:773). */
.hs-kpi--top { position: relative; padding-top: var(--s4); }
.hs-kpi-top {
    position: absolute; top: 0; left: 0; right: 0; height: 3px;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.hs-bars { display: flex; align-items: flex-end; gap: var(--s3); height: 180px; padding-top: var(--s4); }
.hs-bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; }
/* Gradient on data is approved (component-sweep.md): Pulse for neutral series,
   Vital for positive outcomes, solid --warning/--error for risk. Pulse is the
   default because most series here are neither good nor bad — claims by stage,
   codes by frequency — and colouring a neutral count as an outcome is the
   reading error the book's rule 3 exists to prevent.

   The modifiers are on the BAR, not the chart, so a single series can carry a
   risk bar among neutral ones without a second component. */
.hs-bar { width: 100%; max-width: 56px; border-radius: var(--r-sm) var(--r-sm) 0 0; background: var(--g-pulse); min-height: 2px; }
.hs-bar.is-positive { background: var(--g-vital); }
.hs-bar.is-warn { background: var(--warning); }
.hs-bar.is-risk { background: var(--error); }
.hs-bar-label { font-size: var(--t-2xs); color: var(--ink-3); text-align: center; }
.hs-bar-value { font-size: var(--t-2xs); font-weight: var(--w-semi); color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* ── Stepper (kiosk flow) ────────────────────────────────────────────────── */
/* Per dc.html:509-516,1227-1232: done = green (--accent) filled + white ✓,
   current = teal filled + mono numeral, future = --surface-3 outline; the
   label sits UNDER the dot (a centred column, not a row beside it), and the
   connecting bar runs dot-centre to dot-centre via an absolutely positioned
   span (top:13px = half the 26px dot), not a fixed-width line beside the
   label. UI-DESIGN-SYSTEM-AUDIT §3.2/§3.12/§4.7. */
.hs-stepper { display: flex; align-items: flex-start; margin-bottom: var(--s5); }
.hs-step { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; }
.hs-step-dot {
    width: 26px; height: 26px; border-radius: var(--r-full); border: 1px solid var(--rule-2);
    display: grid; place-items: center; font-family: var(--f-mono); font-size: 10.5px; font-weight: var(--w-semi);
    background: var(--surface-3); color: var(--ink-3); z-index: 1;
}
.hs-step.done .hs-step-dot { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.hs-step.current .hs-step-dot { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.hs-step-label { font-size: var(--t-2xs); color: var(--ink-2); text-align: center; line-height: var(--lh-snug); }
.hs-step-bar {
    position: absolute; top: 13px; left: 50%; right: -50%; height: 2px;
    background: var(--rule); z-index: 0;
}
.hs-step-bar.done { background: var(--accent); }

/* ── Rule citation & alerts ──────────────────────────────────────────────── */
.hs-rule {
    border-left: 3px solid var(--brand-line); background: var(--brand-soft);
    padding: var(--s2) var(--s3); border-radius: 0 var(--r-md) var(--r-md) 0;
    font-size: var(--t-xs); color: var(--brand-lo);
}
.hs-rule .quote { font-style: italic; }
/* Alert — the §4.13 left-edge geometry: 3px border-left, squared-off on the
   accent side (border-radius only on the right). The old rule was a uniform
   1px box wrapping a single child with no icon slot. */
.hs-alert {
    padding: var(--s3) var(--s4);
    border: 1px solid; border-left: 3px solid; border-radius: 0 var(--r-md) var(--r-md) 0;
    font-size: var(--t-sm); display: flex; gap: var(--s3); align-items: flex-start;
}
.hs-alert-icon { flex: none; line-height: 1.4; }
/* min-width:0, not just flex:1 — a flex item's default min-width is auto,
   which means "never shrink narrower than my content wants to be". A short
   filename never exposed this; a genuinely long one (UploadInvoices.razor's
   job rows: raw upload filenames, sometimes 60+ characters) grew the whole
   body past the row instead of the inner span's own overflow:hidden ever
   getting a narrower box to truncate against — text-overflow:ellipsis on a
   descendant does nothing if nothing upstream is actually constrained.
   Harmless for short content: this only lowers how far the item CAN shrink,
   it does not force it to. */
.hs-alert-body { flex: 1; min-width: 0; }
.hs-alert-actions { margin-top: var(--s2); display: flex; gap: var(--s2); flex-wrap: wrap; }
.hs-alert-dismissible { padding-right: var(--s2); }
.hs-alert-block { color: var(--st-rejt); background: var(--st-rejt-bg); border-color: var(--st-rejt-line); }
.hs-alert-warn { color: var(--st-pend); background: var(--st-pend-bg); border-color: var(--st-pend-line); }
.hs-alert-info { color: var(--st-eval); background: var(--st-eval-bg); border-color: var(--st-eval-line); }
.hs-alert-ok { color: var(--st-redy); background: var(--st-redy-bg); border-color: var(--st-redy-line); }
/* The permission/restricted-access shell §4.13 specifies and no tone covered:
   neutral ink on --surface-3 with a --rule-2 edge. */
.hs-alert-restricted { color: var(--ink-2); background: var(--surface-3); border-color: var(--rule-2); }
/* A clickable hs-alert acting as a list row (UploadInvoices.razor's job
   list) needs its own selected state on top of whichever tone is active —
   a ring rather than a colour swap, so "selected" and "ready/failed/etc."
   stay two independent signals instead of fighting over the same border. */
.hs-alert.selected { box-shadow: 0 0 0 2px var(--brand); }

/* ── Cover split bar ─────────────────────────────────────────────────────── */
.hs-split { display: flex; height: 26px; border-radius: var(--r-full); overflow: hidden; border: 1px solid var(--rule); }
/* The one #fff in this file that is deliberately NOT a token, and --on-brand is
   the specific wrong answer. The two segments below are painted with literal
   ramp steps — var(--b-600), var(--b-400) — not var(--brand), because a cover
   split is categorical data: insurer and employer must stay distinguishable
   from each other, so they are pinned to the scale rather than following the
   theme. Those two backgrounds are therefore identical in light and dark.
   --on-brand is not: it flips to #04222B when --brand becomes a pale teal. Pair
   the two and dark mode gets near-black text on #00718F at 2.96:1 — a fail
   introduced by tokenising, on the widget that tells a member what they owe.
   White on those two fixed steps measures 5.59:1 and 3.08:1 in both themes.

   The 3.08:1 is the EMPLOYER segment, and it failed AA at 11px — worse than the
   --i-400 case the member fix below caught, and missed by it.

   It does NOT need the ramp re-picked. Walking the scale shows white passes AA
   at no step that is still visibly distinct from --b-600 (b-500 is 4.00:1,
   b-700 is 7.72:1 but reads as the same colour as its neighbour). Turning the
   TEXT over instead costs nothing and measures 5.85:1: --ink on --b-400. The
   backgrounds stay exactly where the design put them, so the three segments
   remain as separable as they ever were.

   The bar therefore reads light-on-dark, dark-on-mid, dark-on-pale — which is
   not an inconsistency but what any value scale does, flipping its ink at the
   luminance crossover. The member segment below already does it. */
.hs-split-seg { display: grid; place-items: center; font-size: var(--t-2xs); font-weight: var(--w-semi); color: #fff; min-width: 2px; overflow: hidden; }
.hs-split-insurer { background: var(--b-600); }
/* 5.85:1. Must stay below .hs-split-seg to beat its #fff on source order. */
.hs-split-employer { background: var(--b-400); color: var(--ink); }
/* White on --i-400 measured 3.3:1 — below the 4.5:1 that 11px text needs, on a
   widget whose whole job is telling a member what they owe. The design does not
   fight the mid-grey with white text; it steps the segment back to a pale
   surface and puts dark ink on it (dc.html:656), which reads at 4.5:1 and has
   the side effect of saying the right thing: the member's share is the quiet
   one, not a third brand colour. Overrides the #fff from .hs-split-seg on
   source order, so this rule must stay below it.

   That 3.3:1 is HISTORY, not a live measurement, and was already stale before
   this note was added: it was taken against the book's retired #8A93AB
   (3.07:1 by calculation). --i-400 has been darkened twice since — it is
   #606A86 today (tokens.css) — and white on it now measures 5.38:1, so the
   premise the number states no longer holds. Kept because it is the reason
   the design chose a pale segment, and harmless because this rule never uses
   white; recorded so the next reader does not re-derive a decision from a
   figure that has not described the token since the first repaint. */
.hs-split-member { background: var(--surface-3); color: var(--ink-2); }
.hs-split-legend { display: flex; gap: var(--s4); margin-top: var(--s2); font-size: var(--t-xs); color: var(--ink-2); flex-wrap: wrap; }
.hs-split-legend span::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; }
.hs-split-legend .i::before { background: var(--b-600); }
.hs-split-legend .e::before { background: var(--b-400); }
/* Follows the segment above. A key that names a colour the bar no longer uses
   is worse than no key at all — and the pale swatch needs an edge, or an 8px
   square of --surface-3 on a card of --surface is just nothing. */
.hs-split-legend .m::before { background: var(--surface-3); border: 1px solid var(--rule-2); }

/* ── Member card (kiosk/portal) ──────────────────────────────────────────── */
.hs-membercard {
    /* --g-card, not --brand-lo/--brand: those flip in dark, and this is the
       one surface the book says must NOT darken. The token is defined once
       and never overridden, so the plate is identical in both themes. 115deg
       is the book's Signal angle. */
    background: var(--g-card);
    /* --r-lg, not --r-xl — dc.html:848 draws the member card at --r-lg.
       UI-DESIGN-SYSTEM-AUDIT §3.2/§3.13. */
    color: var(--on-card); border-radius: var(--r-lg); padding: var(--s5) var(--s6); box-shadow: var(--e-3);
    clip-path: polygon(0 0, calc(100% - var(--notch)) 0, 100% var(--notch), 100% 100%, 0 100%);
}
.hs-membercard .name { font-family: var(--f-disp); font-size: var(--t-xl); font-weight: var(--w-bold); overflow-wrap: anywhere; }
.hs-membercard .mso { font-family: var(--f-mono); font-size: var(--t-sm); opacity: .85; letter-spacing: .04em; }
.hs-membercard .row { display: flex; justify-content: space-between; gap: var(--s4); margin-top: var(--s4); font-size: var(--t-xs); opacity: .92; flex-wrap: wrap; }
/* Photo frame + name/MSO + QR, in one row — photo and QR are both optional
   (QR only when the host passes the Qr slot; the photo frame always renders,
   silhouette or real portrait, so the layout doesn't jump the day a photo
   pipeline lands). */
.hs-membercard-head { display: flex; align-items: center; gap: var(--s3); }
.hs-membercard-id { min-width: 0; flex: 1; }
.hs-membercard-photo {
    width: 44px; height: 44px; border-radius: var(--r-full); flex: none;
    background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.35);
    display: grid; place-items: center; overflow: hidden; color: var(--on-card);
}
/* cover + a top-biased crop: card photos are portraits, and a centre crop of
   a head-and-shoulders shot puts the chin at the middle and the eyes out of
   frame. Top bias keeps the face (owner report 2026-08-30). */
.hs-membercard-photo img { width: 100%; height: 100%; object-fit: cover; object-position: center 20%; }
.hs-membercard.compact .hs-membercard-photo { width: 36px; height: 36px; }
/* The phone member card gets a properly visible portrait — 44px reads as an
   icon on a handset held at arm's length. */
.hs-membercard--phone .hs-membercard-photo { width: 64px; height: 64px; }
/* M-1 (2026-09-07): the previous ≤480 rule lost to the two-class
   .hs-membercard--phone selector above (0,2,0 beats 0,1,0) and never applied.
   At 360px the plate's inner width is 312px: photo 52 + QR 64 leaves the name
   column room for NOM POSTNOM Prénom at --t-lg. */
@media (max-width: 480px) {
    .hs-membercard { padding: var(--s4) var(--s4); }
    .hs-membercard-photo, .hs-membercard--phone .hs-membercard-photo { width: 52px; height: 52px; }
    .hs-membercard.compact .hs-membercard-photo { width: 44px; height: 44px; }
    .hs-membercard--phone .hs-membercard-qr { width: 64px; height: 64px; }
    .hs-membercard .name { font-size: var(--t-lg); }
}

/* The design file's phone member card (dc.html:848-864): the notched gradient
   plate with the QR in a white box on the right. */
.hs-membercard--phone { position: relative; overflow: hidden; }
.hs-membercard-glow {
    position: absolute; right: -36px; bottom: -40px; width: 130px; height: 130px;
    border-radius: var(--r-full); background: rgba(255,255,255,.07);
}
.hs-membercard-main { display: flex; align-items: flex-start; gap: var(--s3); position: relative; }
.hs-membercard-kicker {
    font-family: var(--f-mono); font-size: 9px; letter-spacing: 1.4px; text-transform: uppercase; opacity: .78;
}
.hs-membercard-qr { width: 72px; height: 72px; background: #fff; border-radius: var(--r-sm); padding: 5px; flex: none; }
.hs-membercard-qr .hs-qr { gap: 0; }

/* ── Phone-style layout (member portal) ──────────────────────────────────── */
.hs-phone-head { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s5); }
.hs-avatar {
    width: 38px; height: 38px; border-radius: var(--r-full);
    background: var(--brand-soft); color: var(--brand);
    display: grid; place-items: center; font-weight: var(--w-bold); font-size: var(--t-sm); flex: none;
}
.hs-phone-icon {
    display: grid; place-items: center; width: 44px; height: 44px;
    border: 1px solid var(--rule); background: var(--surface); color: var(--ink-2);
    border-radius: var(--r-md); cursor: pointer; flex: none;
}
.hs-phone-section { margin-bottom: var(--s5); }
.hs-phone-section-title {
    font-family: var(--f-mono); font-size: 9.5px; font-weight: var(--w-semi);
    text-transform: uppercase; letter-spacing: 1px; color: var(--ink-2); margin-bottom: var(--s3);
}
.hs-phone-actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); margin-bottom: var(--s5); }
.hs-phone-action {
    display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
    min-height: 76px; padding: var(--s3);
    border: 1px solid var(--rule); background: var(--surface-2); color: var(--ink);
    border-radius: var(--r-md); font: inherit; font-size: var(--t-sm); font-weight: var(--w-semi);
    cursor: pointer; text-align: left;
}
.hs-phone-action-icon {
    width: 26px; height: 26px; border-radius: var(--r-sm);
    display: grid; place-items: center; font-size: var(--t-md); flex: none;
}
.hs-phone-action--brand { background: var(--brand-soft); color: var(--brand); }
.hs-phone-action--pend { background: var(--st-pend-bg); color: var(--st-pend); }
/* Not --accent: green is reserved for financial confirmations (CLAUDE.md
   conventions) and "Find a provider" is a directory lookup, not one. */
.hs-phone-action--neutral { background: var(--surface-3); color: var(--ink-2); }
.hs-phone-action--eval { background: var(--st-eval-bg); color: var(--st-eval); }
.hs-phone-list { border: 1px solid var(--rule); border-radius: var(--r-md); overflow: hidden; }

/* A modifier, NOT a change to the base rule above — .hs-phone-list is also used
   by the claims and dependants lists, which are short and must keep growing to
   their content.

   The provider directory is 689 rows. Rendered unbounded on a phone it is a
   wall the member scrolls past forever to reach anything below it. Capping it
   at roughly ten rows and scrolling inside is the fix; TAKING ten and
   discarding the rest is not, because unlike claims there is no "see all
   providers" page to reach the other 679 from.

   overflow-x: hidden is load-bearing, not defensive: the base rule's
   overflow:hidden is what clips the rounded corners, and setting overflow-y
   alone computes overflow-x to auto, which would put a horizontal scrollbar
   under a list of long clinic names. */
.hs-phone-list--capped { max-height: 615px; overflow-y: auto; overflow-x: hidden; }
.hs-phone-list-item {
    display: flex; align-items: center; gap: var(--s3);
    padding: var(--s3); border-bottom: 1px solid var(--rule); min-height: 56px;
    background: var(--surface);
}
.hs-phone-list-item:last-child { border-bottom: none; }
.hs-phone-list-item .hs-tick { align-self: stretch; }
/* A row that opens a detail view (Claims) is a <button>, not a <div>, for
   keyboard and screen-reader operability — this resets the browser's own
   button chrome back to plain list-item styling. */
.hs-phone-list-item--btn {
    width: 100%; border: none; text-align: left; font: inherit; color: inherit;
    cursor: pointer;
}
.hs-phone-list-item--btn:hover { background: var(--surface-2); }
.hs-phone-tabbar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
    display: grid; grid-template-columns: repeat(4, 1fr);
    border-top: 1px solid var(--rule); background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
}
.hs-phone-tab {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    min-height: 58px; padding: var(--s2) 0;
    border: none; background: transparent; font: inherit; font-size: 10px; font-weight: var(--w-semi);
    color: var(--ink-3); cursor: pointer;
}
.hs-phone-tab--active { color: var(--brand); }
/* Room for the fixed tabbar so the last list item is not hidden behind it. */
.hs-main-narrow { padding-bottom: 76px; }

/* The design-file kiosk hero (.hs-kiosk-hero/-scan/-actions) was deleted on
   2026-09-07 (M-1): Home.razor renders .hs-stage / .hs-scan-target /
   .hs-stage-actions instead, and no page ever used these. */

/* ── Network graph (S6.3) ────────────────────────────────────────────────── */
.hs-graph { width: 100%; height: 460px; border: 1px solid var(--rule); border-radius: var(--r-lg); background: var(--surface-2); }
.hs-graph-legend { display: flex; gap: var(--s4); margin-top: var(--s2); font-size: var(--t-xs); color: var(--ink-2); flex-wrap: wrap; }

/* ── Kiosk / portal density: touch targets ≥ 44px ────────────────────────── */

/* ── Responsive: grid → cards, panel → bottom sheet ──────────────────────── */
@media (max-width: 820px) {
    /* Below 820px the nav is a horizontal strip under the mast, not a sidebar
       beside it — a fixed-height, single-scroll-region shell doesn't fit that
       shape, so this reverts to one normal, whole-page scroll (.hs-mast's
       own position:sticky still keeps it pinned as that page scrolls). */
    .hs-shell { grid-template-columns: 1fr; height: auto; overflow: visible; }
    /* padding-top back to 0: below 820px the nav is a horizontal strip
       sitting ABOVE main, not a column running beside it, so the desktop
       offset that aligns the sidebar's top with main's page title (added
       above) has nothing to align against here and would just be dead space
       over the row of links. The transparent-gap/glass-panel split above is
       a desktop-only concern for the same reason — .hs-nav-inner just fills
       .hs-nav edge to edge at this width. */
    .hs-nav { padding-top: 0; }
    .hs-nav-inner { flex-direction: row; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--rule); padding: var(--s3) var(--s2); }
    .hs-nav a { min-height: 40px; }
    /* No grouping in the strip. The headers go (they always did, back when
       they were labels), and the row containers become display:contents so
       their children flow straight into the horizontal flex line — collapsed
       or not, because a toggle nobody can reach must not be able to hide a
       destination. display:contents also drops the group rail and indent,
       which have no meaning in a row. */
    .hs-nav-group, .hs-brand small, .hs-brand { display: none; }
    .hs-nav-rows, .hs-nav-rows.is-collapsed { display: contents; }
    .hs-mast { padding: var(--s2) var(--s3); gap: var(--s2); }
    .hs-mast-toggle { display: grid; }
    /* The brand wordmark steps out first — the logo mark is the identity that
       survives at this width. */
    .hs-mast-word { display: none; }
    .hs-main { padding: var(--s4); overflow-y: visible; }
    /* REMOVED 2026-08-19 — this block hid every grid in the workbench on every
       phone.

       `.hs-table { display: none }` plus a `.hs-cards` replacement was the
       pre-MudBlazor swap: a hand-rolled table and a parallel card list, one
       hidden at each width. M1 deleted the `.hs-cards` markup "wholesale"
       (DataGrid.razor's own comment says exactly that) because MudDataGrid does
       card-mode natively through Breakpoint="Breakpoint.Sm".

       The CSS never followed. `.hs-table` is the class on the MudDataGrid
       itself, so below 820px the grid was display:none with nothing left to
       take its place, and `.hs-cards`/`.hs-rowcard` styled markup that no
       longer exists anywhere in src/. Reported as "grids and tables not showing
       on mobile" — and that is literally what it was: not squeezed, not
       clipped, absent. MudDataGrid's own breakpoint now does the job the
       deleted div used to. */
    .hs-grid-2, .hs-grid-3, .hs-grid-4 { grid-template-columns: 1fr; }
}

/* Below 481px the masthead's language + theme controls (five small chip
   targets) crowd the refresh/bell/avatar cluster in the same strip — reported
   2026-08-08 against a 400px viewport. Collapsed behind one gear icon; the
   dropdown itself reuses the account panel's exact anchored-overlay shape. */
@media (max-width: 480px) {
    .hs-mast-settings-toggle { display: grid; }
    .hs-mast-inline-settings {
        display: none;
        position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
        flex-direction: column; align-items: stretch; gap: var(--s3);
        width: min(220px, calc(100vw - var(--s6)));
        background: var(--paper); border: 1px solid var(--rule);
        border-radius: var(--r-lg); box-shadow: var(--e-3);
        padding: var(--s4);
    }
    .hs-mast-inline-settings.open { display: flex; }
}

/* ── Block B components ──────────────────────────────────────────────────── */

/* Pager — sits in the grid footer so it scrolls with the table it belongs to. */
.hs-pager {
    display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap;
    padding: var(--s3) var(--s4); border-top: 1px solid var(--rule); background: var(--surface-2);
    font-size: var(--t-xs); color: var(--ink-2);
}
.hs-pager-range { font-variant-numeric: tabular-nums; }
.hs-pager-controls { display: flex; align-items: center; gap: var(--s2); margin-left: auto; }
.hs-pager-page { font-variant-numeric: tabular-nums; padding: 0 var(--s2); white-space: nowrap; }
.hs-pager-size { max-width: 150px; min-height: 30px; font-size: var(--t-xs); }

/* Sortable headers. The arrow is part of the button so the hit target includes it. */
.hs-th-sort {
    background: none; border: none; padding: 0; cursor: pointer; font: inherit;
    color: inherit; text-transform: inherit; letter-spacing: inherit;
    display: inline-flex; align-items: center; gap: 4px;
}
.hs-th-sort:hover { color: var(--brand); }
.hs-th-arrow { color: var(--brand); font-size: var(--t-xs); }

/* Column-header filters (owner 2026-08-31): a funnel beside the title, a
   popover under it. The wrap anchors the popover; the popover is paper on
   rule with the panel shadow — the disclosure family's look, in a header. */
.hs-th-wrap { position: relative; display: inline-flex; align-items: center; gap: 4px; }
.hs-th-filter {
    background: none; border: none; padding: 2px; cursor: pointer;
    color: var(--ink-2); display: inline-flex; align-items: center;
    border-radius: var(--r-sm);
}
.hs-th-filter:hover { color: var(--brand); }
.hs-th-filter.is-active { color: var(--brand); }
.hs-th-filter:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.hs-th-filter-pop {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
    min-width: 230px; padding: var(--s3);
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-md); box-shadow: var(--e-2);
    text-transform: none; letter-spacing: normal; font-weight: var(--w-reg);
    white-space: normal;
}
.hs-th-filter-range { display: flex; align-items: center; gap: var(--s2); }
.hs-th-filter-range .hs-input { min-width: 0; }
@media (max-width: 820px) {
    /* Card mode has no header row; the popover would anchor to nothing.
       MudDataGrid hides headers at this width, so nothing renders — the
       page-level filter forms remain the phone path, deliberately. */
    .hs-th-filter-pop { left: auto; right: 0; }
}

/* Grid states. A list has four, not one. */
.hs-grid-state {
    display: flex; align-items: center; gap: var(--s3);
    padding: var(--s8) var(--s4); color: var(--ink-2); font-size: var(--t-sm);
    justify-content: center;
}
/* Left accent + squared-off corner on the accent side, per dc.html:493
   ("Service indisponible" state) — the build had a flat tinted block. */
.hs-grid-state-error {
    color: var(--st-rejt); background: var(--st-rejt-bg);
    border-left: 3px solid var(--st-rejt); justify-content: flex-start; text-align: left;
}
.hs-grid-foot { border-top: 1px solid var(--rule); }
/* The open-in-progress overlay (Invoices, 2026-08-28): a slow open with nothing
   on screen reads as a missed click, and the second click was a context race.
   Fixed and centred so it is unmissable, translucent so the queue stays legible
   behind it. */
/* A1: a batch's children, rendered in the grid's detail row. One compact grid
   line per invoice, clickable whole — the same affordance as a grid row, minus
   a second table's chrome. */
.hs-batch-children { display: flex; flex-direction: column; gap: 2px; padding: var(--s1) var(--s4); }
.hs-batch-child {
  /* minmax(0, …) everywhere a text cell lives: OCR intake refs are 40+
     characters, and a fixed track without a min of 0 lets the content set the
     floor, so the ref painted straight across the member name (owner report
     2026-08-30 — "sub items are overlapping so unreadable"). Each span also
     clips with an ellipsis; the full value stays in the title attribute. */
  display: grid;
  grid-template-columns: minmax(60px, auto) minmax(90px, 200px) minmax(80px, 1fr) 110px 110px 90px auto auto;
  gap: var(--s2); align-items: center; text-align: left;
  padding: var(--s1) var(--s2); border: none; border-radius: var(--r-sm);
  background: var(--surface-2); cursor: pointer;
}
.hs-batch-child > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hs-batch-child:hover { background: var(--surface-3); }

.hs-opening-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--surface-2) 55%, transparent);
}

.hs-spinner {
    width: 16px; height: 16px; border-radius: var(--r-full);
    border: 2px solid var(--rule-2); border-top-color: var(--brand);
    /* The one duration that stays hardcoded, and linear with it: a spinner
       turns at a constant rate forever, so it has no arrival to ease into and
       nothing to do with the three-step motion scale. The design leaves its
       own spin unscaled for the same reason. */
    animation: hs-spin .7s linear infinite; display: inline-block;
}
@keyframes hs-spin { to { transform: rotate(360deg); } }

/* Row status modifiers — a held claim should be visible before it is read.
   The five canonical tones (tokens.css --st-*): eval/pend/redy/rejt/paid.
   held and flagged are semantic aliases for pend/rejt kept for the callers
   already using them; eval/pend/redy/rejt are also exposed by their own tone
   names so a page can pass RowStatus straight from a status enum's tone
   (StatusPill.razor's own Claim/Requisition/PreAuth switch) without
   re-deriving a second vocabulary. */
.hs-table tbody tr.hs-row-eval { box-shadow: inset 3px 0 0 var(--st-eval); }
.hs-table tbody tr.hs-row-pend, .hs-table tbody tr.hs-row-held { box-shadow: inset 3px 0 0 var(--st-pend); }
.hs-table tbody tr.hs-row-redy { box-shadow: inset 3px 0 0 var(--st-redy); }
.hs-table tbody tr.hs-row-rejt, .hs-table tbody tr.hs-row-flagged { box-shadow: inset 3px 0 0 var(--st-rejt); }
.hs-table tbody tr.hs-row-paid { opacity: .72; }
/* The status edge tick — a 3px coloured column at the row's left edge, so the
   state is readable before the pill is (dc.html:459). Sits in its own narrow
   cell so it does not compress the first data column. */
.hs-row-tick { width: 6px; padding: 0 !important; }
.hs-row-tick .hs-tick { display: block; width: 3px; height: 100%; min-height: var(--row-h); }
/* The card-mode tick went with the rest of the hand-rolled card markup in M1 —
   see the removal note in the 820px block below. Nothing in src/ has carried
   .hs-rowcard-tick since. */
.hs-hide-narrow { }

/* Tabs */
.hs-tabs { display: flex; gap: var(--s1); border-bottom: 1px solid var(--rule); margin-bottom: var(--s4); overflow-x: auto; }
.hs-tab {
    background: none; border: none; border-bottom: 2px solid transparent;
    padding: var(--s3) var(--s4); cursor: pointer;
    font-family: var(--f-ui); font-size: var(--t-sm); font-weight: var(--w-med); color: var(--ink-2);
    display: inline-flex; align-items: center; gap: var(--s2); min-height: 42px;
    white-space: nowrap;
}
.hs-tab:hover { color: var(--brand); }
.hs-tab.active { color: var(--brand-lo); border-bottom-color: var(--brand); font-weight: var(--w-semi); }
.hs-tab-badge {
    background: var(--surface-3); border-radius: var(--r-full); padding: 1px 7px;
    font-size: var(--t-2xs); font-variant-numeric: tabular-nums;
}
.hs-tab.active .hs-tab-badge { background: var(--brand-soft); color: var(--brand-lo); }
/* Badge tones — "3 errors" (rejt) must not look like "3 lines" (neutral). */
.hs-tab-badge-rejt { background: var(--st-rejt-bg); color: var(--st-rejt); }
.hs-tab-badge-pend { background: var(--st-pend-bg); color: var(--st-pend); }
.hs-tab-badge-redy { background: var(--st-redy-bg); color: var(--st-redy); }

/* Toasts */
.hs-toast-host {
    position: fixed; right: var(--s5); bottom: var(--s5); z-index: 60;
    display: flex; flex-direction: column; gap: var(--s2); max-width: min(420px, 92vw);
}
.hs-toast {
    display: flex; gap: var(--s3); align-items: flex-start;
    /* --paper, not --surface: a toast floats over arbitrary page content, so it
       has to be opaque. --surface is translucent in both themes and would let
       the table underneath show through the text. Matches .hs-confirm,
       .hs-bell-panel and .hs-picker-list, the other three overlays. */
    background: var(--paper); border: 1px solid var(--rule); border-left-width: 4px;
    border-radius: var(--r-md); box-shadow: var(--e-3);
    padding: var(--s3) var(--s4); font-size: var(--t-sm);
    animation: hs-toast-in var(--d-base) var(--ez-out);
}
.hs-toast > div:first-child { flex: 1; }
.hs-toast-redy { border-left-color: var(--st-redy); }
.hs-toast-eval { border-left-color: var(--st-eval); }
.hs-toast-pend { border-left-color: var(--st-pend); }
.hs-toast-rejt { border-left-color: var(--st-rejt); }
@keyframes hs-toast-in { from { opacity: 0; transform: translateY(6px); } }

/* Confirm dialog */
/* position/top/left/transform/z-index deleted 2026-08-17 — identical to
   `.mud-dialog.hs-modal` above; see that rule's note for the full account.
   `z-index: 61` is what made the /claims "Set this line's amount?" confirm
   render grey (product owner, screenshot 2026-08-17): it dropped `.mud-dialog`
   below its own `.mud-overlay.mud-overlay-dialog` scrim inside
   `.mud-dialog-container`'s stacking context. 61 and 41 are the same stranded
   pre-M1 numbers bf57c1a raised on `.hs-loading-scrim`/`.hs-loading-overlay`;
   these two rules were missed by that pass. */
.mud-dialog.hs-confirm {
    width: min(460px, 92vw); background: var(--paper);
    border: 1px solid var(--rule); border-radius: var(--r-lg); box-shadow: var(--e-4);
    padding: var(--s5) var(--s6);
}
.hs-confirm h3 { margin-bottom: var(--s2); }
.hs-confirm-body { margin: var(--s3) 0; }
.hs-confirm-actions { display: flex; justify-content: flex-end; gap: var(--s2); margin-top: var(--s5); }

/* Field validation — the label goes negative with the control, and the border
   is 1.5px (not 1px) so an invalid field reads before the message does. */
.hs-field-invalid .hs-label { color: var(--neg); }
.hs-field-invalid .hs-input,
.hs-field-invalid .hs-select,
.hs-field-invalid .hs-textarea,
.hs-field-invalid .hs-select-wrap .hs-select { border-color: var(--neg); border-width: 1.5px; }
/* The error line carries the design's 14px round badge before the sentence —
   a marker a colour-blind clerk can still find, because the colour alone is
   not the signal. */
.hs-field-error {
    display: flex; align-items: center; gap: var(--s2);
    font-size: var(--t-xs); color: var(--neg);
}
.hs-field-error::before {
    content: "!"; flex: none;
    width: 14px; height: 14px; border-radius: var(--r-full);
    background: var(--neg); color: var(--paper);
    display: grid; place-items: center;
    font-size: 10px; font-weight: var(--w-bold); line-height: 1;
}
.hs-required { color: var(--neg); margin-left: 3px; }

/* Chips with counts */
.hs-chip-count {
    background: var(--surface-3); border-radius: var(--r-full); padding: 0 6px;
    font-size: var(--t-2xs); font-variant-numeric: tabular-nums;
}
/* On an active chip the count sits on --brand, so it is tinted with the ink
   that chip already uses rather than a fixed white wash. rgba(255,255,255,.25)
   was right only while --brand stayed dark; in dark --brand is a pale teal and
   a white wash on it disappears. Mixing --on-brand keeps the wash on the
   opposite side of the chip from its background in both themes — white at 22%
   in light, near-black at 22% in dark — which is the same effect, not a new
   colour. color-mix is the design system's own idiom (dc.html:96). */
.hs-chip.active .hs-chip-count { background: color-mix(in srgb, var(--on-brand) 22%, transparent); color: inherit; }

/* Breadcrumb */
.hs-breadcrumb {
    display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
    font-size: var(--t-xs); color: var(--ink-3); margin-bottom: var(--s3);
}
.hs-breadcrumb-current { color: var(--ink-2); font-weight: var(--w-med); }
.hs-breadcrumb-sep { color: var(--rule-2); }

/* Filter bar — the row of controls above a grid. */
.hs-filters {
    /* row-gap carries the same one-hint-line clearance the padding-bottom
       note below describes, but BETWEEN wrapped rows: the hints are
       position:absolute (see .hs-filter-field .hs-dim), so when the bar
       wraps - six fields on Claims in English do - row 1's hint painted
       straight over row 2's label ("Claim #, MSO number..." over
       "CLAIMED AT LEAST", reported 2026-09-03). */
    display: flex; column-gap: var(--s3); row-gap: calc(var(--s3) + var(--s5));
    flex-wrap: wrap; align-items: flex-end;
    padding: var(--s4); background: var(--surface-2);
    border: 1px solid var(--rule); border-radius: var(--r-lg); margin-bottom: var(--s4);
    /* A field's hint (.hs-dim below) is position:absolute so it stops
       skewing the row's flex-end alignment (see that rule's own comment)
       — but that also takes it out of THIS container's own height
       calculation, so the standard padding-bottom left it sitting flush
       against the card's rounded border with no breathing room
       ("touching the card margin", reported 2026-08-11/12). The extra
       var(--s5) is exactly one hint line's worth of clearance. */
    padding-bottom: calc(var(--s4) + var(--s5));
}
/* Targets .hs-filter-field, the wrapper div each page adds around its own
   HsField, NOT .hs-field. The two are different layers and both are real:
   .hs-field is HsField's own wrapper (HsField.razor:14) around label/control/
   hint; .hs-filter-field is the filter bar's per-column flex item around that.
   The distinction matters because .hs-filters is the flex container — the row
   must wrap per logical filter column, so the min-width/flex sizing belongs on
   the outer .hs-filter-field. (History: until commit 3efa497 HsField emitted no
   wrapper at all and label/control/hint were flat siblings, so a bare
   ".hs-field" selector matched nothing and .hs-filters wrapped mid-field —
   confirmed live on Claims.razor/FraudConsole.razor, 2026-08-08. That is fixed;
   .hs-field now matches, and the zero-margin override up at
   ".hs-grid-2 .hs-field, … .hs-filter-field .hs-field" relies on it. The
   selector below still deliberately targets the outer wrapper.) */
.hs-filters .hs-filter-field { margin-bottom: 0; min-width: 170px; position: relative; }
/* The search field is the one worth reading, not just picking from a short
   list (same reasoning TobBrowser's .hs-filters-compact:first-child already
   documents) — flex:1 lets it claim the row's leftover space instead of
   sizing to bare content and leaving a gap the wrap algorithm then fills
   unpredictably, which is what produced the lopsided "search alone on its
   own row" layout reported 2026-08-11. */
.hs-filters .hs-filter-field:first-child { min-width: 220px; flex: 1 1 220px; }
/* .hs-filters aligns the row on align-items:flex-end (bottom), so every
   field's INPUT lines up regardless of label length — but HsField renders
   an optional hint (.hs-dim) as a normal sibling below the input, and only
   Search passes one ("Claim #, MSO number..."). A field with a hint is
   taller than one without, so flex-end's shared baseline became the
   TALLEST field's bottom (Search's hint text), pushing every hint-less
   field's input down to match it — while Search's own input, sitting above
   its own hint, ended up floating noticeably higher than the rest of the
   row (reported 2026-08-11). Taking the hint out of normal flow removes it
   from the flex item's height entirely, so every field's real (label+input)
   height is equal again and flex-end aligns the inputs, not whichever
   field happens to carry the tallest hint/error text. right:0 keeps it the
   field's own width so it still wraps in place rather than overrunning the
   next field. */
.hs-filters .hs-filter-field .hs-dim { position: absolute; top: 100%; left: 0; right: 0; margin-top: 2px; }
/* .hs-filters-actions carries no label above it, unlike every .hs-filter-field
   beside it — so .hs-filters' own align-items:flex-end (needed so labelled
   fields' INPUTS line up, see above) pins this one-line cluster to the
   bottom of a row whose other members are label+input tall. The result
   reads as the count text hanging low and disconnected from the row
   (reported 2026-08-11/12), even though its bottom genuinely does match
   the inputs' bottom. align-self:center instead centres it against the
   row's full cross-size, landing it roughly level with the inputs rather
   than pinned under an invisible label-sized gap only it doesn't have. */
.hs-filters-actions { margin-left: auto; display: flex; gap: var(--s2); align-items: center; align-self: center; }

/* TOB's filter bar has five fields (search + insurer/plan/category/coverage)
   where every other page's has at most two or three — at the default
   170px-per-field minimum that wraps to two rows on anything narrower than a
   very wide monitor. Narrower fields here keep it on one row at normal
   widths; the search box (the one field worth reading, not just picking from
   a short list) keeps the most room. 2026-08-06, product-owner ask.
   .hs-filter-field, not .hs-field, for the same reason as the plain .hs-filters
   rules above: .hs-field is HsField's own inner wrapper (it does emit one —
   HsField.razor:14, since commit 3efa497), while .hs-filter-field is the div
   TobBrowser.razor wraps around each HsField to be the flex item this bar
   wraps and sizes per logical field. */
.hs-filters-compact { gap: var(--s2); }
.hs-filters-compact .hs-filter-field { display: flex; flex-direction: column; gap: var(--s2); min-width: 128px; }
.hs-filters-compact .hs-filter-field:first-child { min-width: 220px; flex: 1 1 220px; }

/* Member portal sections (B4) */
.hs-section { border: 1px solid var(--rule); border-radius: var(--r-lg); background: var(--surface); margin-bottom: var(--s3); overflow: hidden; }
.hs-section-head {
    display: flex; align-items: center; gap: var(--s4); width: 100%;
    padding: var(--s4) var(--pad-card); background: none; border: none; cursor: pointer;
    font-family: var(--f-ui); text-align: left; min-height: 56px;
}
.hs-section-head:hover { background: var(--brand-soft); }
.hs-section-title { font-weight: var(--w-semi); font-size: var(--t-md); }
.hs-section-sub { font-size: var(--t-xs); color: var(--ink-2); }
.hs-section-meter { flex: 1; max-width: 240px; margin-left: auto; }
.hs-section-caret { color: var(--ink-3); transition: transform var(--d-fast) var(--ez-out); }
.hs-section-caret.open { transform: rotate(90deg); }
.hs-section-body { padding: 0 var(--pad-card) var(--s4); }

@media (max-width: 820px) {
    .hs-hide-narrow { display: none; }
    .hs-pager { flex-direction: column; align-items: stretch; gap: var(--s2); }
    .hs-pager-controls { margin-left: 0; justify-content: space-between; }
    .hs-toast-host { right: var(--s3); left: var(--s3); bottom: var(--s3); max-width: none; }
    .hs-section-meter { max-width: none; margin-left: 0; }
    /* On the member portal the fixed tabbar hugs the bottom of the screen, so
       the toast rack must clear it or a success lands underneath a thumb. */
    .hs-main-narrow ~ .hs-toast-host,
    .hs-app .hs-toast-host { bottom: calc(76px + var(--s3)); }
    /* A filter bar is a toolbar, not a column: on a narrow workbench the
       actions still want to sit beside the last control rather than dropping a
       row below it. */
    .hs-filters-actions { margin-left: 0; flex-wrap: wrap; }
    /* M-2 (live mobile sweep 2026-09-06): the hint under a filter field is
       taken out of flow above (position:absolute) so a desktop row's inputs
       line up. Stacked one per row on a phone there is no row to align, and
       a hint that wraps to two lines sat on top of the next field's label
       (Claims: "Tous les sinistres issus d'une même facture prestataire"
       over "Déposé à partir du"). Back in flow here, with one unit of air. */
    .hs-filters .hs-filter-field .hs-dim { position: static; margin-top: var(--s1); }
}

/* Funnel */
.hs-funnel { display: flex; flex-direction: column; gap: var(--s4); }
.hs-funnel-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.hs-funnel-label { font-size: var(--t-sm); font-weight: var(--w-med); }
.hs-funnel-value { font-variant-numeric: tabular-nums; font-weight: var(--w-semi); }
.hs-funnel-track { height: 10px; background: var(--surface-3); border-radius: var(--r-full); overflow: hidden; }
/* Matches .hs-meter-fill deliberately. Both are a bar travelling to a new
   width, and two widgets doing the same thing at different speeds is a thing
   people notice without being able to say why. */
.hs-funnel-fill { height: 100%; background: var(--g-pulse); border-radius: var(--r-full); transition: width var(--d-slow) var(--ez-out); }
.hs-funnel-stage.clickable { cursor: pointer; }
.hs-funnel-stage.clickable:hover .hs-funnel-fill { filter: brightness(1.08); }

/* Code picker */
/* .hs-picker/.hs-picker-list/.hs-picker-item(:last-child/:hover)/.hs-picker-state
   — deleted 2026-08-16, same reason as .hs-combo-list above: the generation
   before that, CptPicker/Icd10Picker/MemberPicker rendered their own
   position:absolute dropdown directly. All three are thin wrappers around
   Select.razor (→ MudAutocomplete) now — grep confirms zero `hs-picker`/
   `hs-picker-list`/`hs-picker-item`/`hs-picker-state` occurrences anywhere
   under src/. (`.hs-picker-item-main`/`.hs-picker-why` below are a different,
   still-live pair — the option-row content inside a MudAutocomplete
   ItemTemplate, not the dropdown surface itself.) */

/* File upload — the dashed drop zone (dc.html §6.7, HSL.Platform HxFileUpload).
   The native input stretches invisibly across the whole zone, so click,
   keyboard and OS drag+drop all hit the same control. */
.hs-upload {
    border: 1.5px dashed var(--brand-line);
    border-radius: var(--r-lg);
    background: var(--brand-soft);
    padding: var(--s5);
    position: relative;
    transition: border-color var(--d-fast) var(--ez-out), background var(--d-fast) var(--ez-out);
}
.hs-upload--drag { border-color: var(--brand); background: var(--brand-line); }
.hs-upload--busy { opacity: .8; cursor: progress; }
.hs-upload__zone {
    display: flex; flex-direction: column; align-items: center; gap: var(--s2);
    text-align: center; color: var(--ink-2);
}
.hs-app .hs-upload__zone { min-height: 96px; justify-content: center; }
.hs-upload__input {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer;
}
.hs-upload:focus-within { outline: 2px solid var(--brand-line); outline-offset: 2px; border-radius: var(--r-lg); }
.hs-upload__badge {
    width: 36px; height: 36px; border-radius: var(--r-full);
    background: var(--brand); color: var(--on-brand);
    display: grid; place-items: center; flex: none;
}
.hs-upload__label { font-weight: var(--w-semi); color: var(--ink); }
.hs-upload__hint { font-size: var(--t-xs); color: var(--ink-3); }
.hs-upload__browse {
    display: inline-flex; align-items: center;
    height: var(--ctl-sm); padding: 0 var(--s3);
    border: 1px solid var(--brand-line); background: var(--surface);
    color: var(--brand); border-radius: var(--r-sm);
    font-size: var(--t-xs); font-weight: var(--w-semi);
}
.hs-upload__errors {
    display: flex; flex-direction: column; gap: var(--s1);
    margin-top: var(--s2); font-size: var(--t-xs); color: var(--st-rejt);
}
.hs-upload__error { display: inline-flex; align-items: center; gap: 6px; }
.hs-upload__error-badge {
    width: 14px; height: 14px; border-radius: var(--r-full);
    background: var(--st-rejt-bg); color: var(--st-rejt);
    display: inline-grid; place-items: center; font-weight: var(--w-bold); font-size: 9px;
    flex: none;
}

/* QR */
.hs-qr { display: inline-flex; flex-direction: column; align-items: center; gap: var(--s2); }
/* Stays #fff in both themes, and this is not an oversight. A QR code's quiet
   zone and light modules are a scanner requirement, not a surface: the symbol
   is read by a camera thresholding dark modules against a light field, and
   inverting it on a dark page makes the code unreadable on a large share of
   handsets. The white plate is the widget, so it is drawn, not themed. */
.hs-qr svg { border-radius: var(--r-sm); background: #fff; }
.hs-qr-value { font-size: var(--t-xs); color: var(--ink-2); }
.hs-qr-fallback { padding: var(--s4); border: 1px dashed var(--rule-2); border-radius: var(--r-md); }

/* Member card variant */
.hs-membercard.compact { padding: var(--s4); }
.hs-membercard.compact .name { font-size: var(--t-lg); }
.hs-membercard-inactive { color: var(--st-rejt); font-weight: var(--w-semi); }

/* Meter compact — bar only, for section headers */
.hs-meter.compact { margin: 0; }
.hs-meter.compact .hs-meter-track { height: 6px; }
/* Blazor's crash bar. Hidden until the runtime reveals it; when it does show,
   it should look like part of the product, not unstyled text. The PWA loads no
   host-specific css, so this lives in the RCL where both apps get it. */
#blazor-error-ui {
    display: none;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 100;
    background: var(--st-pend-bg, #fff3cd); color: var(--ink, #1f2321);
    border-top: 3px solid var(--st-pend, #b98a00);
    padding: var(--s3) var(--s5); font-size: var(--t-sm);
    box-shadow: var(--e-3);
}
#blazor-error-ui .reload { font-weight: var(--w-semi); margin-left: var(--s2); }
#blazor-error-ui .dismiss { cursor: pointer; float: right; }

/* Sign-in — the first screen either installable app shows. Centred, generous,
   and the only place a large button is right: it is the single thing to do. */
.hs-signin {
    max-width: 460px; margin: 8vh auto; padding: var(--s8) var(--s6);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-lg);
    box-shadow: var(--e-2); text-align: center;
}
/* hsl-brand.css makes .hsl-logo `display:block; margin:2px 0 …` — a block box
   ignores the parent's text-align, so on the member sign-in card (unlike the
   landing page, which already carries this same fix at .hs-landing-head
   .hsl-logo) the mark sat hard left under a centred heading. margin:0 auto
   centres it; height bumped 40px → 56px per product ask. */
.hs-signin .hsl-logo { height: 56px; margin: 0 auto var(--s5); }
.hs-signin h1 { font-size: var(--t-xl); margin-bottom: var(--s2); }
.hs-signin p { margin: 0 0 var(--s5); }
.hs-signin .hs-alert { margin-bottom: var(--s5); }
.hs-signin-foot { margin-top: var(--s6); padding-top: var(--s4); border-top: 1px solid var(--rule); }
.hs-btn-lg { min-height: 48px; padding: 0 var(--s6); font-size: var(--t-md); width: 100%; }

/* ── Notification bell + inbox ───────────────────────────────────────────── */
.hs-bell { position: relative; }
/* The masthead's right cluster (bell → avatar) is pushed to the edge by the
   spacer, but belt-and-braces: margin-left:auto on the first cluster element
   does the same push on its own, so a build where the spacer is missing or
   its flex-grow is defeated cannot bunch the cluster against the brand. */
.hs-mast .hs-bell { margin-left: auto; }
.hs-bell-btn {
    /* A circular outlined chip (product direction 2026-08-22, from the
       reference structure): the bell is the one masthead control whose state
       matters at a glance, and the visible ring is what makes the count badge
       read as attached to something rather than floating in the bar. The
       border is --rule at rest — visible, quiet — where its siblings stay
       transparent until hover. r-full, not r-md: a circle, per the reference,
       and the count badge overlaps its edge the way the reference's dot does. */
    position: relative; background: none; border: 1px solid var(--rule);
    border-radius: var(--r-full); min-height: var(--ctl); min-width: var(--ctl);
    cursor: pointer; line-height: 1; color: inherit;
    display: inline-flex; align-items: center; justify-content: center;
}
.hs-bell-btn:hover, .hs-bell-btn.active { background: var(--brand-soft); border-color: var(--brand-line); }
.hs-bell-count {
    position: absolute; top: 2px; right: 2px;
    min-width: 17px; height: 17px; padding: 0 4px;
    /* The status pair, used the other way up. Everywhere else --st-rejt is ink
       on --st-rejt-bg; a filled count badge inverts that, so the two tokens
       swap roles and the badge follows the theme for free — off-white on deep
       red in light (5.36:1), near-black on salmon in dark (8.21:1). Plain #fff
       was right only while --st-rejt stayed dark, and in dark it is #F2A99C. */
    background: var(--st-rejt); color: var(--st-rejt-bg);
    border-radius: var(--r-full); font-size: var(--t-2xs); font-weight: var(--w-semi);
    display: inline-flex; align-items: center; justify-content: center;
    font-variant-numeric: tabular-nums;
}
.hs-scrim-quiet { background: transparent; }   /* click-away without dimming the page */

/* DropdownMenu.razor's two keydown wrappers — one around MudMenu (the whole
   activator subtree, including the <div tabindex="0"> MudMenu inserts of its
   own accord) and one inside the portalled panel. Both exist only to carry an
   @onkeydown for Escape; neither may exist as far as layout is concerned.

   display:contents is the whole point, not a shortcut. Without it the outer
   wrapper would become the masthead's flex item instead of `.mud-menu`, and
   `.hs-bell`'s margin-left:auto would stop pushing the cluster right; the
   inner one would become `.hs-bell-panel`'s only flex child, and the panel's
   flex-direction:column + the list's overflow-y:auto would stop applying to
   .hs-bell-head/-list/-foot. Boxes are suppressed, the DOM tree is not, so
   keydown still bubbles through both. */
.hs-menu-keys { display: contents; }

/* position/right/top/z-index deleted 2026-08-16 — dead since MudMenu took over
   (M2 Wave 0, commit b154cd8): this class now lands on the list *inside*
   MudMenu's own popover (<div class="mud-popover">...<div class="... hs-bell-
   panel">), not a sibling of `.hs-bell` positioned by it. MudPopover already
   sets position:absolute + explicit left/top (measured off the button's own
   anchor, portalled to <MudPopoverProvider/> in MainLayout) on the OUTER div;
   this inner div also claiming position:absolute pulled it out of flow,
   collapsing that outer popover box to 0×0 — the actual "the bell doesn't
   work" bug (verified live: aria-expanded flipped true, the popover node
   existed with opacity:1, and its own getBoundingClientRect was 0×0/nowhere
   on screen). Same trap as .hs-combo-list before it (see that class's history
   note below) — CSS that reads as the dropdown's positioning rule while a
   MudBlazor popover already owns positioning. Width/background/border/shadow/
   max-height stay; those size and paint the panel, they do not place it.

   (This block's terminator was a Razor-style `*@` typo from 2026-08-16 until
   2026-08-17. CSS does not end a comment there, so it ran on to the next real
   terminator 20 lines down and silently ate the whole of .hs-bell-panel and
   .hs-bell-head — the notification panel opened, but rendered unsized,
   unbordered, transparent and with no scroll cap.) */
.hs-bell-panel {
    width: min(400px, calc(100vw - var(--s6)));
    background: var(--paper); border: 1px solid var(--rule);
    border-radius: var(--r-lg); box-shadow: var(--e-3);
    max-height: min(560px, 70vh); display: flex; flex-direction: column;
    /* The list scrolls INSIDE the cap (see .hs-bell-list); the panel itself
       must not, or the footer scrolls away with the items. */
    overflow: hidden;
}
.hs-bell-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
    padding: var(--s3) var(--s4); border-bottom: 1px solid var(--rule);
}
/* ── Camera scanner overlay ───────────────────────────────────────────────────
   The kiosk used to append a bare <video> at inset:0 with no cancel, no reticle
   and no copy: a clinic tablet went black-with-a-picture-of-the-room for twenty
   seconds with nothing to press. Built by camera.js, styled here — a literal in
   that file could not be themed and would be a white hole in dark mode.

   Fixed, not absolute, so it covers the viewport rather than the nearest
   positioned ancestor. The ink here is deliberately NOT --paper on --ink: a
   camera preview wants a true black surround in both themes, because the
   picture is the content and anything lighter reads as a frame around it. */
.hs-scanner {
    position: fixed; inset: 0; z-index: 200;
    display: grid; place-items: center;
    background: #000;
}
.hs-scanner-video {
    width: 100%; height: 100%; object-fit: cover;
    position: absolute; inset: 0;
}
/* 190px, the design system's scan-target geometry (§6.7). Corner brackets
   rather than a full rectangle: a closed border reads as a frame around the
   whole picture, four corners read as somewhere to aim. */
.hs-scanner-frame {
    position: relative; width: 190px; height: 190px;
    border-radius: var(--r-lg);
}
.hs-scanner-corner {
    position: absolute; width: 28px; height: 28px;
    border: 3px solid var(--paper);
}
.hs-scanner-corner.tl { top: 0; left: 0; border-right: 0; border-bottom: 0; border-top-left-radius: var(--r-lg); }
.hs-scanner-corner.tr { top: 0; right: 0; border-left: 0; border-bottom: 0; border-top-right-radius: var(--r-lg); }
.hs-scanner-corner.bl { bottom: 0; left: 0; border-right: 0; border-top: 0; border-bottom-left-radius: var(--r-lg); }
.hs-scanner-corner.br { bottom: 0; right: 0; border-left: 0; border-top: 0; border-bottom-right-radius: var(--r-lg); }
/* Decoded. The brackets turn brand, and that is the whole confirmation the
   clerk gets before the overlay tears down — it has to be unmissable. */
.hs-scanner-frame.found .hs-scanner-corner { border-color: var(--brand); }
.hs-scanner-foot {
    position: absolute; left: 0; right: 0; bottom: 0;
    display: flex; align-items: center; justify-content: center;
    gap: var(--s4); flex-wrap: wrap;
    padding: var(--s5);
    background: linear-gradient(to top, rgba(0,0,0,.75), transparent);
}
.hs-scanner-copy { color: var(--paper); font-size: var(--t-md); margin: 0; }
.hs-scanner-countwrap { color: var(--paper); font-size: var(--t-sm); opacity: .8; }
/* 44px: this is the only way out of a full-screen modal on a touch device. */
.hs-scanner-cancel { min-height: 44px; padding: 0 var(--s6); }

/* ── Kiosk stage (S1, Home.razor) ─────────────────────────────────────────────
   The single-stage design (Kiosk-1/2 in the design reference) had markup and
   behaviour but no CSS backing before this — every class below rendered on
   bare browser defaults. Centred column, not full-width: a kiosk is read from
   arm's length, and a line that stretches wall to wall is not that. */
.hs-stage, .hs-screen, .hs-done {
    max-width: 620px; margin: var(--s8) auto 0; padding: 0 var(--s5);
}
/* The idle stage (Kiosk-1 in the design reference) is the one screen that is
   fully centred — eyebrow, title, scan target, copy, actions and the status
   line all sit on the column's centre line. The screens after it (MSO/OTP/
   PIN/Reason entry) stay left-aligned: they are forms, and a form reads
   left-to-right, not centre-out. */
.hs-stage { text-align: center; }
/* .hsl-logo is display:block with margin:2px 0 (hsl-brand.css), which ignores
   a centred parent's text-align — same fix as .hs-signin/.hs-landing-head. */
.hs-stage .hsl-logo { height: 56px; margin: 0 auto var(--s4); }
.hs-stage-eyebrow {
    font-family: var(--f-mono); font-size: var(--t-2xs); font-weight: var(--w-semi);
    text-transform: uppercase; letter-spacing: .1em; color: var(--ink-3);
    margin: 0 0 var(--s2);
}
.hs-stage-title, .hs-screen-title, .hs-done-title {
    font-family: var(--f-disp); font-weight: var(--w-bold); font-size: var(--t-3xl);
    line-height: var(--lh-tight); color: var(--ink); margin: 0 0 var(--s3);
}
.hs-stage-body, .hs-screen-hint {
    font-size: var(--t-md); color: var(--ink-2); line-height: var(--lh-base);
    max-width: 46ch; margin: 0 0 var(--s5);
}
.hs-stage .hs-stage-body { margin-left: auto; margin-right: auto; }
.hs-stage-actions { display: flex; flex-wrap: wrap; gap: var(--s3); margin-bottom: var(--s5); }
.hs-stage .hs-stage-actions { justify-content: center; }
.hs-actions-start { justify-content: flex-start; }
.hs-btn-stage {
    min-height: 56px; padding: 0 var(--s6); font-size: var(--t-md); font-weight: var(--w-semi);
    border-radius: var(--r-md);
}

/* The idle-stage scan target — a promise the browser has already confirmed it
   can keep (Home.razor draws this only once Camera.Availability says so). */
.hs-scan-target {
    position: relative; width: 190px; height: 190px;
    display: flex; align-items: center; justify-content: center;
    margin: 0 auto var(--s5);
    border: 2px dashed var(--brand-line); border-radius: var(--r-xl);
    background: var(--brand-soft); color: var(--brand);
}
/* The camera-bump notch on the frame's top edge (Kiosk-1). Decorative only —
   a real element would be one more thing a screen reader has to skip. */
.hs-scan-target::before {
    content: ""; position: absolute; top: -5px; left: 50%; transform: translateX(-50%);
    width: 28px; height: 9px; border-radius: var(--r-full);
    background: var(--st-pend-bg); border: 1px solid var(--st-pend-line);
}
.hs-scan-badge {
    position: absolute; top: var(--s2); right: var(--s2);
    font-family: var(--f-mono); font-size: var(--t-2xs); font-weight: var(--w-semi);
    text-transform: uppercase; letter-spacing: .06em;
    padding: 2px var(--s2); border-radius: var(--r-full);
    background: var(--surface-3); color: var(--ink-2);
}

/* The status/settings line. Now the only connectivity tell on the kiosk — see
   MainLayout.razor, which no longer draws a masthead over it. Plain text with
   a dot, not a chip: Kiosk-1 draws no border or fill around it, and a button
   wearing a full pill outline reads as louder than "everything is fine"
   should be. */
.hs-kioskbar {
    display: inline-flex; align-items: center; gap: var(--s2);
    padding: var(--s2) var(--s3); border: 1px solid transparent; border-radius: var(--r-full);
    background: transparent; font-family: var(--f-ui); font-size: var(--t-xs); color: var(--ink-2);
    cursor: pointer;
}
.hs-kioskbar:hover { background: var(--surface-2); border-color: var(--rule); }
.hs-kioskbar-dot { width: 8px; height: 8px; border-radius: var(--r-full); background: var(--i-400); flex: none; }
.hs-kioskbar-dot.online { background: var(--pos); }
.hs-kioskbar-dot.offline { background: var(--neg); }
.hs-kioskbar-edit {
    padding-left: var(--s2); margin-left: var(--s1); border-left: 1px solid var(--rule);
    color: var(--brand); font-weight: var(--w-med);
}

.hs-screen-form { margin-top: var(--s2); }
.hs-screen-cost { margin-bottom: var(--s4); }
.hs-capture-title { margin-top: var(--s6); }
.hs-capture-foot { margin-top: var(--s4); }

/* Presets — the Pareto 20% (Home.razor's own note on why these exist). */
.hs-presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--s2); margin-bottom: var(--s5); }
.hs-preset {
    display: flex; flex-direction: column; gap: var(--s1); align-items: flex-start;
    padding: var(--s3); border: 1px solid var(--rule); border-radius: var(--r-md);
    background: var(--surface); text-align: left; cursor: pointer;
    transition: background var(--d-fast) var(--ez-out);
}
.hs-preset:hover { background: var(--surface-2); border-color: var(--brand-line); }
.hs-preset-label { font-weight: var(--w-semi); color: var(--ink); }
.hs-preset-desc { font-size: var(--t-xs); color: var(--ink-3); }
.hs-capture-line { margin-top: var(--s4); }

/* Captured-act rows. */
.hs-actrow { padding: var(--s3) 0; border-bottom: 1px solid var(--rule); }
.hs-actrow:last-child { border-bottom: none; }
.hs-actrow-main { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); }
.hs-actrow-tags { display: flex; gap: var(--s2); flex: none; }

/* The pre-auth gate — PENDING tone (amber), not rejected/red: the act is
   awaiting a decision, not refused. */
.hs-gate {
    margin-top: var(--s3); padding: var(--s4);
    border: 1px solid var(--st-pend-line); border-left: 3px solid var(--st-pend);
    border-radius: var(--r-md); background: var(--st-pend-bg);
}
.hs-gate-head { display: flex; gap: var(--s3); align-items: flex-start; }
.hs-gate-icon { font-size: var(--t-lg); flex: none; line-height: 1; }
.hs-gate-title { font-weight: var(--w-semi); color: var(--ink); margin: 0 0 var(--s1); }
.hs-gate-act { color: var(--ink-2); font-size: var(--t-sm); margin: 0; }
.hs-gate-plan { color: var(--ink-3); font-size: var(--t-xs); margin: var(--s2) 0 0; }
.hs-gate-next { margin-top: var(--s3); }
.hs-gate-next-title { font-weight: var(--w-semi); font-size: var(--t-sm); margin: 0 0 var(--s1); }
.hs-gate-next ol { margin: 0; padding-left: var(--s5); color: var(--ink-2); font-size: var(--t-sm); }
.hs-gate-wait { margin-top: var(--s3); font-size: var(--t-sm); color: var(--ink-2); }
.hs-gate-wait strong { color: var(--ink); display: block; margin-bottom: var(--s1); }

/* Done screen — full stop, centred. Accent green is the sanctioned financial
   confirmation; .neutral is the closed-with-nothing-billed case. */
.hs-done { text-align: center; padding-top: var(--s10); }
.hs-done-mark {
    width: 64px; height: 64px; margin: 0 auto var(--s4);
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-full); background: var(--accent); color: var(--on-accent);
    font-size: var(--t-2xl); font-weight: var(--w-bold);
}
.hs-done-mark.neutral { background: var(--surface-3); color: var(--ink-2); }
.hs-done-amount { font-family: var(--f-mono); font-size: var(--t-2xl); font-weight: var(--w-bold); color: var(--ink); margin: 0 0 var(--s2); }
.hs-done-sub { color: var(--ink-2); margin: 0 0 var(--s2); }
.hs-done-ref { color: var(--ink-3); font-size: var(--t-sm); margin: 0 0 var(--s5); }
.hs-done .hs-stage-actions { justify-content: center; }

.hs-countdown { max-width: 280px; margin: 0 auto var(--s5); }
.hs-countdown-track { height: 4px; border-radius: var(--r-full); background: var(--rule); overflow: hidden; margin-bottom: var(--s2); }
.hs-countdown-fill { height: 100%; background: var(--brand); transition: width var(--d-base) linear; }
.hs-countdown-label { font-size: var(--t-xs); color: var(--ink-3); margin: 0; }

.hs-stage-note { margin-bottom: var(--s4); }

/* A plain vertical list of rows, each with a trailing action — the kiosk's
   offline queue sheet is the first user. Named generically on purpose: this is
   the THIRD list-ish pattern in this file, after .hs-bell-* and .hs-picker-*,
   and the three differ only in padding and hover. They should converge here
   rather than a fourth being added the next time something needs a list. */
.hs-rowlist { list-style: none; margin: 0; padding: 0; }
.hs-rowlist-item {
    display: flex; gap: var(--s3); align-items: center; justify-content: space-between;
    padding: var(--s3) var(--pad-card); border-bottom: 1px solid var(--rule);
}
.hs-rowlist-item:last-child { border-bottom: none; }

/* Owner 2026-09-05: "I can't scroll to see all notifications". The panel is a
   capped flex column, but a flex child's min-height defaults to its CONTENT, so
   the list never shrank to the cap and overflow-y had nothing to scroll — the
   items past the cap were simply clipped. min-height:0 + flex:1 is what lets
   the list take the remaining height and scroll inside it; overscroll-behavior
   keeps the wheel on the list instead of the page behind it. */
.hs-bell-list {
    list-style: none; margin: 0; padding: 0;
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}
.hs-bell-item {
    display: flex; gap: var(--s3); align-items: flex-start;
    padding: var(--s3) var(--s4); border-bottom: 1px solid var(--rule);
    cursor: pointer;
}
.hs-bell-item:last-child { border-bottom: none; }
.hs-bell-item:hover { background: var(--brand-soft); }
.hs-bell-item.unread { box-shadow: inset 3px 0 0 var(--brand); }
.hs-bell-item.unread .hs-bell-subject { font-weight: var(--w-semi); }
.hs-bell-body { min-width: 0; }
.hs-bell-subject { font-size: var(--t-sm); }
.hs-bell-meta { font-size: var(--t-2xs); margin-top: 2px; }
.hs-bell-foot {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
    padding: var(--s3) var(--s4); border-top: 1px solid var(--rule); background: var(--surface-2);
}

/* Kiosk/Member have no masthead (Kiosk-1/dc.html:826 draw no top chrome — see
   the doc comments on each app's MainLayout.razor), so .hs-bell cannot sit in
   a masthead's right cluster the way it does on the workbench. Fixed, not
   absolute: it has to stay put regardless of scroll on a long phone-portal
   page. Below .hs-scanner's z-index (200) on purpose — the camera overlay
   covers it during a scan, which is correct; above ordinary content (the
   masthead's own .hs-bell-panel is z-index 60, so 55 sits just under it,
   consistent rather than fighting for the same layer). */
.hs-bell-corner { position: fixed; top: var(--s4); right: var(--s4); z-index: 55; }
/* M-1 (2026-09-07, measured at 390px): the fixed corner cluster (LangToggle
   77x36 + gap + bell 32 ~ 121px wide, y 16-52) sat over .hs-phone-head's 44px
   back button on the member pages and over the kiosk Stepper's 4th step on
   every stage. Reserve the strip under it on the phone/tablet tier. */
@media (max-width: 820px) {
    .hs-app > .hs-main { padding-top: 64px; }
}

/* Kiosk-only: the corner holds the LangToggle alongside the bell, since this
   shell has no masthead to put either in otherwise (see MainLayout.razor). */
.hs-corner-cluster { display: flex; align-items: center; gap: var(--s3); }

/* .hs-app .hs-chip's 44px touch floor (below) is correct for a repeatedly-
   tapped control (a filter, an act preset) on a gloved-thumb clinic tablet —
   but the corner's FR|EN switch is an occasional preference, tapped once
   per shift at most, and at 44px it visually dwarfed the bell beside it
   (product-owner feedback, live review). ~30% down from 44px, landing on
   30px rather than a rounder number so it still clears the 24px AA-relaxed
   floor (WCAG 2.5.5) with margin, just not the 44px "AAA / gloved thumb"
   one this component doesn't need. */
.hs-corner-cluster .hs-lang-toggle { gap: 6px; }
/* The chips-mode globe: quiet ink, vertically centred with the pills. ink-3,
   not ink — it labels the group, it is not one of the choices. */
.hs-lang-mark { color: var(--ink-3); align-self: center; flex: none; }
.hs-corner-cluster .hs-lang-toggle .hs-chip { min-height: 30px; padding: 0 10px; font-size: var(--t-2xs); }

/* Cold-boot loading screen (Kiosk + Member index.html — plain HTML, not
   Razor, since it renders before Blazor exists to render anything). Fires on
   every fresh visit AND every LangToggle switch (CultureSwitcher force-
   reloads so the resolved culture is set before RunAsync — see its own doc
   comment) — worth making legible rather than a bare "Loading…" line, since
   a clerk mid-shift sees it every time they change language. The bar's
   width is driven by boot-progress.js against Blazor's own
   loadBootResource — real fetch progress, not a fake timer. */
.hs-boot {
    display: flex; flex-direction: column; align-items: center; gap: var(--s4);
    height: 100vh; justify-content: center; padding: 0 var(--s5);
    /* var(--f-ui), not system-ui. This was the only PRIMARY system-ui
       declaration left in the RCL, and it sat on the first screen of the two
       installable apps — so the first typography a member or a clerk saw was
       the OS font, then Inter a beat later once the app booted. --f-ui carries
       its own fallback chain, so nothing is lost if Inter has not decoded. */
    font-family: var(--f-ui);
}
/* Square box for a square mark. 96px x auto was sized for the 2.845:1 lockup
   that used to be here and produced a 96x34 box; with an SVG, height:auto lets
   the 512x512 viewBox decide and the splash reflows the moment the file decodes
   -- on the one screen whose whole job is to look composed while waiting. */
.hs-boot-logo { width: 56px; height: 56px; opacity: .9; }

/* The same three dots as every other waiting surface. They sit ABOVE the track:
   the bar carries real fetch progress from boot-progress.js and keeps that job,
   the dots only say "working". */
.hs-boot-dots { display: flex; gap: 6px; }
.hs-boot-dots span {
    width: 8px; height: 8px; border-radius: var(--r-full); background: var(--brand);
    animation: hs-loading-pulse 1.1s var(--ez-io) infinite;
}
.hs-boot-dots span:nth-child(2) { animation-delay: .15s; }
.hs-boot-dots span:nth-child(3) { animation-delay: .3s; }
@media (prefers-reduced-motion: reduce) {
    .hs-boot-dots span { animation: none; opacity: .7; }
}
.hs-boot-track {
    width: min(280px, 70vw); height: 6px; border-radius: var(--r-full);
    background: var(--surface-2); overflow: hidden;
}
.hs-boot-bar {
    height: 100%; width: 5%; border-radius: var(--r-full); background: var(--brand);
    transition: width .25s var(--ez-out);
}
.hs-boot-row {
    display: flex; align-items: center; gap: var(--s3);
    font-size: var(--t-sm); color: var(--ink-2); min-height: 1.4em;
}
.hs-boot-row #hs-boot-pct { font-variant-numeric: tabular-nums; color: var(--ink-3); }

@media (max-width: 560px) {
    /* On a phone the panel is the screen — a 400px dropdown anchored to a bell
       in the corner is unusable at that width. */
    .hs-bell-panel {
        position: fixed; inset: auto var(--s3) var(--s3) var(--s3);
        width: auto; max-height: 70vh;
    }
    /* ── Phone tier: workbench masthead ───────────────────────────────────────
       The H+ mark keeps the product name next to the hamburger; the wordmark
       is already gone at the 820px tier. */
    .hs-mast { padding: var(--s2) var(--s3); gap: var(--s2); }
    /* ── Phone tier: member portal ────────────────────────────────────────────
       The tabbar is fixed; the narrow column is the one main that lives above
       it, so the existing bottom padding is enforced there and only there. */
    .hs-main-narrow { padding-bottom: 76px; }
    .hs-phone-actions { gap: var(--s2); }
    /* ── Phone tier: kiosk stages ─────────────────────────────────────────────
       M-1 (2026-09-07): the rules that used to sit here targeted .hs-kiosk-*
       classes Home.razor never rendered (dead since the stage rewrite). The
       real classes: .hs-stage is the whole screen on a phone — margins and
       padding step down, the scan target shrinks from the 190px tablet
       geometry, and the two stage buttons stop sitting side by side: they are
       the two things a clerk does at the front desk, each thumb-tall and
       full-width. */
    .hs-stage, .hs-screen, .hs-done { margin-top: var(--s5); padding: 0; }
    .hs-stage-actions .hs-btn-stage { flex: 1 1 100%; }
    .hs-scan-target { width: 150px; height: 150px; }
    .hs-stage .hsl-logo { height: 44px; }
    .hs-actrow-main { flex-wrap: wrap; }
    .hs-actrow-tags { flex-wrap: wrap; justify-content: flex-end; }
    .hs-kioskbar { white-space: normal; text-align: left; }
    .hs-presets { grid-template-columns: 1fr 1fr; }
    /* ── Phone tier: toasts ───────────────────────────────────────────────────
       Full-width and pinned to the bottom — above the tabbar where one exists
       (76px), at the very edge otherwise. */
    .hs-toast-host { right: var(--s3); left: var(--s3); bottom: var(--s3); max-width: none; }
    .hs-main-narrow ~ .hs-toast-host,
    .hs-app .hs-toast-host { bottom: calc(76px + var(--s3)); }
}

/* ── App shell for the two installable apps ──────────────────────────────────
   The workbench uses .hs-shell (a sidebar); a phone and a clinic tablet get an
   app bar instead. These classes were referenced by both apps' layouts and
   defined nowhere, so the shells were rendering unstyled. */
/* Transparent on purpose, and it stays transparent — but the reason changed.

   History, because this comment has now been rewritten twice and each rewrite
   reversed the one before it. First it painted an opaque var(--paper), argued
   as deliberate: the grid wash was "a workbench page-canvas treatment" and the
   kiosk and member stages "sit on plain paper" per Kiosk-1/2. Product-owner
   decision 2026-08-18 reversed that and made the graticule wanted on member and
   kiosk too, so the fill became transparent to let the canvas through.

   Client direction 2026-08-19 then removed the graticule everywhere, so the
   thing this rule was made transparent to reveal no longer exists.

   It stays transparent regardless, for the OTHER thing underneath: the fixed
   <Aurora /> layer at z-index 0. Painting an opaque background here would hide
   the aurora on the member portal and the kiosk while the workbench kept it —
   which is how the three shells would silently drift apart again.

   So: transparent, not a removed declaration. .hs-app must keep painting
   nothing rather than inherit. */
/* background and color are set HERE, not left to body, and that is load-bearing
   now that data-theme lives on this element rather than on <html>.

   body sits OUTSIDE the themed subtree, so --paper and --ink resolve there from
   the light :root no matter what the user picked. Measured before this line
   existed: with data-theme="dark" on .hs-app, body painted rgb(245,247,252) and
   text inherited rgb(8,20,58) — Midnight ink on Mist, i.e. the light theme, on
   a shell that believed it was dark. Both halves have to be re-anchored inside
   the subtree.

   Not transparent any more, and Aurora is unaffected: an element's own
   background paints BELOW its descendants, and <Aurora /> is a child. It still
   reads exactly as it did. What is gone is the page-canvas grid wash this used
   to be transparent for, which was removed on 19 Aug. */
.hs-app { min-height: 100vh; display: flex; flex-direction: column; background: var(--paper); color: var(--ink); }

.hs-appbar {
    display: flex; align-items: center; gap: var(--s4);
    padding: var(--s3) var(--s5);
    background: var(--surface-3); border-bottom: 1px solid var(--rule);
    position: sticky; top: 0; z-index: 20;
}
.hs-appbar .hsl-logo { height: 26px; flex: none; }
.hs-appbar-title {
    font-family: var(--f-disp); font-weight: var(--w-semi); font-size: var(--t-md);
    color: var(--brand-lo);
}
.hs-appbar-actions { margin-left: auto; display: flex; align-items: center; gap: var(--s3); }
.hs-appbar a, .hs-appbar .nav-link {
    color: var(--ink-2); font-size: var(--t-sm); font-weight: var(--w-med);
    padding: var(--s2) var(--s3); border-radius: var(--r-md);
}
.hs-appbar a:hover { background: var(--brand-soft); color: var(--brand-lo); text-decoration: none; }
.hs-appbar a.active { background: var(--brand); color: var(--on-brand); }

/* One column, thumb-reachable. A member's cover is read top to bottom; there is
   nothing to put beside it. */
.hs-main-narrow { max-width: 640px; margin: 0 auto; width: 100%; }
/* The kiosk shell's canvas (was an inline padding on <main>, untunable). */
.hs-main-kiosk { padding: var(--s5); }

@media (max-width: 560px) {
    .hs-appbar { padding: var(--s2) var(--s3); gap: var(--s3); }
    .hs-main { padding: var(--s4) var(--s3); }
    .hs-main-kiosk { padding: var(--s4) var(--s3); }
    /* The 44px touch floor reaches the workbench on a phone (UI-DESIGN-SYSTEM-
       AUDIT §1 density seam). .hs-app owns it for the PWAs; the workbench's
       canvas is .hs-main, so it gets the same floor at the phone tier. */
    .hs-main .hs-btn { min-height: 44px; padding: 0 var(--s5); }
    .hs-main .hs-chip { min-height: 44px; }
    .hs-main .hs-input, .hs-main .hs-select, .hs-main .hs-textarea { min-height: 44px; }
}
/* Touch floors for the installable apps. Density decides how much fits; a
   finger is the same size either way, so these are minimums layered on top of
   the compact tokens rather than a competing density. */
.hs-app .hs-btn { min-height: 44px; padding: 0 var(--s5); font-size: var(--t-md); }
.hs-app .hs-input, .hs-app .hs-select, .hs-app .hs-textarea { min-height: 44px; font-size: var(--t-md); }
/* 44, not 40. This sat inside the block that puts a 44px floor under everything
   else and quietly undercut it by four pixels — which is the sort of thing that
   survives review precisely because it is nearly right. The design states ≥44px
   for kiosk and mobile as non-negotiable (dc.html:274), and a chip on a clinic
   tablet is a filter someone taps with a gloved thumb. */
.hs-app .hs-chip { min-height: 44px; }
.hs-app .hs-table tbody td { padding: var(--s3) var(--s4); }

/* ── Landing page (/welcome) ──────────────────────────────────────────────────
   The front door, and the only page in the workbench an anonymous visitor sees.
   Three destinations for three populations, because "the platform" means
   something different to staff, to a member and to a provider.

   Compact still applies: the cards are generous relative to a data grid, not
   relative to a marketing page. This is a signpost, not a brochure. */
.hs-plain { min-height: 100vh; padding: var(--s6); }
/* Content sits above the fixed aurora layer (z-index 0). */
.hs-plain { position: relative; z-index: 1; }

/* ── Error page ────────────────────────────────────────────────────────────
   The server error page (Error.razor), rendered on EmptyLayout. Centred and
   quiet, with the brand mark and a working way back — an error is a dead end
   only if it offers none. */
.hs-error {
    max-width: 560px; margin: 10vh auto; padding: var(--s8) var(--s6);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-lg);
    box-shadow: var(--e-2); text-align: center;
}
.hs-error-card h1 { font-family: var(--f-disp); font-size: var(--t-2xl); margin: var(--s4) 0 var(--s2); }
.hs-error-mark {
    width: 52px; height: 52px; margin: 0 auto; border-radius: var(--r-md);
    background: var(--brand); color: var(--on-brand);
    display: grid; place-items: center;
    font-family: var(--f-disp); font-weight: var(--w-black); font-size: var(--t-lg);
    letter-spacing: -1px; box-shadow: var(--e-2);
}
.hs-error .hs-btn-row { justify-content: center; }

.hs-landing { max-width: 980px; margin: 0 auto; padding: 6vh 0 var(--s10); }

.hs-landing-head { text-align: center; margin-bottom: var(--s10); }
/* margin-inline:auto, not text-align. hsl-brand.css makes .hsl-logo a BLOCK
   with `margin: 2px 0 …`, and a block box ignores the parent's text-align — so
   the mark sat hard left under a centred heading. */
.hs-landing-head .hsl-logo { height: 44px; margin: 0 auto var(--s6); }
.hs-landing-head h1 {
    font-family: var(--f-disp); font-size: var(--t-2xl);
    line-height: var(--lh-tight); margin: 0 0 var(--s3);
}
.hs-landing-head p {
    max-width: 46ch; margin: 0 auto; color: var(--ink-2);
    font-size: var(--t-md); line-height: var(--lh-base);
}

/* .hs-landing-grid and the .hs-landing-card* family were deleted 2026-08-17 with
   the /welcome rebuild — see .hs-welcome below, which replaced them. .hs-landing,
   .hs-landing-head and .hs-landing-foot STAY: /onboard (Onboard.razor:26,27,134)
   still uses all three, so this family is shared, not dead. */

.hs-landing-foot {
    margin-top: var(--s10); padding-top: var(--s5);
    border-top: 1px solid var(--rule);
    display: flex; justify-content: space-between; align-items: center;
    gap: var(--s4); flex-wrap: wrap; font-size: var(--t-sm);
}
.hs-landing-foot a { color: var(--brand); font-weight: var(--w-semi); text-decoration: none; }
.hs-landing-foot a:hover { text-decoration: underline; }

/* ── /welcome — the front door ───────────────────────────────────────────────
   Rebuilt 2026-08-17 from docs/design formats/HSL Welcome Page (standalone).html.
   The markup is MudBlazor components (Welcome.razor); this supplies only the
   layout and skin the reference specifies and Mud has no opinion about — the
   split shell, the dark rail, and the row geometry.

   The rail is a fixed 460px track and the rows take the rest, so the two never
   negotiate: the reference's headline is set at a size that only works against a
   known column width. Below --bp-md the whole thing stacks and the rail stops
   being full height, because a 460px dark band on a phone is most of the screen
   before any content. */
/* LIGHT ONLY, by product decision 2026-08-17. The design is drawn for a light
   ground — the rail's deep brand block reads as intended only against it, and
   the rows' brand wash inverts to a near-black on a dark paper. (The rail also
   carried a .05-alpha graticule when this was written; that lattice was removed
   19 Aug 2026 and is not a reason for this rule any more.) So the nine
   semantic tokens the dark blocks flip
   (tokens.css:264-286 and its byte-identical prefers-color-scheme twin) are
   re-pinned here to the same LIGHT values the base :root declares, which holds
   for all three theme states: explicit dark, explicit light, and system.

   Re-declared rather than hardcoded — each still points at the same scale token
   the light block uses, so a palette change flows through here too. The scale
   itself (the --b- and --i- ramps) is theme-independent and needs no pinning.
   Do not write those two names as a glob pair separated by a slash: the
   asterisk-slash sequence that produces CLOSES THIS COMMENT, and everything
   after it — including the real terminator — is then parsed as CSS. That is not
   hypothetical. It is what this line used to say, and it silently discarded the
   entire .hs-welcome rule below (selector and all), which is why the front door
   rendered as one stacked column from the day it was written while the rule sat
   in the file looking perfectly correct.
   color-scheme:light comes with them so the browser paints form controls,
   scrollbars and the canvas light regardless of the OS setting. */
.hs-welcome {
    /* position: relative only so .hs-welcome-lang can anchor to the split's
       own corner — it changes nothing else (no overflow, no stacking use). */
    position: relative;
    color-scheme: light;
    --brand: var(--b-600);
    --brand-line: var(--b-200);
    --brand-lo: var(--b-800);
    --brand-soft: var(--b-50);
    --paper: var(--i-25);
    --ink: var(--i-950);
    --ink-2: var(--i-500);
    --ink-3: var(--i-400);
    --rule: var(--i-100);
    /* .85, tracking tokens.css. Every other line in this block re-pins a token
       by REFERENCE (var(--i-400) and friends), so they follow the palette on
       their own; this one is a LITERAL and does not. Leaving it at .55 would
       have kept /welcome on the surface that measured --ink-3 at 3.51:1 while
       every other host moved to .85 — one half of a pair, on the one page that
       is not behind a login, with every guard green. If the token in tokens.css
       moves again, this line has to move with it. */
    --surface-2: rgba(245,247,252,.85);

    min-height: 100vh; min-height: 100dvh;   /* dvh: mobile URL bar */
    display: grid;
    background: var(--paper); color: var(--ink);

    /* Plain track sizes, stepped down by the media queries at the end of this
       block. This was briefly clamp(300px, 30vw, 460px), which rendered as a
       SINGLE STACKED COLUMN on a full-width laptop even though the rule was
       live and unopposed in production (verified: only hirms.css defines
       .hs-welcome, it is cache-busted, and neither MudBlazor.min.css nor
       app.css touches it). An invalid track list makes the whole
       grid-template-columns declaration drop, and a grid with no declared
       columns puts every child in its own row — which is exactly the symptom.
       Fixed lengths are the boring, universally parsed form, and after the
       parameter-name outage earlier today the boring form is the right call:
       this page is the front door and the sign-in route for every operator. */
    grid-template-columns: 460px 1fr;
}

/* The rail gives up width in steps before the layout gives up its shape. The
   breakpoints matter more than they look: they are CSS pixels, so Windows
   display scaling at 150% turns a 1360-device-pixel laptop into ~907 of them.
   A single cut at 900px therefore put ordinary laptops on the phone layout. */
@media (max-width: 1180px) { .hs-welcome { grid-template-columns: 380px 1fr; } }
@media (max-width:  940px) { .hs-welcome { grid-template-columns: 320px 1fr; } }
@media (max-width:  700px) { .hs-welcome { grid-template-columns: 260px 1fr; } }

/* Full bleed. EmptyLayout's <main class="hs-plain"> carries padding: var(--s6),
   which is right for /onboard and /Error — both are centred cards on a plain
   ground — and wrong for this one, which is edge-to-edge by design. Left in, it
   framed the dark rail in a pale gutter on all four sides and cost the layout
   the full viewport height it asks for. Scoped with :has so the shared layout
   keeps its padding for the two pages that want it, rather than moving the
   padding onto each page and touching three files to fix one. */
.hs-plain:has(> .hs-welcome) { padding: 0; }

.hs-welcome-rail {
    position: relative; overflow: hidden;
    background: var(--b-900);
    padding: var(--s10) var(--s8);
    display: flex; flex-direction: column; justify-content: space-between;
}

/* Two drifting blobs, clipped by the rail's own overflow. Deliberately NOT
   <Aurora />: that paints three fixed viewport-sized blobs behind an entire
   shell, which is the wrong instrument for a 460px column. */
.hs-welcome-rail-blob {
    position: absolute; border-radius: 50%; pointer-events: none;
    will-change: transform;
}
.hs-welcome-rail-blob-a {
    width: 460px; height: 460px; left: -140px; top: -120px;
    background: radial-gradient(circle, rgba(111,82,246,.55), transparent 70%);
    filter: blur(80px); animation: hs-welcome-drift-a 32s var(--ez-io) infinite;
}
.hs-welcome-rail-blob-b {
    width: 380px; height: 380px; right: -160px; bottom: -60px;
    background: radial-gradient(circle, rgba(45,107,255,.34), transparent 70%);
    filter: blur(90px); animation: hs-welcome-drift-b 40s var(--ez-io) infinite;
}
@keyframes hs-welcome-drift-a {
    0%, 100% { transform: translate3d(0,0,0) scale(1); }
    50%      { transform: translate3d(6%,4%,0) scale(1.12); }
}
@keyframes hs-welcome-drift-b {
    0%, 100% { transform: translate3d(0,0,0) scale(1.05); }
    50%      { transform: translate3d(-7%,-3%,0) scale(1); }
}

/* The 24px graticule is GONE, with the page-canvas one. Both were v1
   "instrument" devices, neither is in Brand Book v2.0, and at the new aurora
   strength they read as noise rather than texture. This one mattered more than
   the canvas grid: it sat on the front door, and being hardcoded to the old
   warm paper it was invisible to both the --grid-line grep and the CI hex
   guard. The element is kept as an empty positioned layer so Welcome.razor's
   markup and the rail's stacking order do not have to change. */
.hs-welcome-rail-grid {
    position: absolute; inset: 0; pointer-events: none;
}

/* position:relative on both stacks — the blobs are absolutely positioned
   siblings, and without a stacking position of their own the content would sit
   under the blur. */
.hs-welcome-rail-top, .hs-welcome-rail-foot { position: relative; }

/* No backing chip: the rail uses the REVERSED logo, which is drawn for dark
   grounds and needs nothing behind it.

   hsl-logo.png is the full-colour mark — mid-blue nodes over a dark-teal
   "Systems Link" wordmark — and on this rail's --b-900 ground it simply
   disappeared. A light chip fixed the contrast but put a white slab in the
   corner of a deliberately dark composition, which the reference design does
   not have. hsl-logo-reversed.png (docs/HSL LOGO/HSL Logo-01.png, RGBA with a
   genuine alpha channel — verified, not assumed) carries the green wordmark and
   white nodes on transparency, so it reads on the dark rail unaided and the
   corner stays dark.

   45px, up from 32 (product-owner ask 2026-08-17, "increase logo size by 40%":
   32 × 1.4 = 44.8). Note the masthead keeps the full-colour mark and its own
   dark-mode chip — that bar is light in the default theme, so it is a different
   problem with a different right answer. */
/* 72px, up 60% from 45px — product-owner ask, 19 Aug 2026, and scoped to the
   welcome rail alone. Every other surface keeps its own size: .hsl-logo 34px in
   the shared lockup, .hs-mast-logo in the masthead, .hs-boot-logo on the boot
   screen. The mobile step below is scaled by the same 60%, 38px -> 61px, so the
   two breakpoints stay in proportion rather than drifting apart. */
.hs-welcome-logo {
    height: 72px; width: auto; margin: 0 0 var(--s10); align-self: flex-start;
    /* max-width is the COMPENSATING HALF of the vector swap, not tidying.
       The vector is aspect 3.5515 against the raster's 2.8453, so at a fixed
       72px height it renders 255.7px instead of 204.9px. The rail steps
       460/380/320/260px and pads var(--s8) each side, giving content boxes of
       396/316/256/196px — so on the 260px step the lockup overhangs by 59.7px,
       and .hs-welcome-rail is overflow:hidden, which would clip "Systems Link"
       on every 561-700px viewport. The raster already overhung it by 8.9px;
       this makes it 6.7x worse without the cap.
       In the BASE rule rather than a new breakpoint, so it covers all four steps
       and any future one. */
    max-width: 100%;
}

.hs-welcome-display {
    font-family: var(--f-disp);
    font-size: clamp(2.5rem, 4.4vw, 4rem);
    line-height: .96; font-weight: var(--w-bold);
    letter-spacing: -.035em; color: var(--i-25);
    margin: 0;
}
.hs-welcome-keyline {
    width: 56px; height: 2px; background: var(--b-200);
    margin: var(--s5) 0;
}
.hs-welcome-lede {
    max-width: 300px; margin: 0;
    color: rgba(245,247,252,.66);
    font-size: var(--t-md); line-height: 1.62; text-wrap: pretty;
}

/* Two classes on all three rules below: these sit on MudBlazor's own
   .mud-divider and .mud-link, which set border-color and color from the Mud
   palette. Matching them on specificity is what the .mud-dialog.hs-modal rules
   above do for the same reason — an override flag would hide the specificity
   question rather than answer it, and CI rejects it on exactly that ground. */
.mud-divider.hs-welcome-rail-rule {
    border-color: rgba(245,247,252,.14); margin-bottom: var(--s4);
}
.mud-link.hs-welcome-out {
    font-family: var(--f-mono); font-size: var(--t-sm);
    letter-spacing: .04em; color: var(--b-200);
}
.mud-link.hs-welcome-out:hover { color: var(--i-25); }

/* ── the four entry rows ──────────────────────────────────────────────────── */
.hs-welcome-rows {
    min-width: 0;                    /* lets the grid track actually shrink */
    display: flex; flex-direction: column; justify-content: center;
}

/* MudLink renders an <a> and sets its own colour/typography; the overrides here
   are what turn it back into a full-bleed row. display:grid on the anchor itself
   (rather than an inner wrapper) keeps the whole row one hit target. */
.mud-link.hs-welcome-row {
    position: relative;
    display: grid; grid-template-columns: 44px 1fr auto;
    align-items: center; gap: 0 var(--s5);
    padding: var(--s8) var(--s10);
    border-bottom: 1px solid var(--rule);
    color: inherit; text-decoration: none;
    transition: background var(--d-fast) var(--ez-out);
}
.hs-welcome-row:hover { background: var(--surface-2); }
.hs-welcome-row-last { border-bottom: 0; }

/* The lead row: brand wash, a heavier accent bar, and top-aligned because its
   description runs longer than the rest. */
.hs-welcome-row-primary {
    align-items: start;
    background: var(--brand-soft);
    border-top: 1px solid var(--rule);
}
.hs-welcome-row-primary:hover { background: var(--b-100); }

/* The accent bar. A pseudo-element, not a div — it is pure decoration and has no
   business in the accessibility tree or the markup. */
.hs-welcome-row::before {
    content: ""; position: absolute; left: 0; top: 0; bottom: 0;
    width: 3px; background: var(--brand-line);
}
.hs-welcome-row-primary::before { background: var(--brand); }

.hs-welcome-row-num {
    font-family: var(--f-mono); font-size: var(--t-sm);
    color: var(--ink-3); align-self: start; padding-top: var(--s1);
}
.hs-welcome-row-primary .hs-welcome-row-num { color: var(--brand); padding-top: 6px; }

.hs-welcome-row-body { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }

.hs-welcome-row-kicker {
    font-family: var(--f-mono); font-size: var(--t-xs);
    letter-spacing: .16em; text-transform: uppercase;
    color: var(--brand-lo); line-height: 1;
}
.hs-welcome-row-title {
    font-family: var(--f-disp); font-weight: var(--w-semi);
    letter-spacing: -.02em; line-height: var(--lh-tight); margin: 0;
}
.hs-welcome-row-primary .hs-welcome-row-title { letter-spacing: -.025em; }
.hs-welcome-row-desc {
    color: var(--ink-2); max-width: 54ch;
    line-height: 1.6; text-wrap: pretty; margin: 0;
}
.hs-welcome-row-go {
    font-size: var(--t-sm); font-weight: var(--w-semi);
    color: var(--brand-lo); white-space: nowrap;
}
/* HtmlTag="span" on the MudButton (an anchor may not contain a button), so it
   needs the pointer restored — Mud only sets it on real button elements. */
/* text-transform:none because MudBlazor uppercases .mud-button-root by Material
   convention, which turned the reference's "Sign in →" into "SIGN IN →". The
   design sets this button in sentence case and it is the page's one primary
   action, so it follows the design rather than Material's default. */
.hs-welcome-row-cta { white-space: nowrap; cursor: pointer; text-transform: none; }

/* 760, not 900. At 900 this fired on ordinary Windows laptops running 150%
   display scaling (see the clamp note on .hs-welcome), so the stacked layout —
   which is meant for phones — was what most desktop visitors actually saw. The
   two-column composition survives to 760px now because the rail is fluid and
   gives up width before the layout gives up its shape. */
/* 560, not 760 — and the history is the point, because this breakpoint has now
   been wrong twice in the same direction.

   These are CSS pixels. Windows display scaling multiplies them: a 1360-device-
   pixel laptop reports ~907 CSS px at 150% and ~755 at 180%, and browser zoom
   stacks on top of that. Both earlier values (900, then 760) therefore served
   the PHONE layout to ordinary laptops — twice confirmed against the product
   owner's own screenshots, on two different browsers, with the CSS rule and the
   DOM both verified correct in production each time (the grid has exactly two
   direct children at depth 1; only hirms.css defines .hs-welcome; it is
   cache-busted). Nothing was wrong except this number.

   560px is below any laptop at any plausible scaling, so the two-column
   composition the design calls for is what a desktop gets, full stop. The rail
   steps 460 -> 380 -> 320 -> 260 on the way down and only genuinely narrow
   phones stack — which is the one place stacking is the right answer. */
@media (max-width: 560px) {
    .hs-welcome { grid-template-columns: 1fr; min-height: 0; }

    /* On a phone the rail is a masthead, not a column, and its job is to be
       read once and scrolled past. Left at desktop proportions it ran to
       roughly 440px — the whole viewport — so "Sign in to the workbench", the
       reason most people open this page, started below the fold. Everything
       below trims the rail rather than the content: nothing is hidden, it is
       just set at a size that suits the width. */
    .hs-welcome-rail { padding: var(--s8) var(--s6); }
    .hs-welcome-logo { height: 61px; margin-bottom: var(--s5); }
    .hs-welcome-display { font-size: clamp(2rem, 9vw, 2.75rem); }
    .hs-welcome-keyline { margin: var(--s4) 0; }
    .hs-welcome-lede { max-width: none; font-size: var(--t-sm); }
    .hs-welcome-rail-foot { margin-top: var(--s6); }

    /* Every row drops its action under the text, not just the primary one.
       Row 1's CTA already did; rows 2-4 kept a three-track grid whose `auto`
       column held "Open the member app →" against a title competing for the
       same ~300px. Uniform beats clever here — one shape for every row. */
    .hs-welcome-row { padding: var(--s6) var(--s5); gap: 0 var(--s4); }
    .mud-link.hs-welcome-row { grid-template-columns: 40px 1fr; align-items: start; }
    .hs-welcome-row-go { grid-column: 2; margin-top: var(--s2); }
    .hs-welcome-row-cta { grid-column: 2; margin-top: var(--s3); justify-self: start; }
}

/* The global reduced-motion rule zeroes animation-duration, which stops the
   drift outright. Restated here because these two are decorative and the
   intent should be obvious at the rule, not three thousand lines away. */
@media (prefers-reduced-motion: reduce) {
    .hs-welcome-rail-blob { animation: none; }
}

/* Blazor's enhanced navigation moves focus to the page heading for screen-reader
   users, and Chrome then paints its default focus ring across it — a black
   rectangle the full width of the content, on a page nobody has interacted with.
   The focus MOVE is correct and must stay: removing tabindex would silently cost
   a screen-reader user their landmark. Only the ring is suppressed, and only for
   a heading, which is not an interactive control a keyboard user tabs to. */
h1[tabindex="-1"]:focus,
h1[tabindex="-1"]:focus-visible,
.hs-landing-head h1:focus,
.hs-landing-head h1:focus-visible {
    outline: none;
}

/* ── Karine, the assistant surface (docs/15 §10, KarineWidget.razor) ─────────
   THE Z-INDEX WAS MEASURED, NOT PICKED. Read from the computed --mud-zindex-*
   variables on :root of the running workbench:

       appbar 1300  ->  mud-overlay-drawer   = appbar+1 = 1301
                        mud-drawer-temporary = appbar+2 = 1302
       dialog 1400  ->  mud-overlay-dialog   = dialog+1 = 1401
                        mud-dialog           = dialog+2 = 1402
       snackbar 1500    popover 1200         tooltip 1600

   1350 clears 1302 and loses to 1401. Both halves matter. SidePanel is a
   temporary MudDrawer with Overlay="true", so every claim drill-down lays a
   full-viewport scrim at 1301 — a widget beneath it goes unclickable exactly
   when an assistant is most wanted. And losing to 1401 keeps HsConfirmDialog
   unarguable, as .hs-loading-scrim at 2000 stays unarguable above everything.
   Nothing else in this file occupies 200..2000.

   This is the third time the range has mattered here; the first two are written
   up at the SidePanel and loading-overlay rules. The difference this time is
   that a browser was opened and the numbers read off a live page, rather than
   inferred from the stylesheet — which is the same lesson the welcome-page grid
   taught, where a comment terminated early by a stray sequence had been
   silently voiding the rule that followed it for the page's whole life.

   Opaque, never glass: this panel can sit over dense claim tables, and
   backdrop-filter over small text is what makes the amount column unreadable. */
/* ── Karine's masthead control ─────────────────────────────────────────────
   Product-owner ask, 2026-08-21: "remove the chat button and add a logo button
   center of the topbar panel, that pulsates periodically, tooltip Chat with
   Karine AI. When the user clicks the logo button we get the chat popup."

   This REPLACES `.hs-mast-karine` — a 32px outline chat bubble that shared
   .hs-mast-icon's geometry and sat in the right-hand cluster between Refresh
   and Bell. That class is deleted here, its markup is gone from
   MainLayout.razor, and its two resource keys (Mast.ShowKarine / Mast.HideKarine)
   are gone from BOTH AppMasthead resx files and from the test that asserted on
   them. Leaving any one of those behind is the half-a-pair defect this rebrand
   has now shipped nine times.

   The MARK (hsl-icon.svg), never a lockup. `.hs-mast-logo` above already
   carries the compact LOCKUP top-left; a second lockup on the same 66px bar
   puts the wordmark on screen twice and reads as a duplicated header rather
   than as a control. The mark is 28px inside the 40px disc — the kit's 72%
   symbol-to-tile ratio rounded down, the same ratio .hs-karine-bubble-mark
   holds at 40-in-56.

   Base state here is IN FLOW, beside the brand. Centring is the override, in
   the min-width:601px block below, and mobile-first is deliberate: the narrow
   tier then needs nothing undone. */
.hs-mast-assist {
    display: grid; place-items: center;
    width: 40px; height: 40px; padding: 0; flex: none;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--r-full);
    cursor: pointer;
    /* PERIODIC, not continuous. See the keyframes below for how. */
    animation: hs-mast-assist-pulse 12s var(--ez-out) infinite;
}
.hs-mast-assist-mark { width: 28px; height: 28px; display: block; }
.hs-mast-assist:hover { border-color: var(--brand); }
/* The global *:focus-visible rule sets border-radius: var(--r-xs) ON the
   focused element — its own comment at the top of this file warns that a pill
   squares off to 3px while focused, and says to override the radius rather than
   the selector when that reads badly. On a 40px disc it reads as the control
   breaking. Restated at (0,2,0) so it wins. (.hs-mast-avatar has the identical
   pre-existing behaviour; left alone deliberately — separate control, separate
   decision, not something to change while nobody is looking at it.) */
.hs-mast-assist:focus-visible { border-radius: var(--r-full); }

/* The OPEN-state VISUAL, and it is not garnish. aria-pressed tells a
   screen-reader user whether the chat is on screen; with no paired visual a
   sighted user has nothing at all. Shipping the ARIA half alone is the same
   half-a-pair shape as adding an English key and forgetting the French one.
   Carried over from .hs-mast-karine, which had exactly this rule for exactly
   this reason. */
.hs-mast-assist[aria-pressed="true"] {
    background: var(--brand-soft);
    border-color: var(--brand);
    /* And the pulse STOPS once she is open. A control that is already on has
       nothing left to ask for, and a mark blinking beside an open chat panel
       for the length of an adjudication is precisely the distraction the
       "periodically" in the ask exists to avoid. */
    animation: none;
}
.hs-mast-assist[aria-pressed="true"]:hover { background: var(--brand-soft); }

/* TWO beats inside a 12s cycle, then 10.4s of nothing.

   That is the whole reason this is one long keyframe timeline rather than the
   obvious `pulse .9s infinite`: a naive infinite animation never stops, and a
   control that blinks at an assessor for the entire length of a claim
   adjudication is both a distraction and a WCAG 2.2.2 (Pause, Stop, Hide)
   problem. Motion here occupies 0% to 13.5% of the timeline — 1.62s of 12s.
   From 13.5% to 100% every animated property holds its resting value, so the
   element is genuinely static for 86.5% of every cycle.

   transform is animated HERE AND NOWHERE ELSE on this element. That is why the
   centring below uses margin-left/margin-top and not translate(-50%, -50%): a
   keyframe that sets `transform` replaces the WHOLE property, so a translate
   used for centring would be dropped for the length of every beat and the
   button would jump half its width left and half its height up, twice per
   cycle. The two cannot fight, because they are different properties.

   The ring is box-shadow, not border and not outline. A border would change the
   box and shove the bar's layout on every beat; outline cannot be
   spread-animated. 12px of spread and no more: the button is 40px centred in a
   66px bar (12px padding + 42px .hs-mast-logo + 12px), so 13px is exactly the
   clearance to the bar's edge and 12px keeps a pixel of it.

   THE RING COLOUR WAS MEASURED, because the previous attempt's was not. That
   one drew 1.5px solid var(--brand-line); --brand-line is --b-200 #B9CEFF,
   which measures 1.52:1 against --paper #FFFFFF — an animation nobody could see
   was running, and far under the 3:1 WCAG 1.4.11 asks of a non-text indicator.
   This peaks at --brand 90%. The token resolves to #2D6BFF in light mode, and
   composited at .9 over #FFFFFF that is rgb(66 122 255) = 3.85:1 (pure --brand
   on --paper is 4.51:1, the figure tokens.css already records for --b-500).
   Dark mode re-derives from the same token — #4C82FF on #0A1230 is 5.2:1 — so
   this is one declaration, not two hand-tuned ones.

   NOTE the sentence shapes above. CI's design guard greps this whole file —
   comments included — for a colon followed by a hex value, because that is what
   a raw colour DECLARATION looks like (.github/workflows/ci.yml). Prose that
   introduces a measured colour with a colon therefore fails the build, and it
   fails it identically to a real violation, so the error message points at
   nothing useful. Written up here rather than fixed by a grep exception,
   because the guard is right and the prose is what should move: keep hex values
   in comments introduced by a word or a dash, never by a colon. This paragraph
   was itself the second occurrence, having explained the first one by quoting
   it. */
@keyframes hs-mast-assist-pulse {
    /* beat one */
    0%    { transform: scale(1);    box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand) 90%, transparent); }
    3%    { transform: scale(1.08); }
    6%    { transform: scale(1);    box-shadow: 0 0 0 12px transparent; }
    /* The ring snaps back to zero spread WHILE it is fully transparent, so the
       reset cannot be seen: a 0-spread, 0-blur, 0-offset shadow is clipped to
       the border box and never paints, whatever its alpha. Interpolating
       straight from "12px transparent" to "0 opaque" instead would flash a
       half-opacity ring contracting inwards between the two beats. */
    6.5%  { transform: scale(1);    box-shadow: 0 0 0 0 transparent; }
    7%    { transform: scale(1);    box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand) 90%, transparent); }
    /* beat two — a double beat reads as deliberate where a lone one reads as a
       repaint glitch */
    10%   { transform: scale(1.08); }
    13%   { transform: scale(1);    box-shadow: 0 0 0 12px transparent; }
    /* rest: 13.5% to 100% is 10.38s in which nothing moves at all */
    13.5% { transform: scale(1);    box-shadow: 0 0 0 0 transparent; }
    100%  { transform: scale(1);    box-shadow: 0 0 0 0 transparent; }
}

/* Movement off, at the operating system's request.

   The global rule at the top of this file already collapses every
   animation-duration to .01ms !important, which on its own would leave THIS
   element restarting a nine-keyframe timeline every hundredth of a
   millisecond, forever. `animation: none` kills animation-NAME instead, so
   nothing runs at all.

   SPECIFICITY IS THE POINT, and it is why this rule reads the way it does:
   (0,1,0), matching .hs-mast-assist exactly, and placed after it so source
   order decides. A media query contributes NO specificity of its own — that is
   the trap this note exists to record. The only other rule touching `animation`
   on this element is the [aria-pressed="true"] one at (0,2,0), and that one
   already sets `animation: none`, so there is no state in which movement
   survives a reduced-motion preference. */
@media (prefers-reduced-motion: reduce) {
    .hs-mast-assist { animation: none; }
}

/* CENTRING, and only from 601px up.

   .hs-mast is position:sticky, which already makes it a containing block for
   absolutely positioned descendants — no new position:relative is needed, or
   wanted: adding one would break the sticky the whole shell depends on.

   601px, not 481px, and not "the narrowest widths". The band that actually
   collides is 481-600px, where .hs-mast-inline-settings is still an INLINE row
   — language dropdown, theme dropdown, refresh, bell, avatar, roughly 300px of
   cluster — and the bar's midpoint lands inside it. Below 481px that row
   collapses behind the gear (the max-width:480px block above), but the viewport
   shrinks faster than the cluster does, so the midpoint is no safer there. One
   gate at 601px covers both, and below it the control simply stays where the
   markup puts it: in flow, beside the brand.

   That markup position is the other half of this rule. AppMasthead renders
   @Assistant immediately after .hs-mast-brand, so DOM order — and therefore tab
   order — runs brand, assistant, settings, refresh, bell, account: left to
   right, exactly as the centred button reads on screen. Leaving the markup in
   the right-hand cluster and centring it with CSS would make a keyboard user
   tab PAST the visually-centred control to reach it, which is WCAG 2.4.3
   (Focus Order) failed by a stylesheet. */
@media (min-width: 601px) {
    .hs-mast-assist {
        position: absolute;
        left: 50%; top: 50%;
        /* Half of 40px, on both axes. NOT translate(-50%, -50%) — see the
           keyframes note above; the pulse owns `transform` alone. */
        margin-left: -20px; margin-top: -20px;
    }
}

/* IN FLOW, WEAR THE ICON ROW'S SIZE. The 40px disc is what the control is worth
   when it is centred and costs the bar nothing; below 601px it is a flex item
   again and every pixel is somebody else's.

   The breakpoint MATCHES the centring gate above at 600/601, and 600 rather
   than 480 is what the measurement said. Chromium, this stylesheet, the real
   masthead markup, document.scrollWidth:

       viewport   32px control (what this replaces)   40px disc, in flow
         390px    417  (overflows — see below)        425  (overflows)
         481px    481  (exactly full, 0 slack)        484  ← 3px of scrollbar
         500px    500                                 500
         600px    600                                 600

   481px is the pinch, and it is the width the design pass predicted: 481 is the
   first pixel at which .hs-mast-inline-settings stops being a gear and becomes
   an inline row, so the right-hand cluster is at its widest relative to the
   viewport. Three pixels is a scrollbar on the masthead, which is not a
   rounding error to anyone looking at it.

   At var(--ctl) the footprint is exactly what .hs-mast-karine occupied, so every
   width below 601px measures byte-for-byte what it did before this change, and
   601px up measures 40px NARROWER, because an absolutely positioned child
   contributes nothing to a flex row. Holding 40px only where it costs the row
   nothing is the whole rule.

   SEPARATELY, and NOT caused by this change: with Karine enabled the workbench
   masthead already overflows below ~417px — 417px of min-content in a 390px
   viewport, identical before and after (the 32px button is the same 32px
   either way; without any assistant control the bar measures 377px). That is a
   pre-existing defect of the narrow tier, it predates this control, and it is
   left alone here rather than folded into an unrelated change. It is worth
   fixing: the candidates are the 42px lockup (~149px of the 417px, and pinned
   to the kit's 148px minimum WIDTH for a horizontal lockup) and the ≤480px
   cluster. scripts/ui-baseline.py captures the workbench at sm=390 and records
   it as `derived.scrollW`, where the harness reports movement as information
   rather than failure — which is exactly how it has stayed unnoticed. */
@media (max-width: 600px) {
    .hs-mast-assist { width: var(--ctl); height: var(--ctl); }
    .hs-mast-assist-mark { width: 23px; height: 23px; }
}

.hs-karine,
.hs-karine-bubble {
    position: fixed;
    z-index: 1350;
}

/* Docks as four classes. Free drag would need pointer capture and persisted
   coordinates through JS interop, which the app layer does not get; four
   corners serve the actual need — get out of the way of what I am reading —
   and unlike a drag handle they are keyboard-reachable and assertable. */
.hs-karine--br { right: var(--s5); bottom: var(--s5); top: auto; left: auto; }
.hs-karine--bl { left: var(--s5); bottom: var(--s5); top: auto; right: auto; }
.hs-karine--tr { right: var(--s5); top: var(--s5); bottom: auto; left: auto; }
.hs-karine--tl { left: var(--s5); top: var(--s5); bottom: auto; right: auto; }

/* 370x520 was too small to hold a conversation — it fitted three canned chips
   and little else, which suited a menu and not a chat. 440 wide gives an answer
   room to be read without wrapping every clause, and a min-height keeps the
   panel from collapsing around a one-line empty state and then jumping to full
   size on the first reply. */
.hs-karine {
    display: flex; flex-direction: column;
    width: min(440px, calc(100vw - var(--s5) * 2));
    height: min(600px, calc(100vh - var(--s5) * 2));
    min-height: min(420px, calc(100vh - var(--s5) * 2));

    /* GLASS, and three things make the depth rather than one — a translucent
       plate, a real blur behind it, and an elevation that pairs a tight contact
       shadow with a wide ambient one. --e-4 also carries --glass-hi, the 1px
       inset highlight along the top edge, which is what stops the panel reading
       as a flat rectangle laid on the page.

       --surface-3, and NOT --paper, and not a local colour either. --surface-3 is
       already "the raised plane" in both themes — rgba(255,255,255,.86) light,
       rgba(22,32,76,.86) dark — so it steps UP from the canvas in each, which is
       the direction an elevated surface has to move. A local token would have had
       to reproduce that flip by hand and would drift the first time --surface-3
       moved.

       THICKENED, and the ratio is measured rather than chosen. --surface-3's own
       .86 is right for a card sitting INSIDE a page; this panel floats over a
       live claims grid and the aurora, so .86 passed enough of a raised dark-mode
       blob to drop the exposed --ink-3 text to 4.26:1 — under AA. That is the
       exact failure tokens.css's --surface-2 note records, in its mirror image: a
       BRIGHT blob lifts a DARK ground, so light ink loses in dark precisely where
       dark ink lost in light.

       Swept in a browser at 1440x900, both themes, both ends of the aurora's
       28-40s drift, sampling the composited pixel with the glyphs set transparent
       (worst node = .hs-karine-note, --ink-3 at 12px, straight on the plate):

           plate alpha   .860   .888   .899   .910   .923
           dark          4.26   4.38   4.45   4.52   4.57
           light         4.86   4.88   4.93   4.89   4.90

       .923 and not .910: .910 is the first value that technically passes, with
       0.02 of margin, and tokens.css:407 already names that mistake — "not a
       solved value but a value sitting on the line". .923 buys real headroom and
       is still visibly glass, because what reads as glass is the blur, not the
       last 8% of alpha.

       MIXED WITH --i-25, deliberately, and not with --paper. --i-25 is the canvas
       step: white in light, #0A1230 in dark. So the mix thickens the plate AND
       moves its colour further from the ink in each theme — whiter under dark ink,
       darker under light ink. Mixing with --paper instead was tried first and cost
       light 0.28 of ratio to buy dark 0.31, because light --paper is Mist, i.e.
       darker than the plate. Light must not be spent to reach dark.

       --e-4 up from --e-3: this is the only surface in the product that floats
       over the whole workbench rather than sitting inside a column, and at --e-3
       it read as pasted onto the grid rather than above it. */
    background: color-mix(in srgb, var(--surface-3) 55%, var(--i-25));
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--rule);
    border-radius: var(--r-lg);
    box-shadow: var(--e-4);
    overflow: hidden;

    /* Arriving, not appearing. The panel is summoned from somewhere else on the
       screen — the masthead mark, or the bubble in the corner — so a little
       travel is what ties the click to the thing that answers it.

       Runs ONCE, on open. The <section> lives inside @if (_open) in
       KarineWidget.razor, so Blazor creates the element at that moment and keeps
       it through every later render; a dock change swaps a class on the same
       element rather than making a new one, so the panel does not re-enter every
       time the operator moves it. */
    animation: hs-karine-in var(--d-slow) var(--ez-out) both;
}
@keyframes hs-karine-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

.hs-karine-head {
    display: flex; align-items: center; gap: var(--s2);
    padding: var(--s3) var(--s3) var(--s3) var(--s4);
    border-bottom: 1px solid var(--rule);
    background: var(--surface);
    flex: none;
}
.hs-karine-title {
    font-family: var(--f-disp); font-size: var(--t-sm);
    font-weight: var(--w-semi); color: var(--ink);
    margin-right: auto;
}

/* Answer language. Two text buttons rather than icons: EN and FR are already the
   shortest possible labels, and a flag or a globe would say less than the two
   letters do. Sits left of the docks because it changes what Karine SAYS, which
   matters more than where she sits. */
.hs-karine-langs { display: flex; gap: 2px; margin-right: var(--s2); }
.hs-karine-lang {
    font-family: var(--f-ui); font-size: var(--t-xs); font-weight: var(--w-med);
    color: var(--ink-3); background: transparent;
    border: 1px solid transparent; border-radius: var(--r-sm);
    padding: 1px var(--s1); cursor: pointer; line-height: 1.5;
}
.hs-karine-lang:hover { color: var(--ink-2); }
/* Styled from the same attribute a screen reader announces, so the two cannot
   drift apart the way a parallel "selected" class would. */
/* --brand-lo, not --brand, and this one was MEASURED failing.
   --brand on --brand-soft is Blue #2D6BFF on #EEF3FF: 4.06:1 in light and
   4.42:1 in dark (#4C82FF on #16204C), sampled off the running panel with the
   glyphs set transparent. Both are under AA for a 12px label, and neither was
   visible to any guard — the tokens are correct, the PAIRING was not.
   tokens.css:346 states the rule this restores: Blue clears AA on white or Mist
   with almost nothing to spare, so on any tinted ground step to --b-700. That
   is what --brand-lo already is (--b-800 light, --b-200 dark), and it is the
   token .hs-tab.active and .hs-appbar a:hover use for exactly this pairing. */
.hs-karine-lang[aria-pressed="true"] {
    color: var(--brand-lo); border-color: var(--brand-line); background: var(--brand-soft);
}

/* ARROWS, not dots, and the same 28px box as Minimise and Close.
   ─────────────────────────────────────────────────────────────────────────────
   Two identical 18px dots said "there are two of these" and nothing about which
   way either one moved her; the label was the only thing carrying the direction,
   and only to a screen reader. A chevron is the mark's own unit — the same 60°
   family, the same round caps and joins the book specifies for every icon
   (p31) — so the affordance is drawn from the system rather than imported.

   28px, not 18px: WCAG 2.2's target-size minimum is 24x24 CSS px, and an 18px
   button fails it outright. Sharing the box with .hs-karine-min and
   .hs-karine-close also gives the header one rhythm instead of three.

   Stroke, not fill: p31's terminals rule is round caps and joins, no filled
   shapes, so the glyph is a path with currentColor and the colour rules below
   move the whole control at once. */
.hs-karine-docks { display: flex; gap: 2px; }
.hs-karine-dock {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; flex: none; padding: 0;
    border: 1px solid transparent; border-radius: var(--r-sm);
    background: transparent; color: var(--ink-3); cursor: pointer;
    transition: background var(--d-fast) var(--ez-out),
                color var(--d-fast) var(--ez-out);
}
.hs-karine-dock:hover { background: var(--surface-2); color: var(--ink); }
.hs-karine-dock:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* aria-pressed carries the current dock, so the selected state is styled from
   the same attribute a screen reader announces rather than a parallel class
   that could drift out of step with it. --brand-lo for the same measured reason
   as the language button above: this is brand ink on a tinted ground. */
.hs-karine-dock[aria-pressed="true"] {
    color: var(--brand-lo); border-color: var(--brand-line); background: var(--brand-soft);
}

.hs-karine-close {
    display: grid; place-items: center;
    width: var(--ctl); height: var(--ctl); flex: none;
    border: none; background: transparent; color: var(--ink-2);
    border-radius: var(--r-sm); cursor: pointer;
}
.hs-karine-close:hover { background: var(--surface-2); color: var(--ink); }

/* STICKS TO THE BOTTOM AS THE ANSWER GROWS, without a line of JS.
   ─────────────────────────────────────────────────────────────────────────────
   The problem: an answer streams in over ~14 seconds and the panel stayed put,
   so the operator watched the top of a reply that was being written below the
   fold and had to scroll after every question. Streaming made it worse, not
   better — the text moves and the viewport does not follow.

   The obvious fix is scrollIntoView through JS interop, which the app layer of
   this repo does not get. This is the CSS answer, and it is not a hack so much
   as a different reading of which end is the start: with `column-reverse` the
   scroll container's origin is the BOTTOM, so when content grows the browser
   keeps that origin pinned and new text stays in view for free. The turns keep
   their normal order because they sit inside one child element, which is what
   gets reversed — not the list.

   It also does the right thing when the operator scrolls UP to re-read: because
   the anchor is the bottom rather than a computed offset, scrolling away holds
   position instead of being yanked back by the next delta. A JS
   implementation has to special-case that; this gets it by construction. */
.hs-karine-body {
    flex: 1 1 auto; overflow-y: auto;
    display: flex; flex-direction: column-reverse;
    padding: var(--s4);
    font-size: var(--t-sm); color: var(--ink);
}
/* The single child the reversal actually applies to. Its own children keep
   document order, so nothing is upside down. `margin-top: auto` keeps a short
   conversation at the TOP of the panel rather than floating it against the
   composer, which is what column-reverse would otherwise do. */
.hs-karine-body > * { margin-top: auto; }

/* Before the first token. Not a spinner: a spinner says "wait" and says nothing
   about what is being waited on, and Karine's pause is often a tool call rather
   than latency. */
.hs-karine-thinking {
    display: inline-flex; align-items: center; gap: var(--s1);
    color: var(--ink-3); font-size: var(--t-xs);
}
.hs-karine-dots { display: inline-flex; gap: 3px; }
.hs-karine-dots i {
    width: 4px; height: 4px; border-radius: 50%;
    background: var(--ink-3);
    animation: hs-karine-think 1.2s ease-in-out infinite;
}
.hs-karine-dots i:nth-child(2) { animation-delay: .15s; }
.hs-karine-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes hs-karine-think {
    0%, 60%, 100% { opacity: .25; }
    30%           { opacity: 1; }
}
/* Motion is the whole point of this indicator, so when motion is unwelcome the
   dots hold steady rather than disappearing — the row still reads as "working",
   which is the information it carries. */
@media (prefers-reduced-motion: reduce) {
    .hs-karine-dots i { animation: none; opacity: .6; }
}
/* --ink-2, not --ink-3. This sentence is the only thing on the panel before the
   first question, and it is an INSTRUCTION — "ask about a claim, an invoice or a
   pre-authorisation you are looking at" — not metadata. The dimmest step in the
   ramp was carrying the one line the panel most needs read.

   It also removes the tightest contrast node on the surface rather than paying
   for it: on the glass plate this measured 4.26:1 in dark as --ink-3, and 8.4:1
   as --ink-2. Both halves of that were fixed — the plate was thickened too — but
   this half is the one that is right on its own terms. */
.hs-karine-empty { color: var(--ink-2); font-size: var(--t-sm); margin: 0; }

.hs-karine-turns { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }

/* A turn carries a speaker now, so the two sides have to be tellable apart.
   The base rule holds everything both sides share; the modifiers below change
   only the surface, the border colour and which margin is indented. Anything
   selecting .hs-karine-turn keeps matching every turn, which is what the render
   tests select on.

   The indent is the load-bearing half, not the colour. --brand-soft against
   --surface-2 is a real difference in light mode and a much smaller one in dark,
   where both collapse towards the same near-black; an asymmetric margin survives
   the theme, and the visible speaker label in the markup survives having no
   colour vision or no screen at all. */
.hs-karine-turn {
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
    padding: var(--s2) var(--s3);
    /* THE ROOT CAUSE of the panel scrolling sideways when Karine answers with a
       table, and it is one property.

       A grid item's automatic minimum size is its content's MAX-CONTENT width,
       not zero. .hs-karine-turns is display:grid (:3804), so without this the
       turn refuses to shrink below the table's natural width and simply grows —
       measured at 431px for a 4-column French table and 550px for a 5-column
       one, against the 382px track it was given.

       Because the turn gave way first, the table was never squeezed, so
       .hs-karine-tablewrap's overflow-x:auto never had anything to scroll. The
       overflow escaped to .hs-karine-body instead, and since that box sets
       overflow-y:auto the spec computes its overflow-x to auto too — so the
       PANEL scrolled under its own pinned header and composer, which is exactly
       what the screenshots showed.

       This is the third time this repo has hit a grid/flex item refusing to
       shrink; .hs-panel-head at :1500 carries the same one-line fix and names
       the same cause. Fixing the class rather than this instance is why the
       first-column wrap above is paired with it. */
    min-width: 0;
}

/* The operator's own words: brand-tinted and indented from the left, the side a
   reader's eye already treats as "mine" in every chat surface they have used. */
.hs-karine-turn--operator {
    background: var(--brand-soft);
    border-color: var(--brand-line);
    margin-left: var(--s6);
}

/* Karine's reply: the quieter surface. Deliberately NOT brand-coloured — the
   brand tint reads as emphasis, and an assistant's answer should not shout over
   the claim data behind the panel. */
.hs-karine-turn--assistant {
    background: var(--surface-2);
    border-color: var(--rule);
    margin-right: var(--s6);
}

/* Who spoke. In the markup rather than only in the styling: a background colour
   is invisible to a screen reader, and this panel will be read aloud by one the
   moment docs/15 §8's voice work lands. */
.hs-karine-turn-who {
    display: block;
    font-size: var(--t-xs);
    font-weight: var(--w-med);
    color: var(--ink-3);
    margin-bottom: var(--s1);
}
.hs-karine-turn-text { display: block; }

/* An answer that is still being written (docs/15 §9).
   Deliberately NOT a different surface: it is the same reply, arriving a few
   seconds early, and re-styling it would make the panel appear to change its
   mind about what a turn looks like the moment the last token lands. The only
   difference is a caret, which is what tells a reader "still writing" apart
   from "wrote four words and stopped".

   Two selectors because there are two states worth marking. The first puts the
   caret inline after the last block the Markdown produced. The second covers
   the second or two before the model says anything at all, when the turn is
   genuinely empty — the widget emits that span on one line precisely so :empty
   can match it.

   No animation rule of its own is needed for reduced motion: the global
   prefers-reduced-motion block at the top of this file already flattens every
   animation-duration in the stylesheet. */
.hs-karine-turn--streaming .hs-karine-turn-text > :last-child::after,
.hs-karine-turn--streaming .hs-karine-turn-text:empty::after {
    content: "";
    display: inline-block;
    width: 2px;
    height: 1em;
    margin-left: 3px;
    vertical-align: text-bottom;
    background: var(--brand);
    animation: hs-karine-caret 1.1s step-end infinite;
}
@keyframes hs-karine-caret { 50% { opacity: 0; } }

/* The shell ships before the answering half, so the limit is stated in the
   interface. An operator who asks and hears nothing concludes it is broken. */
.hs-karine-note {
    margin: var(--s3) 0 0; color: var(--ink-3); font-size: var(--t-xs);
}

/* The composer. A row, not a wrap: the input takes the space and the send
   button stays a fixed square beside it, so the control does not reflow as the
   question grows. */
.hs-karine-foot {
    display: flex; align-items: center; gap: var(--s2);
    padding: var(--s3) var(--s3) var(--s3) var(--s4);
    border-top: 1px solid var(--rule);
    background: var(--surface);
    flex: none;
}
.hs-karine-input {
    flex: 1 1 auto; min-width: 0;
    font-family: var(--f-ui); font-size: var(--t-sm); color: var(--ink);
    background: var(--paper);
    border: 1px solid var(--rule-2);
    border-radius: var(--r-sm);
    padding: var(--s2) var(--s3);
}
.hs-karine-input:focus { border-color: var(--brand); outline: none; }
/* SOLID BLUE, deliberately not the Pulse gradient, and this is the panel's
   whole answer to two book rules that pull against each other.
   ─────────────────────────────────────────────────────────────────────────────
   p36 says "every screen has a single primary button in Pulse gradient".
   p23 defines Pulse as Blue -> Cyan. p24 measures white on Cyan at 1.9:1 and
   says outright that Cyan is a graphic colour, not a text colour. A 32px icon
   button with a white glyph cannot be a Pulse fill and be legible; the book
   contradicts itself at this size, and something has to give.

   What ships in tokens.css gives on the COLOUR: --g-pulse ends at #1A7CB8, our
   own darkened Blue, which is not one of the six palette colours. That is a
   defensible product decision and its measurement is recorded at the token — but
   it is not "the book's Pulse", and this panel does not need to spend a
   non-palette colour to have a primary.

   What this gives on the GRADIENT instead. The book's own accessibility page
   lists "Blue surface · white text" as a SAFE COMBINATION at 4.6:1, and --brand
   IS the book's Blue (#2D6BFF). So the primary is a solid palette Blue, carrying
   --on-brand — the token that flips white/Midnight per theme because white is
   4.51:1 on light --brand and 3.53:1 on the lifted dark one. It was taking
   --paper, which is Mist in light and near-black in dark: right by accident in
   one theme and unmeasured in the other.

   Two further things fall out of it, and both are why this is the right trade:
     · the workbench already spends its one Pulse on .hs-btn-primary (:1116), so
       a Pulse here would have been the SECOND on screen — the same argument
       .hs-karine-bubble already makes for not being a Pulse disc; and
     · it leaves Signal as the only named gradient anywhere on this panel, which
       is what keeps p23's "never overlay two gradients on the same surface"
       true of the surface as a whole rather than only of each pixel. */
.hs-karine-send {
    display: grid; place-items: center; flex: none;
    width: var(--ctl); height: var(--ctl);
    border: none; border-radius: var(--r-sm);
    background: var(--brand); color: var(--on-brand);
    cursor: pointer;
    box-shadow: var(--e-1);
    transition: filter var(--d-fast) var(--ez-out);
}
.hs-karine-send:hover:not(:disabled) { filter: brightness(1.06); }
.hs-karine-send:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Disabled rather than hidden: a send button that vanishes on an empty box
   moves the composer's layout every time the field is cleared. */
.hs-karine-send:disabled {
    background: var(--surface-3); color: var(--ink-3); cursor: default;
}

/* Suggestions, shown only in the empty state — see KarineWidget.razor for why
   they are not a permanent footer row. */
.hs-karine-chips {
    display: flex; flex-wrap: wrap; gap: var(--s2);
    margin-top: var(--s4);
}
.hs-karine-chip {
    font-family: var(--f-ui); font-size: var(--t-xs); color: var(--ink-2);
    background: var(--paper);
    border: 1px solid var(--rule-2);
    border-radius: var(--r-sm);
    padding: var(--s1) var(--s2);
    cursor: pointer;
    transition: border-color var(--d-fast) var(--ez-out),
                color var(--d-fast) var(--ez-out);
}
.hs-karine-chip:hover { border-color: var(--brand); color: var(--ink); }

/* Resting state: a button, not a div. It opens a dialog, so it must be
   focusable and Enter-activated without a role bolted on afterwards. */
.hs-karine-bubble {
    display: grid; place-items: center;
    width: 56px; height: 56px; padding: 0;
    /* GLASS, not a solid brand disc. The bubble used to be a filled --brand
       circle, which fought the full-colour mark now sitting on it — a gradient
       symbol on a flat blue ground reads as a sticker rather than a control, and
       it was spending a second brand-primary in the corner of every screen.

       Same glass recipe as .hs-loading-overlay and .hs-reconnect-card, so the
       three floating surfaces are one material. The mark supplies the colour;
       the disc only has to hold it. */
    background: color-mix(in srgb, var(--paper) 66%, transparent);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--rule);
    border-radius: 50%;
    box-shadow: var(--e-3);
    cursor: pointer;
    transition: box-shadow var(--d-fast) var(--ez-out),
                background var(--d-fast) var(--ez-out);
}
.hs-karine-bubble:hover { box-shadow: var(--e-3); }
/* Was a bold "K" in display type; now the symbol itself. The font declarations
   went with the letter — an <img> ignores them, and leaving them would be four
   rules that look load-bearing and do nothing. 40px inside the 56px bubble is
   the kit's 72% symbol-to-tile ratio, and both axes are pinned so the bubble
   does not reflow when the SVG decodes. */
.hs-karine-bubble-mark { width: 40px; height: 40px; display: block; }

/* Below this the four docks stop meaning anything — the panel is nearly the
   full viewport either way — so it settles to the bottom and spans the width,
   which is also where a thumb reaches on a phone. */
@media (max-width: 560px) {
    .hs-karine {
        left: var(--s3); right: var(--s3);
        bottom: var(--s3); top: auto;
        width: auto; max-height: 70vh;
    }
    .hs-karine-docks { display: none; }
}


/* ── Karine panel · 2026-08-21 product-owner pass ───────────────────────────── */

/* The mark in the header, before the name. Both axes pinned for the same reason
   as every other mark in this rebrand: an SVG with one axis auto reflows its row
   the moment the file decodes. */
.hs-karine-mark { width: 18px; height: 18px; flex: none; }

/* Minimise sits beside Close and must read as its equal, so it inherits the
   close button's box rather than inventing a second one. Only the glyph differs.
   Declared AFTER .hs-karine-close in the file, so where the two overlap this
   wins — which is what is wanted, because it is the same box by intent. */
.hs-karine-min {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; flex: none;
    border: 0; border-radius: var(--r-sm);
    background: transparent; color: var(--ink-3);
    cursor: pointer;
}
.hs-karine-min:hover { background: var(--surface-2); color: var(--ink); }
.hs-karine-min:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* THE CROP — device 03, p30. The panel's ONE device.
   ─────────────────────────────────────────────────────────────────────────────
   The book sanctions exactly three devices, all cut from the mark: THE FIELD
   (chevron pairs on a 60° lattice), THE FLOW (many lines resolving into one) and
   THE CROP — "a single oversized mark bled off two edges", "never crop past the
   centre point". What stood here was none of them: a 280px mark, fully CONTAINED
   inside a 440px panel and centred on both axes. A contained centred mark is a
   watermark, which is a thing the book does not have, and .hs-aurora-mark's own
   note already records the second cost of centring one — it reads as a stalled
   loading state on a surface that has finished loading.

   THE GEOMETRY IS THE RULE, written so it can be checked rather than trusted:
     420px mark, right: -150px, bottom: -150px
     -> it bleeds off the RIGHT and BOTTOM edges: two edges, as specified
     -> its centre sits 420/2 - 150 = 60px inside each of those edges, so the
        point of convergence — the open centre the mark is built around — is
        still on the surface. That is what "never crop past the centre point"
        means, and 60px of margin is what makes it survive the narrow tier,
        where the panel drops to 366px wide at a 390px viewport and the centre
        is still 60px in.

   OVERSIZED against the panel it sits on, not against a fixed idea of big: 420
   into a 440x600 panel is 95% of its width before the bleed, and 270px of the
   mark is visible out of 420.

   ONE DEVICE PER SURFACE, and the panel being GLASS is what makes that worth
   proving rather than asserting. The workbench behind it carries its own bled
   mark (.hs-aurora-mark) plus three aurora blobs, and a transparent panel would
   have shown a second device through the first.

   Measured by compositing the panel twice, stage device on and off, and diffing
   every pixel inside its 440x600 rect (1440x900, both themes, aurora parked):

       worst single-pixel channel delta      light  4/255      dark 10/255
       pixels moving more than 2/255         light  58         dark 50
       weighted ink of the difference        light  0.05%      dark 0.03%

   An earlier draft of this note claimed "under one 8-bit step", which the dark
   figure does not support — it is 10/255 at its single worst pixel. The claim
   that survives measurement is the weaker and sufficient one: fewer than 60
   pixels of 264,000 move visibly, so the stage's mark is diffused into ground
   colour rather than read as a figure. That is what a blur is for, and this crop
   is the only device on the surface.

   Still expressed against --wm-opacity so the panel tracks the one global dial
   rather than drifting from every other surface — but 3.2x rather than 0.84x,
   because the multiplier is no longer describing the same object. 0.84 sized a
   WATERMARK, whose whole job is to stay under the text; this is the signature
   device, and at 0.84 it rendered as the smudge the product-owner note on the
   old mark was itself complaining about. It lands at .144 light and .192 dark —
   dark higher for the reason --wm-opacity's own note gives, that the mark's
   blues separate less from Midnight than from Mist.

   BOUNDED BY TWO MEASUREMENTS, not by taste:
     · CONTRAST. Swept x1.6 / x2.4 / x3.2 / x4.0 / x5.0 against every text node
       in the lower half of the panel, both themes. The worst node does not move
       at all — 4.92:1 dark, 5.06:1 light at every step — because each of those
       nodes sits on its own surface (a turn plate, the composer band) and the
       device is behind all of them. The only figure that moves is the answer
       body, 16.91:1 -> 16.69:1 at x3.2.
     · THE 6% RULE (p21): "accent colour and gradient together should never
       exceed roughly 6% of any surface". Counted as INK rather than as bounding
       boxes, which is the only count that means anything for a crop whose box is
       mostly empty corners: render the panel with every accent layer on, render
       it again with the Signal edge, both keylines, the crop and the Blue primary
       switched off, and weight each changed pixel by how far it moved.

           panel accent + gradient, weighted    light 1.29%    dark 1.32%
           (of 264,000 px)

       An earlier draft of this note put it at 2.31% / 2.68% from a hand count of
       the mark's ink; the rendered figure is roughly half that, because the crop
       is antialiased strokes at .144/.192 rather than solid area. Either way it
       clears the cap, and x5.0 on --wm-opacity still would; x3.2 is where the
       device reads as deliberate without competing with the claim figures, which
       is p37's instruction for anything inside the product.

   THE BOX IS THE PANEL; ONLY THE IMAGE IS OVERSIZED, and that is not cosmetic.
   Written the obvious way — a 420px element at right:-150px — the bleed is real
   overflow, and this panel measured scrollWidth 588 against clientWidth 438
   because of it. It is invisible today only because .hs-karine sets
   overflow:hidden; the moment anyone gives this box a scrolling overflow it
   becomes 150px of sideways scroll into a decoration, which is the exact defect
   class .hs-karine-turn's min-width:0 note is about. Sizing and placing the
   BACKGROUND instead paints the identical pixels and contributes no overflow at
   all: `background-position: right -150px bottom -150px` puts the image's right
   and bottom edges 150px outside the box the four-value syntax measures from. */
.hs-karine::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background-image: url('../img/hsl-icon.svg');
    background-repeat: no-repeat;
    background-size: 420px 420px;
    background-position: right -150px bottom -150px;
    opacity: calc(var(--wm-opacity) * 3.2);
    z-index: 0;
}

/* THE SIGNAL RULE — p35's letterhead move at panel scale.
   The book's own stationery is "4 pt Signal rule bleeding the full top edge",
   and this is that, at 3px on a 440px panel. Gradient as STRUCTURE rather than
   as a fill, which is the whole licence for using it on a surface that sits over
   claim data: it carries the brand at rule scale and tints nothing.

   z-index 2, above the header band at 1: the rule bleeds the panel's full width
   and must not be interrupted by the chrome sitting under it. Clipped to the
   rounded corners by .hs-karine's own overflow:hidden, so it reads as part of
   the panel's edge rather than a bar laid across it. */
.hs-karine::before {
    content: ""; position: absolute; pointer-events: none;
    left: 0; right: 0; top: 0; height: 3px;
    background: var(--g-signal);
    z-index: 2;
}
/* The transcript and the composer sit above the crop. .hs-karine::after paints
   after its static siblings, so without this the device would land ON the
   conversation rather than behind it. */
.hs-karine-body, .hs-karine-foot, .hs-karine-head { position: relative; z-index: 1; }

/* Bringing the turns to life, within the book rather than around it.

   The operator's turn keeps the brand tint and gains a left keyline; Karine's
   keeps the quiet surface and gains one in Signal. That is the gradient rule
   from p35 used at the scale it is specified for — a rule, not a fill — so it
   adds identity without spending the ONE Pulse primary the screen is allowed,
   and without tinting a surface that sits over claim data.

   2px, and vertical: the book's own page furniture is a 4pt rule at roughly 19:1,
   and this is the same device stood on its end at panel scale. */
.hs-karine-turn { position: relative; overflow: hidden; }
.hs-karine-turn--assistant::before {
    content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
    background: var(--g-signal);
}
.hs-karine-turn--operator::before {
    content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
    background: var(--brand);
}
/* The keyline needs room it did not previously need. */
.hs-karine-turn { padding-left: var(--s4); }

/* The speaker label, quieter and tighter — it is a label, not a heading, and at
   full weight it competed with the answer beneath it.

   --ink-2 AND NOT --ink-3 (hirms.css:4653), and this one was measured rather than
   reasoned. It is the only --ink-3 node on the panel that sits on the OPERATOR
   turn, whose plate is --brand-soft rather than the glass — a tinted ground, and
   the tightest one on the surface in both themes. Sampled off the composited
   pixel at 1440x900 with the glyphs set transparent, mid-drift on the aurora:

       .hs-karine-turn-who, operator turn      --ink-3      --ink-2
       light, ground #EEF3FF                    4.84:1       5.30:1
       dark,  ground #16204C                    4.54:1       7.36:1

   Both --ink-3 figures technically pass. 4.54:1 is 0.04 above the line, which is
   the exact shape tokens.css:407 names — "not a solved value but a value sitting
   on the line" — and it is one step of a SHARED ink ramp away from failing for a
   reason that has nothing to do with this panel. --ink-2 is the same move
   .hs-karine-empty above already made, on the same surface, for the same measured
   reason.

   Both speakers take it, not only the operator. Splitting the token by side would
   hold the RATIO even and let the two labels drift apart visually, on adjacent
   rows, where they are read as a pair. */
.hs-karine-turn-who {
    font-size: var(--t-2xs); letter-spacing: .06em; text-transform: uppercase;
    color: var(--ink-2); font-weight: var(--w-med);
}

/* ── Karine · Option B, the shipping pass · 2026-08-22 ─────────────────────── */

/* A NEW TURN RISES INTO PLACE.
   8px and a fade over --d-slow, on the same curve as the panel itself. The
   reason it is worth having at all: this transcript is column-reverse and pinned
   to the bottom (see .hs-karine-body), so a new turn does not push the view — it
   simply IS there on the next frame, and without motion an operator re-reading
   an earlier answer gets no signal that anything arrived.

   It animates ONCE per element, which is the property that makes it safe on a
   streaming panel. The @foreach in KarineWidget.razor is keyless, so Blazor
   diffs by position: appending a turn creates one new <li> at the end and reuses
   every earlier one, and finishing a stream swaps the partial <li>'s content
   rather than replacing the node. Neither restarts an animation on a turn that
   is already on screen. */
.hs-karine-turn { animation: hs-karine-turn-in var(--d-slow) var(--ez-out) both; }
@keyframes hs-karine-turn-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* THE THINKING STATE, and the keyline is the craft in it.
   ─────────────────────────────────────────────────────────────────────────────
   Karine's pause is usually a tool call, and it can run well past the two
   seconds a first token normally takes. Three static dots say "wait" and nothing
   else, so the Signal keyline every answer already carries TRAVELS while she is
   working and settles the instant the first token lands — the same device
   brought to life, rather than a second one invented for the occasion.

   background-size: 100% 300% then sliding background-position is what moves it:
   the gradient is drawn three times the element's height and scrolled through
   the 2px window, so the stops travel and nothing about the element's geometry
   changes. No transform, no layout, no new element.

   The class is on the turn only while _partial is empty (KarineWidget.razor),
   which is precisely "working, nothing written yet". Once a delta lands the class
   goes and the caret takes over the job of saying "still writing" — two states,
   two signals, neither doing the other's work. */
.hs-karine-turn--thinking::before {
    background: var(--g-signal);
    background-size: 100% 300%;
    animation: hs-karine-travel 2.6s var(--ez-io) infinite;
}
@keyframes hs-karine-travel {
    from { background-position: 0 100%; }
    to   { background-position: 0 -100%; }
}

/* REDUCED MOTION, AT A SPECIFICITY THAT CAN ACTUALLY WIN.
   ─────────────────────────────────────────────────────────────────────────────
   A @media block contributes NOTHING to specificity — that is the trap, and this
   repo has already sprung it once (see the note on .hs-mast-assist at the
   prefers-reduced-motion block above, and MastheadAssistantControlTests, which
   exists because of it). Every selector here therefore matches its base rule
   exactly and sits later in the file, so source order decides:

       .hs-karine                    (0,1,0)  vs  (0,1,0)
       .hs-karine-turn               (0,1,0)  vs  (0,1,0)
       .hs-karine-turn--thinking::before  (0,2,1)  vs  (0,2,1)

   `animation: none` and not the global !important animation-duration collapse at
   the top of this file: that collapse kills the DURATION, which would leave the
   travelling keyline restarting a loop every hundredth of a millisecond, forever.
   Killing animation-NAME is what actually stops it.

   The two arrivals are dropped outright — an entrance carries no information once
   it has happened. The thinking keyline HOLDS instead, parked mid-gradient: its
   motion is the message, so removing it would remove the fact that Karine is
   working, and the row would read as an empty answer. Same bargain
   .hs-karine-dots already makes above, for the same reason. */
@media (prefers-reduced-motion: reduce) {
    .hs-karine { animation: none; }
    .hs-karine-turn { animation: none; }
    .hs-karine-turn--thinking::before { animation: none; background-position: 0 50%; }
}

/* ── Karine · full page · 2026-08-22 ────────────────────────────────────────── */

/* The expand toggle. The dock buttons' box and colour rules exactly — 28px for
   WCAG 2.2 target size, --brand-lo on --brand-soft for the measured AA reason
   .hs-karine-lang records — because it lives in the same header row and states
   the same kind of fact (where the panel is) through the same attribute
   (aria-pressed). A third box shape in one header would be a third rhythm. */
.hs-karine-expand {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; flex: none; padding: 0;
    border: 1px solid transparent; border-radius: var(--r-sm);
    background: transparent; color: var(--ink-3); cursor: pointer;
    transition: background var(--d-fast) var(--ez-out),
                color var(--d-fast) var(--ez-out);
}
.hs-karine-expand:hover { background: var(--surface-2); color: var(--ink); }
.hs-karine-expand:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.hs-karine-expand[aria-pressed="true"] {
    color: var(--brand-lo); border-color: var(--brand-line); background: var(--brand-soft);
}

/* FULL PAGE: the third geometry. Same element, same chrome, same z-index —
   KarineWidget.razor swaps this class in INSTEAD of a dock class (PanelClass),
   so the two never fight over the inset properties.

   top clears the masthead rather than covering it: the bar is 66px (12px
   padding + 42px .hs-mast-logo + 12px — the arithmetic .hs-mast-assist's note
   already cites) + 1px border, then one --s3 of breathing room. Keep the 42px
   in step with .hs-mast-logo's height, same maintenance note the boot logo
   carries. The masthead is sticky at the viewport top, so a fixed panel
   measured from the viewport lands under it at every scroll position.

   z-index stays the widget's 1350: the full page must still lose to a
   HsConfirmDialog at 1401 and the loading scrim at 2000 for exactly the
   reasons the widget's header comment measures out.

   max-height: none is not decoration — the 560px tier above caps .hs-karine at
   70vh at the SAME specificity, and only source order lets full-page win it.

   NO transition between the geometries, considered and declined rather than
   forgotten: the dock classes pin the unused edges to `auto`, and auto does not
   interpolate, so a transition would slide two edges and jump the other two —
   worse than the clean cut. Nothing new to hold back under
   prefers-reduced-motion because nothing new moves.

   --karine-measure is a px length, not ch, because two boxes consume it — the
   transcript (at --t-sm) and the composer band (at the root size) — and a
   font-relative unit would resolve differently in each, leaving the input
   visibly off-grid from the turns it feeds. 620px is ~72 characters of the
   transcript's own --t-sm. */
.hs-karine--full {
    --karine-measure: 620px;
    left: var(--s5); right: var(--s5); bottom: var(--s5);
    top: calc(42px + 2 * var(--s3) + 1px + var(--s3));
    width: auto; height: auto; min-height: 0; max-height: none;
}

/* A readable measure, centred. On the BODY'S CHILDREN — the turn list, the
   empty state, the cannot-answer note — never on the body itself: the body is
   the scroll container and the glass, and narrowing it would float a strip of
   panel over bare workbench. margin-top: auto from the base > * rule still
   applies (this only sets the inline axis), so a short conversation stays at
   the top exactly as it does in the panel. */
.hs-karine--full .hs-karine-body > * {
    width: 100%; max-width: var(--karine-measure); margin-inline: auto;
}

/* The composer band spans the full width — it is the panel's floor and the
   border-top must run edge to edge — while its CONTENT aligns to the same
   measure as the transcript above it. Padding does that without new markup:
   half the leftover space each side, never less than the panel-mode inset. */
.hs-karine--full .hs-karine-foot {
    padding-inline: max(var(--s4), calc(50% - var(--karine-measure) / 2));
}

/* THE CROP, rescaled — the SAME device, not a second one (p30: one device per
   surface). The panel's 420px mark reads right on a 440px panel and reads as a
   corner sticker on a full workbench, so the mark scales with its surface:
   640px, bled 220px off the same two edges. The centre point sits
   640/2 − 220 = 100px inside each bled edge, so the convergence stays on the
   surface — the same "never crop past the centre point" arithmetic the panel
   rule proves, at the new size. Opacity is inherited from .hs-karine::after
   untouched: the dial (--wm-opacity × 3.2) describes the device, not the
   geometry it is drawn at. */
.hs-karine--full::after {
    background-size: 640px 640px;
    background-position: right -220px bottom -220px;
}

/* ── Kiosk capture · busy affordance · 2026-08-22 ────────────────────────────
   A disabled button says "you may not"; a kiosk under time pressure needs
   "I am working". .hs-btn-busy appends a pulsing ellipsis to the ACTOR button
   only — the one whose handler set the flag — reusing hs-loading-pulse so
   waiting looks the same here as on the boot loader. Cancel/Back stay plain
   disabled: dots on a button nobody pressed would claim work that is not
   happening. For the same reason the completion trio (scan / OTP / skip, all
   visible at once sharing _completionBusy) carries NO dots — one flag cannot
   say which of three was pressed, and guessing would lie to the operator.

   inline-block because the keyframes scale — a transform on inline content is
   a no-op. Reduced motion is covered by the global rule at the top of this
   file (every ::before and ::after, animation-duration .01ms): the dots freeze faint
   but present, which still reads as "held", not "broken". */
.hs-btn-busy::after {
    content: '···';
    display: inline-block;
    margin-inline-start: var(--s2);
    letter-spacing: .12em;
    animation: hs-loading-pulse 1.1s var(--ez-io) infinite;
}

/* ── /onboard · tabbed progressive application · 2026-08-23 ──────────────────
   The public Join-HIRMS form went from three stacked cards to one card with
   three tabs (product decision 2026-08-23: one page, progressive, no long
   scroll). Everything here rides the existing .hs-tabs/.hs-tab family — these
   rules add only what that family does not already say. Appended section;
   touches no existing rule. */

/* The language switch sits top-right, above the lockup — the one control an
   anonymous applicant needs before reading anything else. Flex end, not
   absolute: at 390px it simply leads the column instead of overlapping it. */
.hs-onboard-lang { display: flex; justify-content: flex-end; padding-top: var(--s4); }

/* p35's letterhead move at card scale: the 4pt Signal rule bleeding the full
   top edge, here 3px via the token — gradient as STRUCTURE, not fill (the
   page's one convergence device stays the Aurora behind it). overflow:hidden
   clips the strip to the card's rounded corners so it reads as the card's own
   edge, exactly the .hs-karine::before construction. */
.hs-onboard-card { position: relative; overflow: hidden; }
.hs-onboard-card::before {
    content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px;
    background: var(--g-signal);
}

/* .hs-tabs already scrolls sideways (overflow-x: auto) and .hs-tab already
   refuses to wrap — that pair is what keeps the bar usable at 390px without
   the PAGE ever scrolling sideways. Nothing to add for mobile. */

/* A locked tab must look locked, not merely ignore clicks: the disabled
   attribute kills the pointer, this kills the invitation. */
.hs-tab:disabled { color: var(--ink-3); cursor: not-allowed; }
.hs-tab:disabled:hover { color: var(--ink-3); }

/* Step state, drawn stroke style (p31): an open ring — border, no fill — with
   the step number, or the drawn-check SVG once the section is valid. Round
   caps live on the SVG strokes; the ring is a border for the same "no filled
   shapes" reason. currentColor throughout so active/locked tabs recolour the
   chip with the label for free. */
.hs-step-chip {
    display: inline-flex; align-items: center; justify-content: center;
    inline-size: 20px; block-size: 20px; flex: none;
    border: 1.5px solid currentColor; border-radius: var(--r-full);
    font-size: var(--t-2xs); font-weight: var(--w-semi);
    font-variant-numeric: tabular-nums; background: none;
}
.hs-step-chip-svg { display: block; flex: none; }

/* Back left, Continue/Submit right — the reading order of a step. */
.hs-onboard-actions {
    display: flex; justify-content: space-between; align-items: center;
    gap: var(--s3); margin-top: var(--s5);
}

/* ── 2026-08-23 · Facility-queue rows → per-visit detail ─────────────────────
   The kiosk queue's rows became clickable surfaces (FacilityQueueRows). The
   hover wash and the focus ring are already global (.hs-table tbody tr:hover
   and *:focus-visible) — the only thing a clickable row still lacks is the
   invitation. Tokens only; both themes get it for free via --surface-2. */
.hs-queue-row { cursor: pointer; }

/* ── /welcome · overlaid language pill · 2026-08-23 ──────────────────────────────────
   Top-right over the LIGHT column (right: it can never sit on the dark rail,
   whose reversed ink the pill's colours are not drawn for). Replaces a flow
   block that pushed the whole split down — the white-band defect. */
.hs-welcome-lang { position: absolute; top: var(--s3); right: var(--s4); z-index: 5; }

/* ── Loader mark spin · 2026-08-23 (owner-approved plan) ─────────────────────
   The mark rotates ONLY inside loading contexts — the boot overlay and
   LoadingOverlayHost — where motion IS the information (working, not frozen).
   The masthead/watermark instances of the mark never spin: outside a wait
   state a spinning logo is decoration, which p29 forbids. 2.4s linear — slow
   and dignified; fast spins read as anxiety, and the six-chevron geometry
   reads best as a smooth continuous turn. transform-origin center so the
   convergence point stays put. Reduced motion: the global rule at the top of
   this file zeroes the duration — those users keep the static mark. */
@keyframes hs-mark-spin { to { transform: rotate(360deg); } }
.hs-loading-overlay .hs-loading-mark,
.hs-boot .hs-loading-mark,
.hs-boot .hs-boot-logo {
    animation: hs-mark-spin 2.4s linear infinite;
    transform-origin: center;
    will-change: transform;
}

/* ── /audit · realtime trail, entity links, payload diff · 2026-08-23 ────────
   The audit page grew a live tail (AuditFeedWorker → AuditHub), an actor chip,
   clickable entity references and an expandable payload with a before/after
   diff. Tokens only; both themes come free of the tokens. The filter bar is
   the standard .hs-filters/.hs-filter-field kit (untouched); the diff table
   scrolls inside its own wrap so the page body never scrolls sideways, and at
   390px the flex-wrap on .hs-filters stacks the bar on its own. */

/* The live toggle row between the filter card and the grid. */
.hs-audit-livebar { display: flex; align-items: center; gap: var(--s3); margin: var(--s3) 0; flex-wrap: wrap; }

/* "N new" — arrivals buffered while the tail is frozen. A button, because
   clicking it is the unfreeze. */
.hs-audit-newchip {
    background: var(--brand-soft); color: var(--brand-lo);
    border: 1px solid var(--rule); border-radius: var(--r-full);
    padding: 2px 10px; cursor: pointer;
    font-family: var(--f-ui); font-size: var(--t-2xs); font-weight: var(--w-semi);
}
.hs-audit-newchip:hover { filter: brightness(1.05); }

/* Who did it — a quiet chip, not a status pill: an actor is identity, and
   borrowing the status palette would make a name read as a verdict. */
.hs-audit-actor {
    display: inline-block; background: var(--surface-3); border-radius: var(--r-full);
    padding: 1px 8px; font-size: var(--t-2xs); white-space: nowrap;
}

.hs-audit-entitylink { color: var(--brand); text-decoration: none; }
.hs-audit-entitylink:hover { text-decoration: underline; }

/* A freshly arrived row announces itself once and settles. The brand wash
   fades rather than snapping — and the reduced-motion rule at the top of this
   file zeroes the duration for users who asked for stillness. The .is-live
   tick keeps marking the row after the wash has gone, so "which rows arrived
   while I watched" survives the animation. */
@keyframes hs-audit-arrive { from { background: var(--brand-soft); } to { background: transparent; } }
.hs-table tbody tr.hs-row-live { animation: hs-audit-arrive 1.6s var(--ez-out); }
.hs-tick.is-live { background: var(--brand); opacity: 1; }

/* The payload expander. Native <details>, styled to sit inside a grid cell
   without inflating the closed row. */
.hs-audit-expand summary {
    cursor: pointer; color: var(--ink-2); font-size: var(--t-sm);
    list-style-position: inside; white-space: nowrap;
}
.hs-audit-expand[open] summary { color: var(--brand-lo); margin-bottom: var(--s2); }
.hs-audit-expand .hs-pre { max-width: 520px; }

/* Before/after diff — label | before | after. Wide payloads ride their own
   horizontal scroll (same rule as every wide table: the wrap scrolls, the
   page does not). Changed rows get the brand wash; the unchanged context
   rows around them stay flat so the change is the thing the eye lands on. */
.hs-audit-diff-wrap { overflow-x: auto; max-width: 560px; }
.hs-audit-diff { border-collapse: collapse; min-width: 320px; font-size: var(--t-sm); }
.hs-audit-diff th {
    text-align: left; font-size: var(--t-2xs); text-transform: uppercase;
    letter-spacing: .06em; color: var(--ink-3); font-weight: var(--w-semi);
    padding: var(--s1) var(--s3) var(--s1) 0; border-bottom: 1px solid var(--rule);
}
.hs-audit-diff td { padding: var(--s1) var(--s3) var(--s1) 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
.hs-audit-diff td.mono { white-space: pre-wrap; }
.hs-audit-diff tr.changed td { background: var(--brand-soft); }

/* -- Invoice panel folds - 2026-08-23 --------------------------------------
   Native details/summary for the two lists reviewers said made the panel a
   scroll marathon (observations, lines). The summary is the one-line tab;
   the default disclosure marker is hidden and a caret glyph turns when
   open. A fold holding a BLOCKING observation arrives open from the markup:
   the collapse must never hide the thing that stops approval. */
.hs-fold > .hs-fold-head {
    display: flex; align-items: center; gap: var(--s3);
    cursor: pointer; list-style: none; min-height: 40px;
    font-weight: var(--w-semi);
}
.hs-fold > .hs-fold-head::-webkit-details-marker { display: none; }
.hs-fold > .hs-fold-head::before {
    content: "A"; display: inline-block; font-weight: var(--w-bold);
    color: var(--ink-3); transform: rotate(0deg);
    transition: transform var(--d-fast) var(--ez-out);
}
.hs-fold[open] > .hs-fold-head::before { transform: rotate(90deg); }
.hs-fold[open] > .hs-fold-head { margin-bottom: var(--s3); }
.hs-fold-alert {
    color: var(--st-rejt); background: var(--st-rejt-bg);
    border-radius: var(--r-full); padding: 1px 10px;
    font-size: var(--t-xs); font-weight: var(--w-semi); flex: none;
}

/* ── Notifications centre (owner 2026-08-30) ───────────────────────────────
   Stacked cards, phone-first: rows wrap instead of columniating, and the
   attachment viewer opens inline full-width — the same reading experience as
   the invoice review's evidence panel, on any width. */
.hs-notif-center { max-width: 860px; }
.hs-notif-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.hs-notif-item {
  border: 1px solid var(--rule); border-radius: var(--r-md);
  background: var(--paper); padding: var(--s3);
}
.hs-notif-item.unread { border-left: 3px solid var(--brand); background: var(--surface-2); }
.hs-notif-row { display: flex; gap: var(--s3); align-items: flex-start; cursor: pointer; flex-wrap: wrap; }
.hs-notif-body { min-width: 0; flex: 1 1 240px; }
.hs-notif-subject { font-weight: 600; overflow-wrap: anywhere; }
.hs-notif-meta { font-size: 12px; margin-top: 2px; }
/* F7: collapsed rows preview like mail — two lines, then ellipsis; the full
   text lives in the expanded panel below, never lost, never truncated. */
.hs-notif-preview {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.hs-notif-item.open { border-color: var(--brand); }
.hs-notif-full { margin-top: var(--s2); white-space: pre-wrap; overflow-wrap: anywhere; }
.hs-notif-actions { margin-top: var(--s2); }
.hs-notif-attachments { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s2); }
.hs-notif-viewer { margin-top: var(--s3); }
/* M-1: the DocumentViewer's frame is height:100% for the review pane; inside
   the auto-height notification row that is zero. Give it a viewport share. */
.hs-notif-viewer .hs-docview-frame { height: 70vh; min-height: 320px; }
.hs-notif-viewer .hs-docview-image { max-height: 70vh; }
.hs-bell-foot-viewall { display: block; padding: var(--s2) var(--s3); text-align: center; }

/* ── B2 · the plate card (MemberPlateCard) ───────────────────────────────────
   Extends .hs-membercard--phone with the personalisation-plate grid and the
   flip back. The card is a button (tap to flip); the outline is the focus
   affordance, not decoration. */
.hs-platecard { cursor: pointer; user-select: none; }
.hs-platecard:focus-visible { outline: 2px solid var(--on-card); outline-offset: 2px; }
.hs-platecard-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2) var(--s4);
    margin-top: var(--s3); font-size: var(--t-xs);
}
.hs-platecard-grid small {
    display: block; font-family: var(--f-mono); font-size: 8px;
    letter-spacing: 1.2px; opacity: .72;
}
.hs-platecard-grid b { font-weight: var(--w-semi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.hs-platecard-back { position: relative; font-size: var(--t-xs); line-height: 1.55; }
.hs-platecard-back p { margin: var(--s2) 0 0; opacity: .95; }
.hs-platecard-contacts { opacity: .9; }
.hs-platecard-caveat { font-size: 10px; opacity: .75; font-style: italic; }

/* A24 / J3: the "End session" chip in the same corner, on every kiosk and
   member shell (WASM and native). Sized like the language chips beside it —
   an occasional control, not a gloved-thumb target — and three selectors
   deep so it outranks .hs-app .hs-chip's 44px floor (both are two classes;
   that rule comes later in the file and would otherwise win). Blue ink on
   the white chip only, no gradient, and a WORD carries the action: the
   brand rules leave nothing else to say here. */
.hs-app .hs-corner-cluster .hs-chip-end {
    min-height: 30px; padding: 0 10px; font-size: var(--t-2xs);
    color: var(--brand); border-color: var(--rule); white-space: nowrap;
}
.hs-app .hs-corner-cluster .hs-chip-end:hover { border-color: var(--brand); background: var(--brand-soft); }

/* The idle warning's countdown (SessionIdleGuard) sits inside HsConfirmDialog's
   body, where the Done-stage's centred 280px track would float oddly — let it
   run the width of the dialog text instead. */
.hs-confirm .hs-countdown { margin: var(--s3) 0 0; max-width: none; }

/* The member's card photo on the admin panel (membership UAT report,
   observation 3). Fixed box so a portrait and a landscape snapshot leave the
   panel the same height, object-fit:cover so neither is distorted to fit. */
.hs-member-photo {
    display: block; width: 128px; height: 128px;
    object-fit: cover; border-radius: var(--r-sm);
    border: 1px solid var(--rule); background: var(--surface-2);
    margin-bottom: var(--s3);
}
