/* ============================================================================
   TranShield console stylesheet.

   Originally a verbatim copy of prototype/transhield-console-prototype.html's
   <style> block. That copy served its purpose - it made the M1 port-fidelity
   claim checkable - and is preserved at the git tag `m1-prototype-parity`.

   THIS FILE is the visual authority. Values are not hand-picked per rule:
   they come from the token scale below. The prototype itself has been removed
   from the repo - it was a stale authority and a second copy of real merchant
   data. References to it in comments record WHY something is shaped as it is;
   they do not point at a live spec. See docs/design-system.md.

   The prototype's raw values were 17 distinct font sizes (including half-pixel
   steps), 18 spacing values and 10 radii - artifacts of a concept built fast,
   not decisions worth preserving. They are collapsed here into named steps.
   ============================================================================ */

/* ============================================================================
   Type: ONE family, two cuts, self-hosted.

   Atkinson Hyperlegible Next (variable 200-800) for everything and Atkinson
   Hyperlegible Mono (variable) for MIDs, account masks, decline codes and KQL
   excerpts. SIL Open Font License 1.1 - the licence ships beside the files at
   wwwroot/fonts/OFL.txt.

   SELF-HOSTED, and that is a privacy requirement rather than a preference: a
   request to fonts.gstatic.com from a partner's browser discloses that this
   console exists and that someone at that partner opened it. The console is a
   private tool. It asks a third party for nothing.

   The upstream filenames are AtkinsonHyperlegibleNext[wght].ttf - square
   brackets, which need percent-encoding in a URL and encoding again inside
   url(). Renamed to -Variable.ttf on download so no consumer has to care.

   THE FALLBACK IS METRIC-MATCHED, measured rather than guessed. With
   font-display: swap the fallback paints first, so a mismatched x-height
   reflows the whole queue the moment the real face arrives - the single most
   visible kind of layout jank on a 342-row table. Measured from the font
   binaries on 2026-09-05:

     Atkinson Hyperlegible Next  unitsPerEm 1000, sxHeight 496, hhea ascent
                                 984, descent -316, lineGap 0
     system-ui (macOS SF NS)     unitsPerEm 2048, sxHeight 1040

   size-adjust  = (496/1000) / (1040/2048) = 0.4960 / 0.5078 = 97.7%
   ascent-override  = (984/1000) / 0.977 = 100.7%
   descent-override = (316/1000) / 0.977 =  32.3%

   Segoe UI (0.500) and Arial (0.519) sit either side of SF's 0.5078, so one
   adjustment is within half a percent of every face in the stack.

   Both cuts share these metrics exactly, so one fallback face serves both.
   ============================================================================ */

@font-face {
  font-family: "Atkinson Hyperlegible Next";
  src: url("fonts/AtkinsonHyperlegibleNext-Variable.ttf") format("truetype-variations");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Atkinson Hyperlegible Mono";
  src: url("fonts/AtkinsonHyperlegibleMono-Variable.ttf") format("truetype-variations");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* The metric-matched stand-in. It has no src of its own beyond local() faces the
   platform already has, so it costs no request; its job is purely to carry the
   overrides above so the pre-swap render occupies the same boxes. */
@font-face {
  font-family: "Atkinson Fallback";
  src: local("SF Pro Text"), local("Segoe UI"), local("Helvetica Neue"), local("Arial");
  size-adjust: 97.7%;
  ascent-override: 100.7%;
  descent-override: 32.3%;
  line-gap-override: 0%;
}

:root {
  /* ---- Type scale -------------------------------------------------------
     ONE geometric chain, ratio ~1.16 (inside the 1.125-1.2 band), stated in
     rem off a 16px root so a reader who scales their browser scales the whole
     console: 11 - 13 - 15 - 17 - 20 - 24 - 28 - 34 - 40.

     Re-derived 2026-08-22. The old scale packed six text steps into 10-14px on
     the thesis that a dense Operate surface takes its hierarchy from weight and
     position rather than size. In use that read as overwhelming rather than
     efficient, so the premise was dropped and the scale rebuilt rather than
     nudged. Five DISTINCT text values now, not six: the old 10px and 12px steps
     had no job the neighbouring step could not do, so `--text-3xs` lifts to 11
     and `--text-xs` is an alias of `--text-2xs`. The slot names survive so that
     ~300 call sites did not have to be rewritten to change a scale; the values
     are the authority, the two aliases are deprecated spellings.

     Data density is untouched by this. Row COUNT is what makes a queue useful;
     13px body text was never what made it dense. */
  --text-3xs: 0.6875rem; /* 11px - chart axis labels, stat keys, badges. The floor. */
  --text-2xs: 0.8125rem; /* 13px - mono codes, hints, chips, table meta */
  --text-xs: var(--text-2xs);   /* deprecated alias of --text-2xs */
  --text-sm: 0.9375rem;  /* 15px - table body, nav, controls */
  --text-md: var(--text-sm);    /* deprecated alias of --text-sm; body default */
  --text-lg: 1.0625rem;  /* 17px - panel titles, stat values */
  --text-xl: 1.25rem;    /* 20px - page titles */
  --display-xs: 1.5rem;  /* 24px - score components */
  --display-sm: 1.75rem; /* 28px - KPI tile values */
  --display-md: 2.125rem;/* 34px - entity scores */
  --display-lg: 2.5rem;  /* 40px - the single largest number on screen */
  --display-xl: 3.25rem; /* 52px */    /* marketing type only. The console never uses it: 52px is a
                            claim, not a datum, and the largest thing on an Operate
                            surface should be a number. */

  /* Reading measure. In `ch` so it tracks whatever size the prose is set at, rather than a px width
     that silently becomes 138 characters per line when the text is small - which is exactly what
     `.disclaimer` was doing at 860px/11px. This caps PROSE only; tables, charts and stat grids are
     unaffected.
     MEASURED, 2026-09-05, and re-derived from the measurement rather than from the unit's promise.
     The critique's overlay had found `p.disclaimer` computing to 572.832px at 13px - exactly 68ch -
     with 88 characters counted in that box, a claimed ratio near 1.3 to one `ch`. Re-measuring in
     Chromium (Playwright, a Range-based first-line character count - the real word-wrap boundary,
     not a raw substring fit) did NOT reproduce that figure: seven running paragraphs across
     `.disclaimer`, `p.t-meta` and `.card .sub` on the queue and Cases pages wrapped at 92-101
     characters in that same 68ch box, not 88 - `ch` is the advance width of the ZERO glyph and this
     face draws a notably wide zero, the same property that makes it good for merchant IDs, and
     running prose's average advance is narrower still than the earlier estimate assumed. 52ch, the
     first re-derivation, still overshot: the same seven paragraphs wrapped at 67-80 characters
     there, two of them past the 75 ceiling. At 50ch they wrapped at 64-72, 1.371 characters per ch
     on average (chars-per-line divided by the token's own ch value, per paragraph, then averaged).
     50 x 1.371 = 68.6 characters, mid-band with room on both sides. The arithmetic is stated here
     the way the `size-adjust` fallback's is, and RhythmContractTests re-computes it from this
     comment on every build: a token whose stated intent is a 70-character line and whose delivered
     result is 88 is verified in exactly the false way DESIGN.md warns about two sections after it
     prescribes the token - and a re-derivation that trusts its own prior estimate rather than
     re-measuring commits the identical error one level up. */
  --measure: 50ch;

  --leading-tight: 1.2;
  --leading-normal: 1.45;
  --leading-relaxed: 1.6;

  --weight-medium: 600;
  --weight-semibold: 650;
  --weight-bold: 700;

  /* ---- Families ---------------------------------------------------------
     The ONLY two places a family name may be spelled. Six literals used to be
     spread through this file - one sans stack and five copies of the same mono
     stack - so changing the mono cut meant editing five rules and hoping.
     TypeSystemContractTests fails the build if a seventh appears. */
  --font-sans: "Atkinson Hyperlegible Next", "Atkinson Fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Atkinson Hyperlegible Mono", ui-monospace, Menlo, monospace;

  /* Weights, named for their job (spec section 3). 750 is the wordmark's and
     nothing else's: it is the one place on any surface that shouts. */
  --weight-label: 500;
  --weight-wordmark: 750;

  /* ---- Spacing ----------------------------------------------------------
     A 2px grid, not 4px. Deliberate: this console's job is dense numeric
     tables at 13-15px, and snapping to 4px would tighten or loosen every
     group by a visible amount for no functional gain. Nine named steps
     replace eighteen arbitrary values, and every step is even. */
  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 10px;
  --space-6: 12px;
  --space-8: 16px;
  --space-10: 20px;
  --space-13: 26px;
  --space-16: 32px;
  --space-20: 40px;
  /* Added 2026-09-04 (breathing-room pass). The scale topped out at 40px, which is a BAND
     separation, not a page-SECTION one - so every section boundary on every page was reaching
     down to --space-8 or --space-16 and the pages read as one undifferentiated run. 48 is the
     step where a reader's eye resets: it separates the queue from the glance section, and it is
     the air above and below the two bands an analyst acts on (Evidence, Actions). Even, so the
     2px grid holds. */
  --space-24: 48px;
  /* The sign-in pitch's fluid edge, and the ONLY place on the scale above a page section. It is
     the upper bound of a clamp() on a full-bleed brand panel, not a spacing step for the console -
     nothing in the app shell should reach for it. It is a token rather than a literal because the
     rule is absolute: no px outside :root. */
  --space-40: 80px;
  --hairline: 1px;       /* borders and rules, never spacing */

  /* WCAG 2.2 SC 2.5.8 minimum pointer target; NOT a --space- rung, so it never leaks into
     paddings. 24px is a constant fixed by the success criterion, not a rhythm altitude a future
     pass could retune — naming it as a spacing step would invite it into paddings where it means
     nothing (2026-09-04 final-review re-review). */
  --target-min: 24px;

  /* ---- Radii ---- */
  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 10px;
  --radius-xl: 12px;
  --radius-pill: 999px;

  /* ---- Motion ----------------------------------------------------------
     Exponential ease-out from an already-visible default. One shared curve so
     everything in the app decelerates the same way. */
  --dur-fast: 120ms;
  --dur-base: 180ms;
  --dur-slow: 240ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}


  /* ---- Colour: one identity family, and the analyst's ramp -----------------
     Stated in OKLCH, and the reason is not fashion: OKLCH's L is perceptual, so
     a theme can be re-valued by moving L and keeping C and H, and two tokens
     one step apart in L look one step apart. Hex could not express "the same
     colour, four steps lighter" without a designer eyeballing it.

     THE CONTRAST FIGURES IN THESE COMMENTS ARE COMPUTED, and TokenContrastTests
     computes them again from these very declarations on every build. That is a
     deliberate replacement of the previous arrangement, where the figures lived
     only in comments: on 2026-09-04 --ink-3 had been measured against a card
     ground and was failing on the footer ground at 4.06:1, and a comment cannot
     fail a build. In OKLCH the risk is worse, because nobody reads
     oklch(0.525 0.008 248) and knows what it clears.

     THE PALETTE IS DATACAP'S OWN, since 2026-09-05 and Ed's direction: brand red
     #941a25, header slate #778799, charcoal #231f20, dark grey #54595f, off
     white #f6f9fc, navy #1f2b37, border grey #d1d2d4, text grey #707070, text
     light #cbcdce. Every token below is derived from one of those. The Active
     Cyan #58d0f5 and the Muted Teal #aebcb9 are deliberately NOT used: cyan as
     an active state would fight both the red accent and the chart blue at once,
     and a third accent is how a single-accent palette stops being one.

     RED IS THE SINGLE ACCENT, AND IT IS ALSO THE CRITICAL COLOUR. That collision
     is the whole design problem here, and it is answered three ways rather than
     avoided:
       - The red is kept UNDER 10% of any surface. The navy and the greys carry
         the shell; the red is the primary action, the active nav glyph, a link,
         a focus ring. Nothing that is a ground except one button.
       - The identity red and the critical red are held a measured OKLab
         delta-E apart, so a claret link is not a scarlet band chip.
         TokenContrastTests asserts >= 0.12 on every identity/critical pair
         and the tightest measures 0.1208. The amendment that ordered this
         change is under section 2 of
         docs/superpowers/specs/2026-09-05-visual-identity-design.md.
       - Severity is never colour alone anyway - a band is a glyph shape and a
         word too (docs/design-system.md).
     Hue distance was the old instrument and no longer applies: the identity red
     sits ~2 degrees from --status-critical BY DESIGN. What is still asserted by
     hue is that the identity's GROUNDS - the navy and the slate - stay >= 60
     degrees from the whole severity ramp; measured 106.4 to 172.6 degrees, the
     nearest being --status-good against --id-ink.

     The one hue the grounds do NOT clear is --series-1, the chart blue at 255:
     the navy is at 249, six degrees away, and always was in that family. They
     are separated by CHROMA instead - the navy carries 17% of the chart blue's
     chroma and the slate 20% - and a near-neutral ground in a series colour's
     hue is not mistakable for data. Asserted as a chroma bar, not hidden.

     Neutrals are tinted toward the SLATE hue (248-253) at chroma <= 0.012. A
     pure grey beside a tinted paper reads as a hole punched in the surface. The
     one warm neutral is --ink-1, which is the palette's charcoal: warm ink on
     cool paper is the pairing the brand already publishes. */
  :root {
    color-scheme: light;

    /* Grounds. Paper first: this is a field notebook, and the surface an
       analyst spends the day on is paper, not chrome. */
    --paper:       oklch(0.981 0.005 248);   /* #f6f9fc - the palette's Off White, exactly */
    --page:        var(--paper);             /* deprecated alias; ~90 call sites predate --paper */
    --surface-1:   oklch(0.995 0.003 248);   /* #fcfeff - panels, a hair above paper */
    --footer-bg:   oklch(0.952 0.006 248);   /* #ecf0f3 - chrome, a step BELOW paper */
    --footer-line: oklch(0.894 0.008 248);
    --grid:        oklch(0.906 0.007 248);
    --axis:        oklch(0.812 0.012 248);

    /* Inks, straight off the palette. Measured on --paper / --surface-1 /
       --footer-bg in that order.
       --ink-1 is Charcoal #231f20, re-tinted to the brand red's hue at the same
       chroma so the one warm neutral is warm in the brand's own direction.
       --ink-2 is Dark Gray #54595f, byte-exact.
       --ink-3 is Text Gray #707070 DARKENED from its own L 0.545 to 0.525: at
       the published value it measured 4.31 on --footer-bg, under the 4.5 floor.
       That is the same footer-ground miss the 2026-09-04 pass found, and the
       smallest step that clears it on all three is 0.525. */
    --ink-1: oklch(0.244 0.006 22.7);  /* 15.44 / 16.07 / 14.18 */
    --ink-2: oklch(0.462 0.011 253);   /*  6.69 /  6.96 /  6.14 - Dark Gray #54595f */
    --ink-3: oklch(0.525 0.008 248);   /*  5.10 /  5.31 /  4.69 - all three clear 4.5 */

    /* Borders and shadows out of the ink rather than out of black, so a hairline
       on tinted paper is the paper's own colour a few steps down. 16%, not the
       previous 10%: the palette's Border Gray is #d1d2d4, and a charcoal ink
       over Off White paper needs 16% to composite there - measured #d0d2d4, at
       1.44:1 against paper, where the old 10% gave #dee0e3 and 1.25:1. The dark
       block below is UNCHANGED at 10% (1.22:1 there): its ink is near-white over
       a near-black paper, so the palette's light-theme border grey has nothing
       to say about it, and re-weighting dark's hairlines is not this pass. */
    --border:     color-mix(in oklab, var(--ink-1) 16%, transparent);
    --shadow-ink: oklch(0.244 0.006 22.7);
    --shadow: 0 1px 3px color-mix(in oklab, var(--shadow-ink) 6%, transparent),
              0 4px 14px color-mix(in oklab, var(--shadow-ink) 5%, transparent);

    /* Dark ink for a theme-INVARIANT fill, and the counterpart of --knob below.
       Blazor's error banner is --status-warn in both themes, so its text cannot
       be --ink-1 (near-white in dark) and was four copies of #0b0b0b - a pure
       grey. Re-tinted to the charcoal's hue with the palette; 10.43:1 on
       --status-warn, measured. */
    --ink-on-fill: oklch(0.170 0.006 22.7);

    /* ---- The identity family ----------------------------------------------
       The navy and the greys carry the shell; the brand red is the single
       accent, under 10% of any surface. */

    /* The Datacap navy #1f2b37, byte-exact: rail ground, sign-in panel ground,
       report masthead. NOT the primary button any more - see --id-fill. */
    --id-ink:      oklch(0.284 0.028 249);
    /* Text ON the ink: the palette's Text Light #cbcdce family, at 9.01:1. The
       rail's nav labels and the wordmark on the rail. */
    --id-ink-hi:   oklch(0.847 0.006 248);   /* #cacdd0 */
    /* The accent that works ON the ink: active nav glyph, the 2px inset rule,
       links and focus rings on the rail, the emphasised word in the sign-in
       claim. Brand red LIFTED from its own L 0.434 to 0.720 - the published red
       measures 1.66:1 on the navy and is unreadable there. 0.720 rather than the
       0.680 that first clears 4.5:1, because at 0.680 it sits only 0.1095 from
       --status-critical in OKLab and the two reds start to read as one; at
       0.720 the distance is 0.1523 and the contrast 5.46:1. */
    --id-accent:   oklch(0.720 0.140 22.7);  /* #f07e7b - 5.45:1 on --id-ink */
    /* The identity as TEXT on paper: links, link-buttons, selected chip text,
       the focus ring on paper. DEEPENED from the published #941a25 (L 0.434) to
       L 0.388, and the reason is measured rather than aesthetic: at the
       published value it sits 0.0753 from --critical-text in OKLab, which is
       one red with two names on the same page. At 0.388 the distance is 0.1208
       and it measures 9.94:1 on paper. */
    --id-text:     oklch(0.388 0.156 22.7);  /* #840118 - 9.94:1 on paper, 10.34 on --surface-1 */
    /* The primary action's GROUND, and the one place the red is a ground: the
       published Brand Red #941a25 itself, theme-invariant the way --accent-fill
       is. A fill and the text on it cannot share a value (docs/design-system.md,
       "the rule that matters"), so this is not --id-text. Its distance from
       --status-critical - the pair that appears side by side on a selected
       Critical row, a button beside a meter - is 0.1443 in OKLab. */
    --id-fill:      oklch(0.434 0.156 22.7); /* #941a25, byte-exact */
    /* And its label: a WARM near-white, 7.68:1 on the fill. Not --id-ink-hi,
       which is the navy's cool high tone and visibly mismatched on a warm red. */
    --id-fill-ink:  oklch(0.960 0.008 22.7); /* #f7f0ef - 7.68:1 on the fill */
    /* The palette's Header Slate #778799, byte-exact, and it exists as a token
       for one reason: the selection wash is mixed FROM IT and never from the
       red. A selected row in the accent red would read as a critical row, which
       is the one thing this palette must not do. A translucent mix never enters
       TokenContrastTests's map, so the wash's source hue has to be a token that
       does - which is how the >= 60 degrees from the severity ramp gets
       asserted at all. */
    --id-slate:     oklch(0.617 0.033 251);
    /* The selection wash and the pane's identity band. 8% of the slate over
       --surface-1 composites to #f1f4f7, which carries .sev-critical's 13% tint
       at 4.79:1 and a band chip's 10% at 5.00:1 - measured, both better than
       the 4.54 / 4.53 the green wash left. */
    --id-wash:      color-mix(in oklab, var(--id-slate) 8%, transparent);
    /* Row HOVER, at half the selection's strength. Without a second step, hover
       and selected paint the same ground and the only difference is the selected
       row's outline, which is a real regression on a queue an analyst arrows
       down. */
    --id-wash-soft: color-mix(in oklab, var(--id-slate) 4%, transparent);
    /* The sign-in panel's one soft glow, and nothing else uses it. This is the
       original pre-branch #3987e5 as an oklch literal - the blue Ed asked for
       back on 2026-09-05 - kept as its own token rather than reaching for
       --series-1, because --series-1 means DATA and this is decoration on one
       panel that would follow a chart re-tune if it borrowed that name. */
    --id-glow:      oklch(0.622 0.161 255);  /* #3987e5 */

    /* A toggle knob and a switch dot are white in BOTH themes, the same way
       --accent-fill is theme-invariant: they sit on a filled track, never on the
       page. Tinted, so it is not a pure neutral. */
    --knob: oklch(0.99 0.004 248);

    /* ---- The severity ramp -------------------------------------------------
       Converted to OKLCH at the SAME measured contrast, not re-tuned: every one
       of these renders to its previous hex BYTE-EXACTLY, which is how "same
       measured contrast" was checked rather than asserted. The previous hex is
       in the comment so the round-trip stays auditable. Four of them carry a
       fourth decimal on L or C (--series-1, --status-good, --status-warn,
       --good-text) because three decimals of L and one of H do not resolve
       finely enough to land on those particular bytes; the rest are exact at
       three.

       The fill/text split is unchanged and load-bearing (docs/design-system.md,
       "the rule that matters"): WCAG applies 3:1 to a bar or a meter and 4.5:1
       to text, so a colour tuned for a fill fails as text and each has a text
       sibling. Never use a --series-* or --status-* as a `color:`. */
    --series-1:        oklch(0.5755 0.1625 255.5);   /* was #2a78d6 */
    --series-2:        oklch(0.671 0.175 40.6);      /* was #eb6834 */
    --status-good:     oklch(0.6215 0.2075 142.55);  /* was #0ca30c */
    --status-warn:     oklch(0.811 0.1645 78.4);     /* was #fab219 */
    --status-serious:  oklch(0.717 0.141 41.6);      /* was #ec835a */
    --status-critical: oklch(0.575 0.186 25.2);      /* was #d03b3b */
    --good-text:       oklch(0.432 0.150 142.55);    /* was #006300 */
    --accent-text:     oklch(0.534 0.150 255.6);     /* was #266cc1 */
    --accent-fill:     oklch(0.505 0.150 256.2);     /* was #1e63b8 */
    --accent-wash:     color-mix(in oklab, var(--series-1) 8%, transparent);
    /* These two DEEPEN rather than convert, and the reason is measured. A band
       chip is a 10% status tint composited over its row's ground, and the row's
       ground is --id-wash. At the previous values (#b53232 / #97553b)
       band-urgent read 4.53 and band-review 4.51 on a selected row - passing,
       with nothing left for a future wash change. --critical-text went one step
       FURTHER than the spec's L 0.512, to 0.508, because at 0.512 the denser
       13% tint of .sev-critical measured 4.46 on a selected row.
       RE-MEASURED 2026-09-05 against the slate wash, which is lighter than the
       green ink it replaced and so lifts the composited row: .sev-critical now
       reads 4.79 and band-urgent 5.00 on a selected row, band-review 4.99.
       Both values are UNCHANGED - the ramp is not re-tuned by this pass - and
       the gap they were tuned to close is wider than it was, not narrower. */
    --critical-text:   oklch(0.508 0.168 25.2);   /* was #b53232, now #b22e2f */
    --serious-text:    oklch(0.510 0.096 41.9);   /* was #97553b, now #945238 */

    /* The rail's three tokens are ALIASES of the identity now, not their own
       palette. Kept as names because ~30 rules spell them, and because "the
       rail's ground" is a different idea from "the identity ink" even when they
       are the same value. --brand-navy / -ink / -hi are gone: they were a
       fourth blue that existed only because the sign-in panel needed a dark
       ground before there was an identity to reach for. The identity ink IS a
       navy again since 2026-09-05, which is the palette's own, and the old
       tokens do not come back - one name for one colour. */
    --sidebar-bg:     var(--id-ink);
    --sidebar-ink:    var(--id-ink-hi);
    --sidebar-active: color-mix(in oklab, var(--id-ink-hi) 12%, transparent);
  }
  /* Dark is the SAME tokens re-valued, never a second design (spec section 1:
     "Dark stays, as the same tokens re-valued, for the few who choose it").
     Both blocks declare the same names - TokenContrastTests fails the build if
     one drifts - because the 2026-09-04 finding generalises: the failures run in
     OPPOSITE directions. --critical-text fails in dark and needs lightening;
     --ink-3 fails in light and needs darkening. A token defined in one theme
     only inherits a value tuned for the other. */
  :root[data-theme="dark"] {
    color-scheme: dark;

    --paper:       oklch(0.19 0.005 248);   /* #121416 */
    --page:        var(--paper);
    --surface-1:   oklch(0.23 0.005 248);   /* #1b1d1f */
    /* Dark inverts the direction: the footer is a step LIGHTER than paper,
       because darker than #121416 is effectively black. */
    --footer-bg:   oklch(0.21 0.005 248);   /* #17191b */
    --footer-line: oklch(0.292 0.008 248);
    --grid:        oklch(0.292 0.008 248);
    --axis:        oklch(0.340 0.010 248);

    --ink-1: oklch(0.975 0.006 248);   /* 17.19 / 15.72 / 16.49 - not pure white */
    --ink-2: oklch(0.812 0.010 248);   /* 10.30 /  9.42 /  9.89 */
    /* L 0.645, a step above the spec's 0.640: --ink-3 is read inside table rows
       (.t-micro, .m-loc, .band-chip.band-tripwire) and at 0.640 it measured
       4.47 on a SELECTED row - the same composited-ground miss the 2026-09-04
       pass found on --footer-bg. At 0.645 the worst of the five grounds is 4.62,
       re-measured 2026-09-05 against the slate wash. */
    --ink-3: oklch(0.645 0.010 248);   /*  5.60 /  5.13 /  5.38 */

    /* 10%, unchanged, against the light block's 16%: #d1d2d4 is a light-theme
       border grey and there is no dark counterpart in the palette. 1.22:1
       against dark paper, measured. */
    --border:     color-mix(in oklab, var(--ink-1) 10%, transparent);
    --shadow-ink: oklch(0.05 0.004 248);
    --shadow: 0 1px 3px color-mix(in oklab, var(--shadow-ink) 40%, transparent);

    /* Theme-invariant, like --knob: the amber it sits on is the same in both. */
    --ink-on-fill: oklch(0.170 0.006 22.7);

    /* LIFTED from L 0.245 on 2026-09-05 (critique P1). At 0.245 the rail measured 1.14:1 against
       the dark page ground and 13.62:1 against the light one: the identity's primary surface
       existed in one theme and vanished in the other, and nothing failed, because this file
       measured text against grounds and never two grounds against each other.
       0.330 is the top of the window its own text bar leaves. --id-ink-hi has to clear 7:1 on it,
       which caps the rail at a relative luminance of 0.1 even with pure white text, so 3:1 between
       the two grounds is not reachable at any value - the proof is in TokenContrastTests. What 0.330
       buys is 1.52:1 against the page, up from 1.14, with --id-ink-hi still at 7.64:1 and
       --id-accent at 7.67:1. The 3:1 the success criterion asks for is carried by the hairline
       edge in --id-slate on .sidebar and .auth-pitch. */
    --id-ink:    oklch(0.330 0.026 249);  /* #2b3742 - the navy a step deeper, against a dark page */
    --id-ink-hi: oklch(0.847 0.006 248);  /* 10.15:1 on the dark ink - the same value as light */
    /* LIGHTER than the light theme's accent, and this is the 2026-09-04 finding
       generalising again: the failures run in opposite directions. In light,
       --critical-text is DARK (L 0.508) and the identity accent can stay near
       the brand red at L 0.720. In dark, --critical-text lifts to L 0.700, so
       an accent at 0.720 would sit 0.021 from it in OKLab - one colour. It
       lifts past it instead: 0.1727 away, and 10.20:1 on the dark ink. */
    --id-accent: oklch(0.860 0.080 22.7); /* #ffbdb9 */
    --id-text:   oklch(0.840 0.090 22.7); /* #ffb4af - 10.88:1 on dark paper, 0.1504 from --critical-text */
    --id-fill:      oklch(0.434 0.156 22.7); /* the same brand red in both themes */
    --id-fill-ink:  oklch(0.960 0.008 22.7); /* 7.68:1 on it, in both themes */
    /* The slate LIFTED to L 0.750, because the wash is a percentage of it over a
       near-black page and the published #778799 at L 0.617 barely moves the
       ground. It is still the slate and still nowhere near the red: a selected
       row must never read as a critical one in either theme. */
    --id-slate:     oklch(0.750 0.030 251);
    /* 6% rather than the light theme's 8%: at 8% the composited band chips fall
       under 4.5:1 - measured, and the whole reason this value differs from the
       spec's "8% tint" instruction, which is a light-theme instruction. At 6% of
       the lifted slate a selected row on --surface-1 carries .sev-critical at
       4.83 and band-normal at 4.68. */
    --id-wash:      color-mix(in oklab, var(--id-slate) 6%, transparent);
    --id-wash-soft: color-mix(in oklab, var(--id-slate) 3%, transparent);
    --id-glow:      oklch(0.622 0.161 255);  /* #3987e5, the same glow in both themes */

    --knob: oklch(0.99 0.004 248);

    --series-1:        oklch(0.622 0.161 255.1);     /* was #3987e5 */
    --series-2:        oklch(0.622 0.173 40.1);      /* was #d95926 */
    --status-good:     oklch(0.6215 0.2075 142.55);
    --status-warn:     oklch(0.811 0.1645 78.4);
    --status-serious:  oklch(0.717 0.141 41.6);
    --status-critical: oklch(0.575 0.186 25.2);
    /* LIGHTENS, and this one is a fix rather than a conversion: at the previous
       #0ca30c (the fill's own value, used as text) .band-chip.band-normal
       measured 3.96 on a selected row and 4.48 on a plain card - a pre-existing
       dark-theme failure the flat-pair figures never showed, because a green
       this saturated only fails once its own 10% tint lifts the ground under it.
       At L 0.660 the worst ground is 4.68, re-measured 2026-09-05 against the
       slate wash (5.20 on a plain card). */
    --good-text:       oklch(0.660 0.2075 142.55);   /* was #0ca30c, now #27b023 */
    --accent-text:     oklch(0.643 0.152 254.9);     /* was #458ee7 */
    --accent-fill:     oklch(0.505 0.150 256.2);     /* same fill in both themes */
    --accent-wash:     color-mix(in oklab, var(--series-1) 12%, transparent);
    /* LIGHTENS, and this one is a fix rather than a conversion. The new dark
       grounds are lighter than the old (--surface-1 L 0.23 against 0.217), and
       at the previous #db6868 a band chip read 4.59 on a plain card and 4.11 on
       a selected row. 4.11 was ALREADY the state before this pass - the
       selected-row gap docs/design-system.md records as open. At L 0.700 the
       worst ground is 4.78:1, so the pre-existing gap closes here rather than
       being carried forward. */
    --critical-text:   oklch(0.700 0.145 21.8);      /* was #db6868, now #eb7675 */
    /* NUDGED off the fill. It was byte-identical to --status-serious above - a fill used as
       text - and a fill and its text having one value is how the next re-tune moves both at
       once. L 0.719 is the smallest three-decimal step that renders a DIFFERENT sRGB byte
       (#ed835a against the fill's #ec835a); at 0.718 the two still round to the same hex.
       Measured on the three grounds TokenContrastTests checks: 7.04 on --paper, 6.44 on
       --surface-1, 6.75 on --footer-bg. */
    --serious-text:    oklch(0.719 0.141 41.6);      /* was #ec835a, now #ed835a */

    --sidebar-bg:     var(--id-ink);
    --sidebar-ink:    var(--id-ink-hi);
    --sidebar-active: color-mix(in oklab, var(--id-ink-hi) 10%, transparent);
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  /* min-height, not height: the prototype pinned body to exactly the viewport, so the flex
     children could never grow past it and the nav rail stopped mid-page on any long view. */
  html, body { min-height: 100%; }
  html { scrollbar-color: var(--axis) var(--page); }
  /* GRID, not flex. The footer has to sit beneath BOTH the nav rail and the content and span
     the full width - a real page footer, not a block at the end of the content column. Flex in
     a row cannot express that; a two-area grid can, and it says the layout out loud:

         rail   main
         footer footer

     Everything else in <body> (the drawer, toasts, the Blazor error bar) is position:fixed, so
     it is out of flow and never becomes a stray grid item. */
  body {
    font-family: var(--font-sans);
    background: var(--page); color: var(--ink-1); font-size: var(--text-md);
    display: grid;
    min-height: 100vh;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: 1fr auto;
    grid-template-areas:
      "rail main"
      "footer footer";
    accent-color: var(--id-text); caret-color: var(--id-text);
  }
  ::selection { background: color-mix(in srgb, var(--series-1) 24%, transparent); }
  button, input { font-family: inherit; }
  :focus-visible { outline: 2px solid var(--id-text); outline-offset: 2px; border-radius: var(--radius-xs); }
  /* THE ONE EXCEPTION, and it is scoped as narrowly as it can be (critique P1, 2026-09-05).
     FocusTarget moves focus to the page's h1 on every route change so a screen-reader user hears
     where they landed. That focus is programmatic and the heading is not a control, so the ring
     Chromium painted around it said "something is wrong with this" about the first thing in the
     reading order - a 2px claret rectangle, in the colour this page also uses for its primary
     action and its links, on every load in both themes.
     The move stays and the paint goes. A heading with tabindex="-1" is not reachable by Tab, so
     SC 2.4.7 has no claim on it, and the next Tab still lands on a real control with its ring
     intact. Do not widen this: the drawer's close button, every cell button and every field rely
     on the rule above. */
  h1[tabindex="-1"]:focus-visible { outline: none; }
  .ic { width: 14px; height: 14px; flex: none; vertical-align: -2px; }
  .ic-s { flex: none; vertical-align: -1px; }
  @media (prefers-reduced-motion: reduce) {
    .drawer, .overlay, .toast, .tt, .vpop { transition: none !important; }
    /* Animations, not only transitions - the rule docs/design-system.md records. `none` rather
       than a 0ms duration, so the sign-in brand block simply renders in its final state. */
    .auth-brand { animation: none !important; }
  }
  /* ---------- Sidebar ---------- */
  .sidebar {
    grid-area: rail;
    width: 208px; background: var(--sidebar-bg); color: var(--sidebar-ink);
    display: flex; flex-direction: column;
    /* Light-on-dark type gets +0.05 line-height (spec section 3): light type reads as a
       lighter weight than it is and needs the extra room. */
    line-height: calc(var(--leading-normal) + 0.05);
    /* The rail's boundary against the page, and the thing WCAG 2.2 SC 1.4.11 actually asks 3:1 of.
       In --id-slate, which measures 3.91:1 on the rail and 3.48:1 on paper in light, 5.49 and 8.32
       in dark. A hairline, so it is a boundary and not a stripe. */
    border-inline-end: var(--hairline) solid var(--id-slate);
  }

  /* The rail's identity block: the mark, then the wordmark. The gradient plate the borrowed
     shield sat on is gone with it - a rounded gradient tile behind a glyph is the "large
     rounded-corner icon above every heading" pattern, and it existed to make a borrowed mark
     look deliberate. */
  .logo {
    display: flex; align-items: center; gap: var(--space-5);
    padding: var(--space-2) var(--space-4) var(--space-10);
    color: var(--id-ink-hi);
  }

  /* The mark takes the ink of whatever it sits in; these two rules only place it. */
  .mark { flex: none; display: block; }

  .wordmark { display: grid; gap: var(--hairline); min-width: 0; }
  .wordmark-name {
    font-family: var(--font-sans);
    font-size: var(--text-md);
    font-weight: var(--weight-wordmark);
    letter-spacing: -0.02em;
    line-height: var(--leading-tight);
  }
  .wordmark-by {
    font-size: var(--text-2xs);
    font-weight: 400;
    letter-spacing: 0;
    opacity: .8;
  }
  .wordmark-lg .wordmark-name { font-size: var(--text-xl); }
  .nav { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-3); }
  .nav a {
    display: flex; align-items: center; gap: var(--space-5); padding: var(--space-4) var(--space-5); border-radius: var(--radius-md);
    color: var(--sidebar-ink); text-decoration: none; font-size: var(--text-sm);
  }
  .nav a svg { width: 16px; height: 16px; flex: none; opacity: .85; }
  /* The active item's indicator lives INSIDE the rail, drawn as an inset shadow on the item's own
     box (spec 5.1). Two reasons it is not a border-left, and they point the same way: a 2px
     coloured side border on content is the stripe anti-pattern, and a border that appears on
     activation shifts the label 2px sideways every time an analyst navigates. An inset shadow
     paints within the box that already exists, so activating an item moves nothing.
     The 2px is the one px literal the spec names: it is a rule, not spacing and not a hairline. */
  .nav a.active {
    background: var(--sidebar-active);
    color: var(--id-ink-hi);
    font-weight: 600;
    box-shadow: inset 2px 0 0 0 var(--id-accent);
  }
  /* The glyph carries the accent, at full opacity: the inactive glyphs are dimmed to .85, and an
     accent-coloured glyph at .85 would be a third colour nobody picked. */
  .nav a.active svg { color: var(--id-accent); opacity: 1; }
  .nav a:not(.active):hover { background: color-mix(in oklab, var(--id-ink-hi) 8%, transparent); }
  .sidebar-foot { margin-top: auto; padding: var(--space-5) var(--space-4) var(--space-1); font-size: var(--text-3xs); line-height: 1.5; opacity: .72; }
  /* The narrow-viewport shell block used to live here and was silently defeated by the
     pinned-workspace sticky rules further down the file (equal specificity, later position —
     the cascade won and C6 was the result). It now lives BELOW those rules, near the sticky
     block it overrides; ShellCascadeContractTests pins that ordering. */
  /* ---------- Main / topbar ---------- */
  /* Top padding is a step tighter than the sides on purpose: the sides are protecting the prose
     measure, the top is only separating the page from the window edge.

     2026-09-04 breathing-room pass: the 2026-08-22 above-the-fold pass had taken the top to
     --space-8 and the topbar's bottom margin to --space-6 to win 4px and 4px above the fold. That
     left the page TITLE 12px above the first card, which is closer than two chips inside a group,
     and it is the first thing that reads as cramped. Both go back up a step. The cost is 12px of
     fold and it is the right trade: the chrome has to look like chrome, not like the first row of
     the content. Above 1220px the side gutter takes a step too - there is demonstrably room, and
     26px against a full-bleed rail reads as an edge rather than a margin. */
  .main { grid-area: main; min-width: 0; padding: var(--space-10) var(--space-13) var(--space-20); }
  @media (min-width: 1220px) { .main { padding-inline: var(--space-16); } }
  .topbar { display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap; margin-bottom: var(--space-10); }
  .entity-head { display: flex; align-items: center; gap: var(--space-5); min-width: 0; }
  .entity-head > div { min-width: 0; }
  .entity-avatar {
    width: 34px; height: 34px; border-radius: var(--radius-lg); flex: none;
    background: var(--accent-wash); color: var(--accent-text);
    display: grid; place-items: center; font-weight: 700; font-size: var(--text-sm);
    border: 1px solid var(--border);
  }
  .entity-head h1 { font-size: var(--text-xl); font-weight: 650; }
  /* Reflows rather than clips — the meta names the portfolio and its processors, which is data. */
  .entity-head .meta { font-size: var(--text-xs); color: var(--ink-2); margin-top: var(--hairline); overflow-wrap: anywhere; }
  .topbar-right { margin-left: auto; display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }

  /* One line by default — unchanged from before, and the right shape when the pill already has a
     row to itself. It STACKS only in the one-row topbar below, where its 427px single line is what
     pushed the whole right-hand group onto a second row. Same two facts either way. */
  .env-pill {
    display: flex; align-items: center; line-height: 1.3;
    font-size: var(--text-2xs); padding: var(--space-2) var(--space-5); border-radius: var(--radius-pill);
    background: var(--id-wash); color: var(--id-text); font-weight: 600;
    border: 1px solid var(--border); white-space: nowrap;
  }
  .env-pill .ep-read { font-weight: 400; opacity: .85; }
  /* Decorative punctuation only; it carries no meaning that the two spans do not already carry. */
  .env-pill .ep-read::before { content: "·"; margin: 0 var(--space-2); opacity: .7; }

  /* ---- One-row topbar, wide viewports only (2026-08-22 above-the-fold pass) -----------------
     With flex-wrap, the browser pushes the right-hand group onto a SECOND ROW the moment
     max-content exceeds the line — it never shrinks first — and that row was 45px of chrome above
     the first datum on every page. Above 1220px there is demonstrably room for one row, so:
       · the vantage control never shrinks (it is a hit target, not text),
       · the right-hand group never shrinks (stacking Refresh under Theme is worse than a wrap),
       · the identity block takes flex-basis 0 so it does not CLAIM its content width, absorbs
         whatever is left, and lets its meta LINE reflow to two lines inside the row.
     Text rewraps; nothing truncates and nothing is hidden. The floor stops the meta from being
     squeezed into a one-character column.
     1220px is measured, not guessed: vantage 238 + identity floor + controls 426 + two 16px gaps
     needs ~960px of .main, and .main is the viewport less the 208px rail and 52px of padding. */
  @media (min-width: 1220px) {
    .topbar { flex-wrap: nowrap; }
    .topbar > .vantage, .topbar > .topbar-right { flex: none; }
    .entity-head { flex: 1 1 0; min-width: 24ch; }
    /* Stacked here and only here: two lines fit inside the row height the identity block already
       claims, where one 427px line would not fit on the line at all. */
    .env-pill { flex-direction: column; align-items: flex-end; border-radius: var(--radius-lg); }
    .env-pill .ep-read::before { content: none; }
  }
  .top-btn {
    border: 1px solid var(--border); background: var(--surface-1); color: var(--ink-2);
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-5); cursor: pointer; font-size: var(--text-xs);
    display: inline-flex; align-items: center; gap: var(--space-3);
    min-height: var(--target-min);
  }
  .top-btn:hover { background: var(--id-wash); }
  /* ---------- Vantage switcher ---------- */
  .vantage { position: relative; }
  .vantage-btn {
    display: flex; align-items: center; gap: var(--space-4);
    border: 1px solid var(--border); background: var(--surface-1); color: var(--ink-1);
    border-radius: var(--radius-lg); padding: var(--space-4) var(--space-6); cursor: pointer; font-size: var(--text-sm); font-weight: 600;
    box-shadow: var(--shadow);
  }
  .vantage-btn .vb-type { font-size: var(--text-3xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--id-text); }
  .vantage-btn svg { color: var(--ink-2); }
  .vpop {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 60; min-width: 320px;
    background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-xl);
    box-shadow: 0 8px 30px color-mix(in oklab, var(--shadow-ink) 18%, transparent); padding: var(--space-3);
    opacity: 0; transform: translateY(-4px); pointer-events: none; transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
  }
  .vpop.open { opacity: 1; transform: none; pointer-events: auto; }
  .vpop .vg { font-size: var(--text-3xs); font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-2); padding: var(--space-4) var(--space-5) var(--space-2); }
  .vpop button.vi {
    display: flex; width: 100%; text-align: left; align-items: center; gap: var(--space-4);
    border: none; background: transparent; color: var(--ink-1);
    padding: var(--space-4) var(--space-5); border-radius: var(--radius-md); cursor: pointer; font-size: var(--text-sm);
  }
  .vpop button.vi:hover { background: var(--id-wash); }
  .vpop button.vi.sel { background: var(--id-wash); font-weight: 600; }
  .vpop .vi .vi-sub { display: block; font-size: var(--text-2xs); color: var(--ink-2); font-weight: 400; margin-top: var(--hairline); }
  .vpop .vi .vi-dot { width: 8px; height: 8px; border-radius: var(--radius-pill); flex: none; }
  /* ---------- Filter row ---------- */
  /* space-6 rather than space-10 below the filters: they belong TO the tiles they filter, so a
     tighter gap here and the wider one under the tiles reads as grouping rather than compression. */
  .filter-row { display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-6); }
  .chip-group { display: flex; gap: var(--space-2); background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-2); }
  .chip {
    border: none; background: transparent; color: var(--ink-2);
    padding: var(--space-2) var(--space-6); border-radius: var(--radius-sm); font-size: var(--text-xs); cursor: pointer; transition: background var(--dur-fast) var(--ease-out);
    min-height: var(--target-min);
  }
  .chip:hover:not(.active) { background: color-mix(in srgb, var(--ink-3) 10%, transparent); }
  .chip.active { background: var(--id-wash); color: var(--id-text); font-weight: 600; }
  .search {
    flex: 1; min-width: 180px; max-width: 320px;
    background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: var(--space-4) var(--space-6); font-size: var(--text-sm); color: var(--ink-1);
  }
  .search::placeholder { color: var(--ink-2); opacity: .75; }
  .search:focus { border-color: var(--id-text); outline: none; box-shadow: 0 0 0 3px var(--id-wash); }
  .snapshot-note { font-size: var(--text-2xs); color: var(--ink-2); margin-left: auto; }
  /* ---------- KPI tiles ---------- */
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-8); margin-bottom: var(--space-13); }
  .tile { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: var(--space-10); box-shadow: var(--shadow); }
  .tile .label { font-size: var(--text-xs); color: var(--ink-2); }
  .tile .value { font-size: var(--display-sm); font-weight: 650; margin-top: var(--space-2); letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
  .tile .delta { font-size: var(--text-2xs); margin-top: var(--space-2); color: var(--ink-2); }
  .tile .delta .up-good { color: var(--good-text); font-weight: 600; }
  .tile .delta .up-bad { color: var(--critical-text); font-weight: 600; }
  /* ---------- Cards / charts ---------- */
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); margin-bottom: var(--space-13); }
  @media (max-width: 980px) { .grid-2 { grid-template-columns: 1fr; } }
  /* The card's own gutter, one step wider than its top and bottom: the sides carry a chart axis
     or a paragraph and want the air, the vertical is already separated by the card's edge. */
  .card { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: var(--space-10) var(--space-13); box-shadow: var(--shadow); min-width: 0; }
  .card h2, .card h3 { font-size: var(--text-lg); font-weight: 650; }
  .card .sub { font-size: var(--text-2xs); color: var(--ink-2); margin-top: var(--space-1); margin-bottom: var(--space-8); max-width: var(--measure); }
  svg text { font-family: inherit; }
  /* ---------- Table ---------- */
  .table-card { padding: 0; overflow: hidden; }
  /* The horizontal padding STAYS at --space-10 while the card's own gutter goes to --space-13:
     a table card's header aligns with the table below it, not with a prose card's edge. */
  .table-head { display: flex; align-items: baseline; gap: var(--space-5); padding: var(--space-10) var(--space-10) var(--space-6); flex-wrap: wrap; }
  .table-head .count { font-size: var(--text-xs); color: var(--ink-2); }
  /* The queue's own header. Everything a reader needs before the first row sits on one line:
     what the list is, how many rows, what DNA cleared off it, and the standing "a lead is not a
     finding" label. Baseline, not centre: the disclosure at the end of the row grows downward
     when opened, and centring would drag the heading down with it. */
  /* 2026-09-04 breathing-room pass. Measured before, in Chromium at 1616px: the header's last row
     sat 10px above the table's first heading row and its own three rows (title, controls,
     disclosure) were 35px, 4px and 0px apart. That is not a rhythm, it is a pile. The header now
     closes 20px above the table and its rows step 16 / 16, against the 4px that groups chips
     INSIDE a group. */
  .queue-head {
    display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-4) var(--space-5);
    padding: var(--space-10) var(--space-10);
  }
  .queue-head h2 { font-size: var(--text-lg); font-weight: 650; margin: 0; }
  .queue-head .count { font-size: var(--text-xs); color: var(--ink-2); }
  /* Its own row at the end of the header, never in the flex line. With margin-left:auto it sat at
     the right edge while closed and slid ~340px left when opened, because an open <details> is as
     wide as its prose - the control moved out from under the pointer that had just clicked it.
     A full-basis row opens downward and moves nothing. */
  .queue-head .disclosure { flex-basis: 100%; margin-top: var(--space-8); }
  .queue-head .disclosure p { margin: var(--space-4) 0 0; max-width: var(--measure); }
  /* The controls that filter THIS list, on their own row of the header. They used to sit in a
     filter row above four KPI tiles - which have moved below the queue - so they were separated
     from the only thing they change. Its own basis row rather than the flex line: the chips and
     the search box are hit targets on their own baseline, and wrapping them into the heading's
     line drags the heading off the top of the card.

     The gap is --space-8, FOUR TIMES the --space-2 that separates chips inside a group. That
     ratio is the whole point: at the old 10px against 4px the two chip groups and the search box
     read as one undifferentiated strip of controls, and a reader had to parse the borders to find
     where "population" ended and "severity" began. Space does that job now. */
  .queue-controls {
    flex-basis: 100%; display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap;
    margin-top: var(--space-8);
  }
  /* Below the work surface's breakpoint the controls wrap, and the population note is what lands
     awkwardly: margin-left:auto pins it to the right of whatever row it falls on, so it read as a
     label for the search box beside it. On its own full row it is a caption for the controls, which
     is what it is. A deliberate wrap, not a leftover one. */
  @media (max-width: 1219px) {
    .queue-controls .snapshot-note { flex-basis: 100%; margin-left: 0; }
  }
  /* A LABEL, not an alert. What it says is permanently true, so it takes the neutral chip shape
     and the border token rather than a status colour. */
  .state-chip {
    font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: .04em;
    border: var(--hairline) solid var(--border); border-radius: var(--radius-pill);
    padding: 0 var(--space-4); color: var(--ink-2); white-space: nowrap;
  }
  /* One risk encoding per row: number, band as text, meter - together in one cell, said once. */
  .risk-cell { display: inline-flex; align-items: center; gap: var(--space-4); }
  /* A quiet group label inside the scroll, so the band stays legible once the header has gone.
     Deliberately the footer ground and the smallest step on the scale: it is structure, not a
     row and not a banner. */
  .band-boundary td {
    font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: .04em;
    color: var(--ink-2); background: var(--footer-bg); padding-block: var(--space-2);
  }
  /* The selected row. Outline inside the row's own box so it never shifts the grid, and the
     identity wash beneath it so the selection survives losing focus to the detail beside it.
     TWO steps, not one: --id-wash here and --id-wash-soft on :hover above. They used to be the
     same --accent-wash, which meant an analyst arrowing down a queue saw the hovered row and
     the selected row paint an identical ground, told apart only by this outline. */
  /* The outline is the SLATE, not --id-text, and for the same reason the wash is: a claret
     hairline drawn round one row of a table whose whole business is severity reads as "this row is
     critical", and the row it lands on most often IS the top-scored one. Caught in the 2026-09-05
     adjacency screenshot. The slate says "selected" and can say nothing else - nothing else in
     this table carries a coloured border. 3.62:1 on --surface-1 light and 7.61:1 dark, over the
     3:1 SC 1.4.11 bar for a non-text indicator. The focus ring stays --id-text: it is transient,
     it is 2px, and PRODUCT.md commits the identity to the focus ring by name. */
  tr.selected {
    outline: var(--hairline) solid var(--id-slate);
    outline-offset: calc(-1 * var(--hairline));
    background: var(--id-wash);
  }

  /* ---- Queue memory (2026-09-05, Plan C Task 3) ----------------------------------------------
     A row the team has already judged. MUTED, never hidden and never re-banded: hiding is how a
     team forgets a merchant, and the band chip still says what the merchant IS. The mute is the
     row's ink stepping back one level, so a judged row is quieter than its neighbours and still
     perfectly readable — this is emphasis, not removal. The label beside it is the redundancy that
     keeps this off colour alone.

     Re-measured 2026-09-05 against the identity wash, which replaced --accent-wash as the row
     ground: --ink-2 is 7.81:1 on --surface-1, 6.78:1 on a selected row, 7.28:1 on a hovered row
     and 6.89:1 on --footer-bg in the light theme; 9.43 / 8.39 / 8.92 / 9.89 in the dark one.
     --ink-3 remains the wrong choice for the mute even though it now clears the floor (4.70:1 on
     a light selected row, 4.56:1 on a dark one): a mute with 0.06 of headroom is a mute that the
     next wash change breaks, and this row is the one an analyst reads longest. */
  tr.judged td { color: var(--ink-2); }
  tr.judged .m-name, tr.judged .score-num { font-weight: 500; }
  /* Selection outranks judgement: a judged row the analyst has open is still the row they are
     reading, and its identity wash must survive the mute. */
  tr.judged.selected td { color: var(--ink-1); }
  /* The judgment label on the Signals cell's second line. It takes .m-loc's size and colour (the
     established secondary-line pair in this table) and adds only the truncation, because a long
     workspace name must not push the cell's own chips into a second row. */
  .judged-label { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Where you stopped. Its own full-basis row in the queue header, like .queue-controls and for the
     same reason: wrapped into the heading's flex line it would drag the heading off the top of the
     card. Quiet, because it is memory rather than news. */
  .queue-resume {
    flex-basis: 100%; display: flex; align-items: baseline; gap: var(--space-4);
    margin-top: var(--space-4); font-size: var(--text-xs); color: var(--ink-2);
  }
  /* .cell-btn is the reset that makes a button look like the text it replaces; the accent and the
     underline are the affordance, never colour alone.

     Scoped to its parent deliberately: .cell-btn is declared LATER in this file, so a bare
     .resume-btn loses the colour on equal specificity and the control rendered in the line's own
     muted ink. Measured on screen before the fix. Two classes beat one whatever the order. */
  .queue-resume .resume-btn {
    color: var(--id-text); text-decoration: underline; text-underline-offset: 2px;
  }
  /* Standard sortable-header affordance: the whole label is the button and a plain arrow says
     which way. The reset makes the button look like the <th> it replaces - and because it strips
     the UA focus ring with everything else, the ring is put back explicitly.

     The CELL's padding moves onto the button so the whole header cell is the target. `all: unset`
     makes a button `display: inline`, which left these 16px tall - inside a 33px cell whose padding
     was not part of the hit area, and under WCAG 2.2 SC 2.5.8's 24px floor (2026-09-04 review, M5).
     box-sizing has to be restated because `all: unset` resets that too. text-align is inherited, so
     `th.num`'s right alignment reaches the button without a rule of its own. */
  th.sortable { padding: 0; }
  th.sortable button {
    all: unset; cursor: pointer; font: inherit; color: inherit; letter-spacing: inherit;
    display: block; box-sizing: border-box; width: 100%;
    padding: var(--space-4) var(--space-8);
  }
  th.sortable button:focus-visible { outline: 2px solid var(--id-text); outline-offset: calc(-1 * var(--hairline)); }
  /* Spacing only. The glyph itself is markup (SortArrow.razor) so it stays out of the header's
     accessible name; generated content does not. */
  .sort-arrow { margin-left: var(--space-2); }
  table { width: 100%; border-collapse: collapse; }
  thead th {
    text-align: left; font-size: var(--text-2xs); font-weight: 600; letter-spacing: .05em;
    text-transform: uppercase; color: var(--ink-2);
    padding: var(--space-4) var(--space-8); border-bottom: 1px solid var(--grid);
  }
  tbody td { padding: var(--space-6) var(--space-8); border-bottom: 1px solid var(--grid); font-size: var(--text-sm); vertical-align: middle; }
  tbody tr[data-mid] { cursor: pointer; }
  /* :not(.selected) is load-bearing now that hover and selection are two DIFFERENT washes.
     `tbody tr[data-mid]:hover` out-specifies `tr.selected` (0,2,2 against 0,1,1), so without
     this a selected row that the pointer happens to rest on painted the SOFT wash - hovering
     the row you have open made it lighter, which is backwards. It did not matter while both
     rules named the same --accent-wash. Measured in Chromium 2026-09-05. */
  tbody tr[data-mid]:not(.selected):hover { background: var(--id-wash-soft); }
  tbody tr:last-child td { border-bottom: none; }
  td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
  /* A table cell whose identifier opens a drawer. The existing pattern puts role="button" on the
     <tr>, which only works when the WHOLE row means one thing; a shopper's kiosk row has its own
     drill-in in one cell, so the control has to be the cell. Nothing existing expresses "a button
     that inherits the text it replaces" — .tog-btn is the nearest and carries margin-left:auto,
     which would shove the MID to the right of its column.

     IT MUST STAY ABOVE THE CELL CLASSES IT COMPOSES WITH. It is a reset: `font: inherit` and
     `color: inherit` exist so a bare .cell-btn looks like its cell, and they tie on specificity
     with .m-name, .mid-code and .mono-id. Below them it silently WINS those ties and strips the
     monospace off a MID and the weight off a name — defined classes, defeated, which reads as an
     unstyled cell next to correctly styled siblings. The :hover rule can live anywhere, since a
     pseudo-class outranks a plain class either way; it stays here for legibility.
     The affordance is the underline and the focus ring, never colour alone. */
  .cell-btn {
    border: none; background: transparent; padding: 0; margin: 0;
    font: inherit; color: inherit; letter-spacing: inherit;
    text-align: left; cursor: pointer;
  }
  .cell-btn:hover { color: var(--id-text); text-decoration: underline; text-underline-offset: 2px; }
  .m-name { font-weight: 600; }
  .m-loc { font-size: var(--text-2xs); color: var(--ink-2); margin-top: var(--hairline); }
  .mid-code { font-size: var(--text-2xs); color: var(--ink-3); font-family: var(--font-mono); }
  .score-wrap { display: flex; align-items: center; gap: var(--space-4); justify-content: flex-end; }
  .score-num { font-weight: 650; font-variant-numeric: tabular-nums; min-width: 22px; text-align: right; }
  .score-track { display: block; width: 64px; height: 6px; border-radius: var(--radius-pill); background: var(--grid); overflow: hidden; flex: none; }
  .score-fill { display: block; height: 100%; border-radius: var(--radius-pill); }
  .sev {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-2xs); font-weight: 600; padding: var(--space-2) var(--space-4); border-radius: var(--radius-pill);
    border: 1px solid var(--border); white-space: nowrap;
  }
  .sev .ico { display: inline-flex; align-items: center; }
  .sev-critical { color: var(--critical-text); background: color-mix(in srgb, var(--status-critical) 13%, transparent); border-color: color-mix(in srgb, var(--status-critical) 35%, transparent); font-weight: 700; }
  .sev-high    { color: var(--critical-text); background: color-mix(in srgb, var(--status-critical) 9%, transparent); }
  .sev-medium  { color: var(--serious-text);  background: color-mix(in srgb, var(--status-serious) 10%, transparent); }
  .sig-chip {
    display: inline-block; font-size: var(--text-3xs); color: var(--ink-2);
    background: color-mix(in srgb, var(--ink-3) 10%, transparent);
    border-radius: var(--radius-sm); padding: var(--space-1) var(--space-4); margin: var(--hairline) var(--space-1) var(--hairline) 0; white-space: nowrap;
  }
  .deep-dot { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-2xs); color: var(--accent-text); font-weight: 600; }
  .deep-dot::before { content: ""; width: 7px; height: 7px; border-radius: var(--radius-pill); background: var(--series-1); }
  /* The lead state's counterpart glyph: the same 7px pill, hollow. It exists as the second channel
     beside the word - the Case column's two values are a state, and a state that is only ever a
     word reads as a note. Until 2026-09-05 it existed because the words were visually hidden when
     the queue shared its width, and the column then measured 1px of "Lead" at 1220: the word is
     kept at every width now and the glyph stays beside it.
     The ring is currentColor, NOT --axis as the design sketch had it: a glyph that is the second
     channel for the Case state has to clear WCAG 2.2 1.4.11's 3:1,
     and --axis is the chart-gridline token, which is meant to disappear. It measures 1.77:1 on
     --surface-1, 1.62:1 on a SELECTED row and 1.69:1 on a HOVERED one in light, and 1.44 / 1.30 /
     1.37 in dark. Those two row grounds are --id-wash and --id-wash-soft, the slate washes; the
     single --accent-wash they replaced is gone. currentColor resolves to the --ink-2 on the rule
     below, which is the colour of the word "Lead" this ring stands in for, so the two can never
     disagree: 6.96 / 6.40 / 6.68 on the same three grounds in light, 9.42 / 8.49 / 8.96 in dark.
     The .t-meta went with it, which also settles a 1px mismatch — "Lead" was --text-xs beside
     "Case open" at --text-2xs in the same column. Figures re-measured 2026-09-05 against the
     palette; the table is in docs/design-system.md, "Measured contrast — the queue's hollow
     .lead-dot". */
  .lead-dot { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-2xs); color: var(--ink-2); }
  .lead-dot::before {
    content: ""; width: 7px; height: 7px; border-radius: var(--radius-pill);
    border: var(--hairline) solid currentColor;
  }
  /* ---------- Business vantage ---------- */
  .biz-head { display: flex; align-items: flex-start; gap: var(--space-8); margin-bottom: var(--space-13); flex-wrap: wrap; }
  .biz-head .card { flex: 1; min-width: 260px; display: flex; align-items: center; gap: var(--space-8); }
  .biz-id-block .bh-name { font-size: var(--text-lg); font-weight: 650; }
  .biz-id-block .bh-meta { font-size: var(--text-xs); color: var(--ink-2); margin-top: var(--space-2); }
  .bh-score { margin-left: auto; text-align: right; }
  .bh-score .n { font-size: var(--display-md); font-weight: 700; letter-spacing: -.02em; line-height: 1; }
  .bh-score .l { font-size: var(--text-3xs); color: var(--ink-2); margin-top: var(--space-2); }
  /* ---------- Drawer ---------- */
  .overlay { position: fixed; inset: 0; background: color-mix(in oklab, var(--shadow-ink) 32%, transparent); opacity: 0; pointer-events: none; transition: opacity var(--dur-base) var(--ease-out); z-index: 40; }
  .overlay.open { opacity: 1; pointer-events: auto; }
  .drawer {
    position: fixed; top: 0; right: 0; height: 100vh; width: min(560px, 94vw);
    background: var(--surface-1); border-left: 1px solid var(--border);
    box-shadow: -8px 0 30px color-mix(in oklab, var(--shadow-ink) 15%, transparent);
    transform: translateX(102%); transition: transform var(--dur-slow) var(--ease-out); z-index: 50;
    overflow-y: auto; padding: var(--space-10) var(--space-13) var(--space-20);
  }
  .drawer.open { transform: translateX(0); }
  /* The rail that keeps the close button in the scrollport. Zero height and the button hanging
     out of it, so the exit scrolls WITH the sticky identity band instead of away with the content
     and still costs no vertical space - nothing below it moved when this replaced the absolutely
     positioned button. -6px puts it where the old `top: 14px` did: the scrollport's sticky
     rectangle starts inside the drawer's own --space-10 padding, so -6 is 14 from the border box.
     z-index above the band (1), which it deliberately paints over. */
  .drawer-bar { position: sticky; top: calc(-1 * var(--space-3)); z-index: 2; height: 0; display: flex; justify-content: flex-end; }
  .drawer-close {
    border: 1px solid var(--border); background: var(--surface-1); color: var(--ink-2);
    border-radius: var(--radius-md); width: 30px; height: 30px; cursor: pointer;
    display: grid; place-items: center;
  }
  .d-mid { font-size: var(--text-2xs); color: var(--ink-3); font-family: var(--font-mono); margin-bottom: var(--space-2); }
  .d-name { font-size: var(--text-lg); font-weight: 650; padding-right: var(--space-20); }
  .d-meta { font-size: var(--text-xs); color: var(--ink-2); margin-top: var(--space-2); margin-bottom: var(--space-8); }
  .d-scorebar { display: flex; align-items: center; gap: var(--space-10); padding: var(--space-10); border: 1px solid var(--border); border-radius: var(--radius-xl); margin-bottom: var(--space-8); }
  .d-score-big { font-size: var(--display-lg); font-weight: 700; letter-spacing: -.02em; line-height: 1; font-variant-numeric: tabular-nums; }
  .d-score-lab { font-size: var(--text-2xs); color: var(--ink-3); margin-top: var(--space-2); }
  .d-meter { flex: 1; }
  .d-meter .score-track { width: 100%; height: 8px; }
  .d-section { font-size: var(--text-xs); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-3); margin: var(--space-13) 0 var(--space-4); }
  .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: var(--space-4); }
  .stat { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-6); }
  .stat .k { font-size: var(--text-3xs); color: var(--ink-3); }
  .stat .v { font-size: var(--text-lg); font-weight: 650; margin-top: var(--space-2); font-variant-numeric: tabular-nums; }
  .signal-row { display: flex; gap: var(--space-4); padding: var(--space-5) 0; border-bottom: 1px solid var(--grid); align-items: flex-start; }
  .signal-row:last-child { border-bottom: none; }
  .signal-ico { flex: none; width: 20px; text-align: center; }
  .signal-name { font-weight: 600; font-size: var(--text-xs); }
  .signal-detail { font-size: var(--text-xs); color: var(--ink-2); margin-top: var(--space-1); line-height: 1.45; }
  /* A prose callout: explanation, not a reading and not an action. It was on --accent-wash, the
     chart blue, which made it the loudest thing on the Rules page and the last surface that still
     disagreed with the shell. Neutral rather than the identity, because the identity is the rail,
     the primary action and the focus ring, and a paragraph of prose is none of those.
     --footer-bg rather than --surface-1: a note sits INSIDE a --surface-1 panel, where a
     --surface-1 ground would leave only the hairline to say where the callout starts. --footer-bg
     is a step below --surface-1 in BOTH themes, so the callout reads as recessed either way. */
  .note {
    background: var(--footer-bg); border: var(--hairline) solid var(--border); border-radius: var(--radius-lg);
    padding: var(--space-8) var(--space-10); font-size: var(--text-xs); line-height: 1.55; color: var(--ink-2);
    max-width: var(--measure);
  }
  .note b { font-weight: 650; color: var(--ink-1); }
  /* .t-muted is --ink-2, which is now the note's own ink, so `class="note t-muted"` - fifteen call
     sites - would have become indistinguishable from a plain note. The muted variant steps one
     further to --ink-3, a pair TokenContrastTests already holds at 4.5:1 on --footer-bg in both
     themes. Scoped to .note so the general utility keeps its meaning everywhere else. */
  .note.t-muted { color: var(--ink-3); }
  .dna {
    display: flex; align-items: baseline; gap: var(--space-4);
    font-size: var(--text-xs); color: var(--ink-2); line-height: 1.5;
    border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-6); margin-bottom: var(--space-8);
  }
  .dna-tag {
    flex: none; font-size: var(--text-3xs); font-weight: 700; letter-spacing: .08em;
    color: var(--accent-text); background: var(--accent-wash);
    border: 1px solid var(--border); border-radius: var(--radius-xs); padding: var(--space-1) var(--space-3);
  }
  /* ---------- DNA Lab ---------- */
  .lab-chips { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-13); }
  .lab-chip {
    display: flex; align-items: center; gap: var(--space-4);
    border: 1px solid var(--border); background: var(--surface-1); color: var(--ink-1);
    border-radius: var(--radius-lg); padding: var(--space-4) var(--space-6); cursor: pointer; font-size: var(--text-xs); font-weight: 600;
    box-shadow: var(--shadow);
  }
  .lab-chip .n { font-variant-numeric: tabular-nums; font-weight: 700; }
  /* The border is the SLATE, matching tr.selected and the checked workspace card: one persistent
     selected state, one colour. The TEXT stays --id-text, which is what says "the identity is
     here" without a second border colour competing with the queue's. */
  .lab-chip.active { border-color: var(--id-slate); background: var(--id-wash); color: var(--id-text); }
  .verdict-tag {
    display: inline-block; font-size: var(--text-3xs); font-weight: 700; letter-spacing: .07em;
    border-radius: var(--radius-xs); padding: var(--space-1) var(--space-4); border: 1px solid var(--border);
  }
  .feat-table { width: 100%; font-size: var(--text-xs); }
  .feat-table th { font-size: var(--text-3xs); padding: var(--space-3) var(--space-5); }
  .feat-table td { padding: var(--space-4) var(--space-5); font-size: var(--text-xs); }
  .feat-dev { font-weight: 650; }
  .legend { display: flex; gap: var(--space-8); font-size: var(--text-2xs); color: var(--ink-2); margin-bottom: var(--space-4); }
  .legend .key { display: inline-flex; align-items: center; gap: var(--space-3); }
  .legend .swatch { width: 14px; height: 3px; border-radius: var(--radius-xs); }
  .formula {
    font-size: var(--text-2xs); color: var(--ink-2); line-height: 1.65;
    border-top: 1px solid var(--grid); margin-top: var(--space-10); padding-top: var(--space-8);
    max-width: var(--measure);
  }
  .rule-toggle { display: inline-block; width: 30px; height: 17px; border-radius: var(--radius-pill); position: relative; flex: none; border: 1px solid var(--border); vertical-align: -3px; }
  .rule-toggle::after { content: ""; position: absolute; top: 1.5px; width: 12px; height: 12px; border-radius: var(--radius-pill); background: var(--knob); box-shadow: 0 1px 2px color-mix(in oklab, var(--shadow-ink) 25%, transparent); }
  .rule-toggle.on { background: var(--status-good); } .rule-toggle.on::after { right: 2px; }
  .rule-toggle.off { background: var(--grid); } .rule-toggle.off::after { left: 2px; }
  /* ---------- Settings ---------- */
  /* align-items: start -- a two-column grid stretches its tracks to the tallest row by default,
     so a short field sitting beside Organization's taller one inherited ~250px of dead space it
     never asked for. Each field sizes to its own content instead. */
  .set-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); align-items: start; }
  @media (max-width: 980px) { .set-grid { grid-template-columns: 1fr; } }
  .field { margin-bottom: var(--space-8); }
  .field label { display: block; font-size: var(--text-2xs); font-weight: 600; color: var(--ink-2); margin-bottom: var(--space-2); }
  .field input, .field select {
    width: 100%; background: var(--page); border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--space-4) var(--space-6); font-size: var(--text-sm); color: var(--ink-1);
  }
  .field input:focus, .field select:focus { border-color: var(--id-text); outline: none; box-shadow: 0 0 0 3px var(--id-wash); }
  .field .hint { font-size: var(--text-3xs); color: var(--ink-3); margin-top: var(--space-3); max-width: var(--measure); }
  /* The label block must be allowed to shrink and wrap, or a longer explanation at the
     larger type scale runs flush into the control on its right. */
  .tog-row { display: flex; align-items: center; gap: var(--space-8); padding: var(--space-6) 0; border-bottom: 1px solid var(--grid); }
  .tog-row > :first-child { flex: 1 1 auto; min-width: 0; }
  .tog-row:last-child { border-bottom: none; }
  .tog-row .t-lab { font-size: var(--text-xs); font-weight: 600; }
  .tog-row .t-sub { font-size: var(--text-2xs); color: var(--ink-2); margin-top: var(--hairline); }
  .tog-btn { margin-left: auto; cursor: pointer; border: none; background: transparent; padding: 0; }
  .save-bar { display: flex; gap: var(--space-5); align-items: center; margin-top: var(--space-13); }
  .badge-locked { font-size: var(--text-3xs); font-weight: 700; letter-spacing: .05em; color: var(--ink-3); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: var(--space-1) var(--space-3); }
  .sib-table { width: 100%; font-size: var(--text-xs); }
  .sib-table th { font-size: var(--text-3xs); padding: var(--space-3) var(--space-4); }
  .sib-table td { padding: var(--space-4); font-size: var(--text-xs); }
  .cta {
    display: block; width: 100%; text-align: center; margin-top: var(--space-8);
    background: var(--id-fill); color: var(--id-fill-ink); border: none; border-radius: var(--radius-lg);
    padding: var(--space-5); font-size: var(--text-sm); font-weight: 600; cursor: pointer;
  }
  /* Secondary is an OUTLINE in --border, not a second coloured button (spec 5.1). A row of two
     saturated buttons asks the analyst to choose between two equal-looking things, which is the
     one thing the Actions row exists not to do, so the outline is the quiet neutral and the
     identity survives only in the label. */
  .cta.ghost { background: transparent; color: var(--id-text); border: var(--hairline) solid var(--border); }
  /* ---------- Tooltip / toast / footer ---------- */
  .tt {
    position: fixed; z-index: 90; pointer-events: none;
    background: var(--ink-1); color: var(--page);
    border-radius: var(--radius-md); padding: var(--space-4) var(--space-5); font-size: var(--text-xs); line-height: 1.45;
    box-shadow: var(--shadow); opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); max-width: 240px;
  }
  .tt b { font-weight: 650; }
  .toast {
    position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(8px);
    background: var(--ink-1); color: var(--page); font-size: var(--text-xs);
    padding: var(--space-4) var(--space-8); border-radius: var(--radius-lg); opacity: 0; transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); z-index: 95; pointer-events: none;
    max-width: min(560px, 90vw); text-align: center;
  }
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
  /* The Undo action is the only route back from a mis-filed disposition. */
  .toast-action {
    margin-left: var(--space-6); background: transparent; border: var(--hairline) solid currentColor;
    color: inherit; border-radius: var(--radius-sm); padding: var(--space-1) var(--space-4);
    font-size: var(--text-2xs); cursor: pointer; pointer-events: auto;
    min-height: var(--target-min); min-width: var(--target-min);
  }
  .disclaimer { font-size: var(--text-2xs); color: var(--ink-2); line-height: 1.6; margin-top: var(--space-13); max-width: var(--measure); }
  /* Progressive disclosure. Teaching copy that is true but not needed while you work
     lives behind this rather than permanently on screen; see NetworkFooter.razor for
     which copy is allowed to go behind it and which is not. */
  .disclosure { max-width: var(--measure); margin-top: var(--space-8); }
  .disclosure > summary {
    display: inline-flex; align-items: center; gap: var(--space-3);
    font-size: var(--text-2xs); font-weight: var(--weight-medium); color: var(--id-text);
    padding: var(--space-2) var(--space-3) var(--space-2) 0;
    border-radius: var(--radius-sm); cursor: pointer; list-style: none;
  }
  .disclosure > summary::-webkit-details-marker { display: none; }
  .disclosure .disclaimer:first-of-type { margin-top: var(--space-6); }

  /* ---- The disclosure marker, once (2026-09-04) -------------------------------------------
     One shape for every <details> in the console: DisclosureChevron.razor, drawn in the same
     SVG vocabulary as the workspace caret and the vantage arrow. It replaced a "\203A" text
     character here, a CSS-border triangle on .glance, and the browser's own marker on
     .assistant-trace - three shapes for one gesture, two of which were the only non-SVG icons
     left in the app.

     The rotation is on the OPEN state of the <details>, so it works for all three hosts without
     any of them knowing about the others. */
  .disclosure-chevron {
    flex: none; vertical-align: -1px;
    transition: transform var(--dur-base) var(--ease-out);
  }
  details[open] > summary .disclosure-chevron { transform: rotate(90deg); }
  .glance summary,
  .assistant-trace summary { display: flex; align-items: center; gap: var(--space-3); }
  .glance summary::-webkit-details-marker,
  .assistant-trace summary::-webkit-details-marker { display: none; }
  .assistant-trace summary { list-style: none; }

  .network-strip {
    display: flex; gap: var(--space-10); flex-wrap: wrap; align-items: baseline;
    background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-xl);
    padding: var(--space-8) var(--space-13); box-shadow: var(--shadow); margin-top: var(--space-13);
  }
  .network-strip .ns-title { font-size: var(--text-xs); font-weight: 650; color: var(--ink-2); }
  .network-strip .ns-item { font-size: var(--text-xs); color: var(--ink-2); }
  .network-strip .ns-item b { color: var(--ink-1); font-weight: 650; }
  /* One group per SOURCE. The strip mixes figures that came from the live screen with figures
     that came from the captured directory; each group leads with a small provenance word so the
     reader never has to guess which is which (2026-09-03). Text tokens, not fill tokens: these
     read as text. */
  .ns-group { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-4) var(--space-6); }
  .ns-prov { font-size: var(--text-3xs); color: var(--ink-3); font-weight: 650; text-transform: uppercase; }
  .ns-prov-live { color: var(--good-text); }
  .estate-facts dt .ns-prov { margin-left: var(--space-3); text-transform: none; font-weight: 400; }

/* ---------- Blazor-specific additions (not in the prototype) ---------- */
/* NavLink emits href-matched <a class="active">, which the .nav a.active rule above
   already styles — nothing extra needed for nav state. */
#blazor-error-ui {
  position: fixed; bottom: 0; left: 0; width: 100%; z-index: 1000; display: none;
  background: var(--status-warn); color: var(--ink-on-fill);
  padding: var(--space-5) var(--space-10); font-size: var(--text-xs);
  box-shadow: 0 -1px 6px color-mix(in oklab, var(--shadow-ink) 20%, transparent);
}
#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 14px; top: 8px; }
/* The prototype is a single document; Blazor swaps <main> content per route, so the
   view container needs the same min-width:0 the prototype's #view inherits. */
.view { min-width: 0; }

/* The drawer is a vertical scroller only. Wide children (tables) get their own
   overflow-x container; without this a wide table pans the whole panel on a phone. */
.drawer { overflow-x: hidden; }

/* ============================================================================
   Utilities and semantic state classes.

   These exist to keep design decisions out of Razor `style=` attributes. The
   rule (CLAUDE.md): inline style is for DATA-DRIVEN values only — a meter width,
   a colour ramp computed from a score. Anything categorical (severity, band,
   disposition, priority) gets a class, so the palette lives in one file.
   ============================================================================ */

/* ---- Layout utilities ---- */
.scroll-x { overflow-x: auto; }          /* wide tables scroll themselves, never the page */
.push-right { margin-left: auto; }
.stack-sm { margin-top: var(--space-4); }
.stack-md { margin-top: var(--space-6); }
.stack-lg { margin-top: var(--space-8); }
/* A PAGE SECTION, not a block: the step a reader's eye resets on. Applied in Razor where a page
   genuinely changes subject, never as a bigger .stack-lg. */
.section-gap { margin-top: var(--space-24); }
.gap-below { margin-bottom: var(--space-10); }
.gap-below-sm { margin-bottom: var(--space-5); }
.min-w-0 { min-width: 0; }
.align-right { text-align: right; }

/* ---- Text roles ---- */
.t-muted { color: var(--ink-2); }
.t-meta { color: var(--ink-2); font-size: var(--text-xs); }
/* .t-meta is also a table cell's class, and a cell is not prose - so the cap lands on the PARAGRAPH
   form only. Two of the four p.t-meta instances computed to `max-width: none` and ran the full width
   of whatever held them (critique P1, 2026-09-05); the pane's are held by the pane, which is why
   only two of them showed it. */
p.t-meta { max-width: var(--measure); }
/* Next action is a sentence, not a word, and it sat in a column that shrank to whatever the
   narrowest neighbour allowed -- wrapping it to one word per line. A floor gives it room to read
   as a sentence; the ceiling keeps it from crowding out Owner and Shape on a narrow viewport. */
.cases-next-action { min-width: 28ch; max-width: 48ch; }
.t-micro { color: var(--ink-3); font-size: var(--text-3xs); }
.t-good { color: var(--good-text); }
.t-critical { color: var(--critical-text); }
.t-serious { color: var(--serious-text); }

/* Severity suffix beside a signal name — "· Critical" in a quieter voice. */
.signal-sev { font-weight: 400; color: var(--ink-3); font-size: var(--text-2xs); }

/* ---- Categorical state colour -----------------------------------------
   Named by MEANING, not by hue, so a palette change is a token change. */
.state-urgent { color: var(--critical-text); font-weight: var(--weight-medium); }
.state-watch { color: var(--serious-text); font-weight: var(--weight-medium); }
.state-clear { color: var(--good-text); font-weight: var(--weight-medium); }
.state-neutral { color: var(--ink-1); font-weight: var(--weight-medium); }
/* In-flight work reads as active; not-started reads as quiet. Both keep the medium
   weight the status column uses throughout. */
.state-progress { color: var(--accent-text); font-weight: var(--weight-medium); }
.state-idle { color: var(--ink-2); font-weight: var(--weight-medium); }

/* Score bands. The threshold logic stays in C#; the palette stays here. */
.score-critical { color: var(--critical-text); }
.score-serious { color: var(--serious-text); }
.score-warn { color: var(--status-warn); }

/* The score meter's track is tinted from the same band, so the two cannot
   disagree. The fill WIDTH is the only genuinely data-driven inline value. */
.score-track.score-critical { background: color-mix(in srgb, var(--status-critical) 18%, var(--surface-1)); }
.score-track.score-serious { background: color-mix(in srgb, var(--status-serious) 18%, var(--surface-1)); }
.score-track.score-warn { background: color-mix(in srgb, var(--status-warn) 18%, var(--surface-1)); }
.score-fill.score-critical { background: var(--status-critical); }
.score-fill.score-serious { background: var(--status-serious); }
.score-fill.score-warn { background: var(--status-warn); }

/* A tripwire row carries no DNA composite, so it has no band and no severity to paint. It takes
   the band chip's own band-tripwire class (see ScoreMeter): the track keeps the neutral --grid the
   bare .score-track rule gives it, and the fill is --ink-3, the same neutral ink
   .band-chip.band-tripwire reads in. The magnitude is still visible, the severity ramp is not
   available to it, and the meter cannot say a lead is Critical.
   --ink-3 against --grid is a non-text boundary and is asserted at WCAG 1.4.11's 3:1 in both
   themes by TokenContrastTests, not recorded as a figure here. */
.score-fill.band-tripwire { background: var(--ink-3); }

/* Azure Monitor rule severities run 0 (worst) to 4. Applied by interpolation
   (class="sig-chip azsev-@r.AzureSeverity"), so a text search for "azsev-1" finds
   no markup hit — these are used. */
.azsev-0, .azsev-1 { color: var(--critical-text); }
.azsev-2 { color: var(--serious-text); }
.azsev-3, .azsev-4 { color: var(--ink-2); }

/* ---- Composed bits that repeated verbatim ---- */
.detail-wide { max-width: 420px; white-space: normal; }
.detail-narrow { max-width: 340px; white-space: normal; }
.title-md { font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.value-lg { font-size: var(--display-xs); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
.value-xl { font-size: var(--display-md); font-weight: var(--weight-bold); line-height: 1; }
.empty-row { text-align: center; color: var(--ink-2); padding: var(--space-13); }
.chip-note { margin-left: var(--space-3); }
.formula-block { font-size: var(--text-xs); line-height: var(--leading-relaxed); color: var(--ink-1); max-width: var(--measure); }
.score-components { margin-left: auto; display: flex; gap: var(--space-10); align-items: center; flex-wrap: wrap; }
.score-headline { text-align: right; border-left: var(--hairline) solid var(--grid); padding-left: var(--space-10); }
.dismiss-backdrop { position: fixed; inset: 0; z-index: 55; }
.jira-link { color: var(--id-text); font-weight: var(--weight-medium); text-decoration: none; }
.jira-link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ---- Component pieces introduced when inline styles were removed ---- */
.rule-state {
  display: inline-flex; align-items: center; gap: var(--space-4);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
}
.cta-inline { width: auto; padding: var(--space-5) var(--space-13); margin-top: 0; }
.hint-block { font-size: var(--text-2xs); padding: var(--space-3) var(--space-1); }
.chip-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }


/* ---- Classes replacing the last of the inline styles ---- */
.dot-urgent { background: var(--status-critical); }
.dot-watch { background: var(--status-serious); }
.dot-default { background: var(--series-1); }

.swatch-baseline { background: var(--series-1); }
.swatch-current { background: var(--series-2); }

/* DNA bands. verdict-tag and band-chip both pick up a matching tinted background from the same
   class — one set of band colours, never redefined per surface. */
.band-urgent { color: var(--critical-text); }
.band-review { color: var(--serious-text); }
.band-normal { color: var(--good-text); }
.verdict-tag.band-urgent { background: color-mix(in srgb, var(--status-critical) 10%, transparent); }
.verdict-tag.band-review { background: color-mix(in srgb, var(--status-serious) 10%, transparent); }
.verdict-tag.band-normal { background: color-mix(in srgb, var(--status-good) 10%, transparent); }

/* The DNA band beside a queue score: text first (the word carries the meaning), tint second
   (redundant reinforcement, never the only signal — WCAG 2.2 AA). Same shape as .verdict-tag,
   used inline next to a number instead of standing alone in its own cell. A row with no DNA
   composite gets the bare .band-chip look (bordered, ink-3, no tint) via .band-tripwire, which
   adds nothing of its own — the absence of colour IS the "this is not a band" signal. */
.band-chip {
  display: inline-block; margin-left: var(--space-3); font-size: var(--text-3xs);
  font-weight: 700; letter-spacing: .07em; border-radius: var(--radius-xs);
  padding: var(--space-1) var(--space-4); border: 1px solid var(--border);
}
/* Colour lives on the compound selector, never on the bare .band-chip: a single-class .band-chip
   rule would tie with .band-urgent/.band-review/.band-normal at equal specificity and let source
   order (not the band) decide the winner — which is exactly the regression this shape avoids. */
.band-chip.band-tripwire { color: var(--ink-3); }
.band-chip.band-urgent { color: var(--critical-text); background: color-mix(in srgb, var(--status-critical) 10%, transparent); border-color: color-mix(in srgb, var(--status-critical) 35%, transparent); }
.band-chip.band-review { color: var(--serious-text); background: color-mix(in srgb, var(--status-serious) 10%, transparent); border-color: color-mix(in srgb, var(--status-serious) 35%, transparent); }
.band-chip.band-normal { color: var(--good-text); background: color-mix(in srgb, var(--status-good) 10%, transparent); border-color: color-mix(in srgb, var(--status-good) 35%, transparent); }

.lab-header,
.biz-header { display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap; }
.stack-xs { margin-top: var(--space-2); }
.t-dim { opacity: .7; }
.dna-inline { margin: var(--space-5) 0 0; }
.section-note { font-weight: 400; text-transform: none; letter-spacing: 0; }
/* The severity chip and the coverage label ("30-day investigation") share this row. It had
   space-between and NO gap, so in the pane - which is 34% of the viewport - the label had nowhere
   to go and wrapped to two lines pressed flat against the chip's pill, reading as one collided
   object rather than two facts. A gap and a deliberate wrap: with no room the label takes its own
   line under the chip, which is what the reader needs, and the meter below it gains a step so the
   track is not tight against whichever of the two ends lowest. */
.d-meter-head {
  display: flex; justify-content: space-between; align-items: baseline;
  flex-wrap: wrap; gap: var(--space-2) var(--space-6);
  font-size: var(--text-2xs); color: var(--ink-2); margin-bottom: var(--space-4);
}
.evidence-pulled { font-size: var(--text-2xs); color: var(--ink-2); margin-bottom: var(--space-4); }
.chip-row-spaced { margin: var(--space-5) 0; }
.t-code { font-size: var(--text-2xs); }

/* Two population panels, side by side above the evidence rows. The discriminators' working shown
   to an internal analyst: which population, what it was, and the numbers behind the verdict. */
.pop-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--space-8); margin-block: var(--space-10); }
.pop-panel { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8); }
.pop-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); }
.pop-name { font-weight: 600; }
/* The verdict is a WORD, never a colour alone (WCAG 2.2 AA, and CLAUDE.md's severity rule). */
.pop-verdict { font-size: var(--text-2xs); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; }
.pop-stats { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-8); margin-block-start: var(--space-4); color: var(--ink-2); font-size: var(--text-xs); }

/* A drafted outreach message, rendered for reading and editing rather than as a form field. The
   whitespace is preserved because the model writes paragraphs and they carry the structure. */
.draft-panel { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8); margin-block: var(--space-6); white-space: pre-wrap; }
/* A figure the evidence does not support. Underline AND a mark element, never colour alone: the
   analyst is about to send this to a merchant (WCAG 2.2 AA, and CLAUDE.md's severity rule). */
.draft-unverified { background: transparent; color: inherit; text-decoration: underline wavy; text-underline-offset: var(--space-1); font-weight: 600; }

/* The assistant panel's question row: field first, button never wider than its label needs. */
.assistant-ask { display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-4); }
.assistant-ask .top-btn { flex: 1; text-align: left; }
/* A collapsible trace section - the working or the reasoning behind an assistant answer. Closed
   by default (the <details> element itself carries that): the answer is what the analyst reads
   first, this is what they open to check it against its source. */
.assistant-trace { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8); margin-block: var(--space-6); }
.assistant-trace summary { cursor: pointer; font-size: var(--text-xs); font-weight: 600; color: var(--ink-2); }
.assistant-reasoning-line { font-size: var(--text-xs); color: var(--ink-2); line-height: 1.5; margin-top: var(--space-4); }
/* One block per tool call, in the order the loop actually made them. */
.assistant-step { border-top: 1px solid var(--grid); padding-block: var(--space-4); }
.assistant-step:first-child { border-top: none; padding-top: var(--space-2); }
.assistant-step-name { font-weight: 650; }
.assistant-step-args { margin-top: var(--space-1); font-family: var(--font-mono); }
.assistant-step-result {
  margin-top: var(--space-2); font-size: var(--text-2xs); font-family: var(--font-mono);
  white-space: pre-wrap; word-break: break-word; max-height: 12em; overflow-y: auto;
}

/* The link to a fired investigation's session. Underlined, not colour alone - it is the one
   affordance in this panel and it has to read as a link at a glance. break-word rather than
   break-all: a session URL is long enough to need wrapping and legible enough to be worth
   keeping readable when it does. */
.launch-link {
  display: inline-block; margin-block: var(--space-4); font-size: var(--text-xs);
  color: var(--id-text); text-decoration: underline; word-break: break-word;
}

/* One disposition block per OPEN CASE. A MID can carry two at once (a RetryStorm outreach case and
   a CardTesting payments case), so each block is bordered and names its own case: an analyst must
   never be able to file a durable action against a case whose identity they cannot see. */
.case-disp { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8); margin-block-start: var(--space-10); }
.case-disp-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); }
/* Groups the archetype with its CURRENT status so the pair reads as one unit at the head's start,
   leaving .case-disp-head's space-between free to still push the id to the far end. */
.case-disp-title { display: inline-flex; align-items: baseline; gap: var(--space-4); }
.case-disp-name { font-weight: 600; }
.case-disp-id { font-size: var(--text-2xs); color: var(--ink-3); }

/* The whole assertion a Save is about to make, in BODY type. It is the last thing read before a
   durable statement about a named merchant reaches every workspace on the case, so it is not a
   footnote — the visibility clause used to sit in t-micro, which put the one fact about who else
   sees it in the smallest type on the panel. */
.confirm-line { font-size: var(--text-sm); color: var(--ink-1); margin: var(--space-4) 0; max-width: var(--measure); }

/* The Actions row. One primary (.cta) and the rest ghost, so the row reads as a recommendation
   rather than a rack of equal buttons. Wraps because the drawer is narrower than the pane.

   The ROW owns the sizing, not the buttons: base .cta is a full-width block with a top margin
   (the login form's shape), which inside a flex row would split the width between two controls
   and add a stray gap above them. The button keeps its own appearance and the row decides that
   here they sit side by side at their natural width — the same override .cta-inline makes for a
   single control, without touching the class PrimaryClass computes. */
.md-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-block: var(--space-10); }
.md-actions .cta { width: auto; margin-top: 0; }
/* The judgment action, subordinate to the case actions beside it. `.cta.ghost` already carries the
   quiet treatment; this only pushes it to the end of the row, so the band's ONE recommendation is
   never the last thing the eye lands on. */
.md-actions .judged-action { margin-inline-start: auto; }


/* A measured traffic shape (WHEN, never WHAT — contract 3 is not an archetype). NotAssessed
   renders none of this at all; see MerchantDetail's `_history is { Shape: not NotAssessed }`
   guard. It is a plain subsection of the Baseline band now, not a bordered panel nested inside
   one, so the chip needs a row of its own and nothing needs a border. */
.shape-row { margin-block-start: var(--space-3); }

/* ---- The work surface -------------------------------------------------------------------------
   Master-detail: the queue on the left, the selected merchant's detail on the right, each
   scrolling on its own. One grid, always the same two columns above the breakpoint, so the
   analyst's eye learns one structure and stops hunting for the detail after every click.

   Below 1220px the grid is one column and the detail is the drawer instead - the same component
   in a different host, never a second layout with its own copy of the bands.

   1220px is the breakpoint the topbar already uses (measured; see docs/design-system.md), not a
   new one. 360px is the narrowest the five bands read at - the stat grid's own 118px tracks stop
   pairing below it - and it is a STRUCTURAL grid minimum, not a type size. */
/* minmax(0, 1fr) EVEN AT ONE COLUMN, and min-width:0 on the queue side. A grid item's implicit
   `auto` track sizes to MAX-CONTENT, so below the breakpoint the six-column queue table pushed the
   whole page 215px wider than a 900px viewport and the body scrolled sideways - the table's own
   .scroll-x can only clamp it once something above it is allowed to be narrower than its content.
   Measured in Chromium at 900px before the fix. */
.work-surface { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-13); }
.work-queue { min-width: 0; }
@media (min-width: 1220px) {
  .work-surface.wide { grid-template-columns: minmax(0, 1fr) minmax(360px, 34%); align-items: start; }
  /* Sticky to the viewport with its own scroll, so stepping down the queue with j never scrolls
     the detail out of reach. A quiet surface: one hairline and the panel ground, no shadow and no
     card padding of its own - the bands inside already carry their own rhythm, and a card here
     would be a card inside a card. */
  .work-pane {
    position: sticky; top: var(--space-10);
    max-height: calc(100vh - var(--space-20)); overflow-y: auto;
    border: var(--hairline) solid var(--border); border-radius: var(--radius-lg);
    /* The gutter matches a card's, because at this width the pane IS the reading column: the
       bands inside run prose and a stat grid, and --space-8 put both against the hairline. */
    background: var(--surface-1); padding: 0 var(--space-10);
  }
}
/* The pane is a scroll container the analyst can focus with Enter, so it needs the ring inside
   its own box rather than outside a sticky element. */
.work-pane:focus-visible { outline: 2px solid var(--id-text); outline-offset: -2px; }
/* The empty pane TEACHES the surface rather than saying "nothing here": what a selection gets you,
   and the keys that work the queue without the mouse. */
.pane-empty { padding-block: var(--space-20); }
.pane-empty .t-meta { display: block; margin-block-start: var(--space-6); }

/* ---- The queue's column budget when its track is narrow (2026-09-05, Plan C Tasks 5 + 5b) ------
   The Plan A walk found the queue behind horizontal scroll with the pane open, and merchant names
   wrapping to three lines. Six columns do not fit a track that has just given a third of itself
   away — and, as the 2026-09-05 ruling put it, THE TRACK IS WHAT MATTERS, not what narrowed it.
   Two different things narrow it:

     below 1220px   the VIEWPORT is narrow. There is no pane; the drawer hosts the detail, and its
                    identity band carries the processor, so the budget applies unconditionally.
     1220–1615px    the PANE is taking a third of the surface. Gated on `.pane-open`. Measured
                    2026-09-05: on the Overview that is true above the breakpoint whether or not a
                    row is selected, because the empty pane still reserves its track — the gate is
                    not there to catch a closed pane on THIS page, it is there so a queue on a
                    surface that declares no pane keeps every column.
     1616px and up  nothing is dropped in either case.

   Priority, best kept first: Merchant, DNA score, Driver, Signals, Case, Processor. Only the last
   is dropped, and its datum is one glance away in whichever host holds the detail — the pane's
   identity band or the drawer's. Nothing is hidden and nothing is encoded by colour.

   Measured in Chromium, snapshot mode, `.scroll-x` scrollWidth minus clientWidth — how much of the
   queue was unreachable without scrolling sideways:

     900px   track 640px, table wanted 752px  ->  114px over, now 0
     1220px  track 562px, table wanted 752px  ->  192px over, now 0
     1616px  track 861px, table wanted 859px  ->    0px over, unchanged

   The three shared declarations are stated once per RANGE rather than hoisted into one selector
   list, because the two conditions differ by viewport width and not by anything in the DOM: no
   selector can say "below 1220px". The alternative was a custom property toggled per range and
   read by one rule, which buys deduplication with a layer of indirection over `display` — worse to
   read than the repetition it removes.

   `.scroll-x` STAYS as the last resort. A fraud queue must never truncate silently: the goal is
   that the scrollbar does not engage, never that it is gone, and it is still there for the row of
   data wider than the fixture measured above.

   Below 1220px, where there is no pane, the three shared rules are scoped under `.work-queue` (the
   queue's own wrapper, and the only one on the surface) rather than on the bare class: `.risk-cell`
   is also MerchantHome's, and this budget is a fact about this table's track, not about risk cells
   everywhere. Between 1220 and 1615px the three corresponding rules are scoped under `.pane-open`
   instead — that is the class actually true in that range, and `.work-queue` would add nothing
   there. */

/* The Case column's floor (critique P1, 2026-09-05). At 1220 the cell measured 67px, the glyph 9px
   and span.case-label 1px: the word was in the accessible name and not on the screen.
   The arithmetic, so the number is not a guess. The longest label is "Case open", nine characters
   at --text-2xs (13px). This face draws a wide zero, so 1ch is 0.648em - 9.72px at the cell's own
   --text-sm - while running text in it averages about 0.501em, so nine characters at 13px is about
   58.6px. The glyph adds 7px and its --space-2 gap 4px: 69.6px, which is 7.16ch at the cell's font
   size. 7.5ch, for the rounding a browser does between characters.
   The space comes from Signals, which wraps its chips and degrades gracefully; auto table layout
   takes it from there because that is the most compressible column in the row. Scoped under
   .work-queue because this is a fact about THIS table's track. */
.work-queue .col-case { min-width: 7.5ch; }

/* Two lines, then an ellipsis. The full name is on the cell's title, in the row button's
   accessible name, and in the host's identity band. `-webkit-line-clamp` is the only spelling
   Chromium and WebKit implement; the standard `line-clamp` sits beside it so this stops being
   prefixed the day they ship it. Not in a media query: `.pane-open` only exists above the
   breakpoint, and the clamp is wanted at every width the pane is open at, 1616px included. */
.pane-open .name-clamp {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden;
}

/* Below the work surface's breakpoint: narrow because the viewport is, drawer hosting, no pane. */
@media (max-width: 1219px) {
  .work-queue .col-processor { display: none; }
  .work-queue .name-clamp {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; line-clamp: 2;
    overflow: hidden;
  }
  .work-queue .risk-cell { flex-wrap: wrap; row-gap: var(--space-2); }
  /* The Case column keeps its WORDS here, as it does at every width since 2026-09-05 (see the
     .col-case floor above). Dropping Processor and letting the risk cell wrap already takes the
     table to 537px of a 640px track. */
}

/* Above the breakpoint and below 1616px: narrow only while the pane is taking a third of it. */
@media (min-width: 1220px) and (max-width: 1615px) {
  .pane-open .col-processor { display: none; }
  /* Dropping Processor is not enough on its own. Measured at 1220px, where the pane sits on its
     360px floor and leaves the queue a 562px track: the five remaining columns still ask for 628px,
     and the DNA score cell is 210px of it — the widest column in the table, for one number said
     three ways (the figure, the band as text, the meter). So the cell is allowed to WRAP when it is
     squeezed: the meter takes its own line under the number and its band, nothing is dropped and no
     encoding is lost. Above 1615px the column has the room and the cell stays on one line. */
  .pane-open .risk-cell { flex-wrap: wrap; row-gap: var(--space-2); }
}

/* ---- Screen at a glance -----------------------------------------------------------------------
   The tiles, the histogram and the signals card, below the queue and visibly secondary: they were
   competing with the queue for the analyst's first look, and the queue is the job. Open by
   default, because the figures are still worth having - collapsing them is the analyst's choice,
   not the page's. */
/* --space-24, the page-section step: this is the one boundary on the Overview where the analyst
   stops reading the queue and starts reading figures about it, and 32px did not say so. */
.glance { margin-block-start: var(--space-24); }
.glance summary { cursor: pointer; list-style: none; }
.glance summary::-webkit-details-marker { display: none; }
/* The marker is DisclosureChevron.razor, the same one every other disclosure in the app uses.
   It used to be a triangle built out of CSS borders - the only non-SVG icon left in the console,
   at a different size and colour from every drawn glyph beside it. */
.glance summary h2 { font-size: var(--text-lg); font-weight: 650; }
/* The chevron on a section heading is a heading-sized target, so it takes the heading's ink
   rather than the accent a prose disclosure uses. */
.glance summary .disclosure-chevron { color: var(--ink-2); }
.glance-grid {
  display: grid; gap: var(--space-10); margin-block-start: var(--space-10);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); align-items: start;
}
/* A chart needs more than a tile's track. Two tracks each above the breakpoint, so the four tiles
   take one row and the two cards the next - still ONE grid, which is what keeps their tops
   aligned. 280px is the narrowest an axis with four labels reads at: structural, not type. */
@media (min-width: 1220px) {
  .glance-grid > .card { grid-column: span 2; }
}
/* The four KPI figures keep their own auto-fit row - they want 180px tracks and a chart wants
   280px+ - and span every track of the glance grid so the charts start on a line of their own. */
.glance-grid > .tiles { grid-column: 1 / -1; margin-bottom: 0; }
/* Inside the glance the tiles are not the page's own headline figures any more, so they drop the
   shadow that lifted them off the ground and keep the hairline only. */
.glance-grid .tile { box-shadow: none; }

/* ---- Merchant detail bands -------------------------------------------------------------------
   Five bands with real headings (Identity and score, Baseline, Evidence, Actions, Record),
   replacing fifteen whisper-weight uppercase labels that gave the drawer no hierarchy at all.

   Hierarchy comes from SCALE AND WEIGHT, not from boxes: one hairline rule between bands, no
   border or fill on the bands themselves, and no card nested inside a card. Spacing differs by
   band - identity is tight because it is a header, the rest breathe - rather than one padding
   applied everywhere. */
/* THE BANDS ARE NOT ALL THE SAME (2026-09-04 breathing-room pass). Before this, every band ran on
   one --space-10 and every subsection inside one ran on --space-8, so the five-band structure the
   design intends was not legible: at a squint the band headings sat at the same interval as the
   subsection labels beneath them and the whole detail read as one column of text.

   The ladder now, and each step earns its place:
     --space-4   a heading to the content it names
     --space-10  a subsection to the next INSIDE a band (.md-band h4)
     --space-16  a band to the next
     --space-24  a band to EVIDENCE, and below both EVIDENCE and ACTIONS
   Identity is tighter than any of them because it is a header, not a band of content.
   ACTIONS opens at --space-13 instead; the reason is with the rule itself, below. */
.md-band { padding-block: var(--space-13); border-top: var(--hairline) solid var(--border); }
.md-band:first-of-type { border-top: 0; }
/* Where the hairline actually shows. The first band has no rule above it and needs no clearance. */
.md-band + .md-band { padding-block-start: var(--space-16); }
/* Evidence is the transaction rows that justify the case; Actions is what the analyst is about to
   do about it. They are the two bands a reader stops at, so they get the page-section step above
   and below rather than the band step - the eye resets before reading either. Specificity ties
   with .md-band + .md-band and wins on source order, which is why these sit after it. */
/* Evidence opens with the page-section step, same as every plain band-to-band boundary gets on
   its OWN closing edge below. Actions does not repeat it on its opening edge: Evidence already
   closes at the page-section step (below), and stacking that step on both sides of the one
   boundary between them made Evidence -> Actions 96px -- the largest gap in the detail, bigger
   than either of its neighbours, for no reason tied to what the two bands mean. Actions' opening
   is instead --space-13, so Evidence -> Actions comes out even with Baseline -> Evidence: identity
   tight, then even, then a slightly larger breath before Record (2026-09-04 fix). */
.md-band + .md-band-evidence { padding-block-start: var(--space-24); }
.md-band + .md-band-actions { padding-block-start: var(--space-13); }
.md-band-evidence,
.md-band-actions { padding-block-end: var(--space-24); }
.md-band h3 { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--ink-1); margin: 0 0 var(--space-8); }
.md-band h4 { font-size: var(--text-2xs); font-weight: var(--weight-medium); color: var(--ink-2); margin: var(--space-10) 0 var(--space-4); }

/* The assistant's answer, split at the breaks the model wrote. Paragraph spacing is what makes a
   four-section answer readable at all: as one block it was skimmed and abandoned.

   MUST stay after .md-band h4, which it deliberately overrides at equal specificity. The band's
   h4 is a whisper-weight UI label for a subsection ("Ask the assistant"); the headings INSIDE the
   answer are structure within prose, so at that size and ink they would have read smaller than
   the body text beneath them. Same size as the paragraph, heavier, full ink. */
.assistant-answer { max-width: var(--measure); }
.assistant-answer p { margin: 0 0 var(--space-6); }
.assistant-answer h4 {
  font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-1);
  margin: var(--space-8) 0 var(--space-3);
}
.assistant-answer > :last-child { margin-block-end: 0; }

/* Identity stays put: scroll into the evidence rows and the MID and score they belong to must not
   leave the screen. */
/* The band is the one place the identity appears as a GROUND inside the work surface (spec 5.1).
   Two background layers, not one: --id-wash is a translucent mix, and a translucent sticky header
   lets the evidence rows scroll straight through it. The wash rides on --surface-1 as a one-stop
   gradient rather than being color-mix'd against it, so the pane and the drawer (whose grounds
   differ) both composite correctly without a second token. */
.md-identity {
  position: sticky; top: 0; z-index: 1;
  padding-block: var(--space-6) var(--space-10);
  background-color: var(--surface-1);
  background-image: linear-gradient(var(--id-wash), var(--id-wash));
}
/* In the drawer the scrollport is .drawer, which carries its own top padding, so sticking to the
   padding edge would let content scroll through the gap above the band. */
.md-drawer .md-identity { top: calc(-1 * var(--space-10)); padding-block-start: var(--space-10); }
/* The drawer's close button is absolutely positioned in this corner and paints above the band, so
   the row has to keep out of it: without this it clips the Next button and steals 4px of a control
   that CLOSES the dialog instead of stepping the queue. Drawer only - the pane has no close
   button, and reserving the corner there would just push the controls off the edge. */
.md-drawer .md-identity-row { padding-inline-end: var(--space-13); }
.md-identity-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-6); margin-block-end: var(--space-4); }
/* The merchant's name is the h2 (the subject of every band below), so it needs the heading margin
   cleared and none of .d-name's clearance for the absolutely positioned close button - the step
   controls occupy that corner of the row now. */
.md-title { margin: 0; }
.md-identity .d-name { padding-right: 0; }
.md-step { display: flex; gap: var(--space-2); }

/* A subsection heading with a control on its own baseline (the 30d / 7d window switch). */
.md-subhead { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-6); }
.md-subhead h4 { margin-block-end: 0; }

/* The shape chip: the WORD carries the meaning (WCAG 2.2 AA — never colour alone), the tint is
   redundancy only. Fill and text each get their OWN value of the same hue per the design-system
   rule, matching the .sev-* idiom this mirrors. */
.shape-chip {
  display: inline-flex; align-items: center; font-weight: 600; text-transform: lowercase;
  font-size: var(--text-2xs); padding: var(--space-2) var(--space-4); border-radius: var(--radius-pill);
  border: 1px solid var(--border);
}
.shape-burst     { color: var(--critical-text); background: color-mix(in srgb, var(--status-critical) 13%, transparent); border-color: color-mix(in srgb, var(--status-critical) 35%, transparent); }
.shape-episodic  { color: var(--serious-text);  background: color-mix(in srgb, var(--status-serious) 10%, transparent); }
.shape-sustained { color: var(--good-text);     background: color-mix(in srgb, var(--status-good) 10%, transparent); }

/* Bar height is the only data-driven value (a percentage of the peak day); everything else here —
   width, gap, colour, radius — is a design decision and stays in this class per CLAUDE.md's rule. */
.spark { display: flex; align-items: flex-end; gap: var(--space-1); height: var(--space-20); margin-block-start: var(--space-6); }
.spark-bar { flex: 1; min-width: 2px; background: var(--series-1); border-radius: var(--radius-xs); }

/* Rows that open the entity drawer. The prototype set cursor inline on sibling rows;
   tbody tr[data-mid] already covers the queue tables. */
.row-click { cursor: pointer; }
.row-click:hover { background: var(--id-wash-soft); }
/* STILL LIVE, unlike the tbody tr[data-mid] variant deleted beside it: Lanes and PartnerHome
   render focusable role="button" rows, and this is what keeps their focus ring inside the row's
   own box instead of clipped by the scroll container. */
.row-click:focus-visible { outline: 2px solid var(--id-text); outline-offset: -2px; }

/* SVG numeric labels. Charts are string-built, so this replaces an inline
   font-variant-numeric on every axis label. */
.svg-nums { font-variant-numeric: tabular-nums; }
/* The hand-rolled SVG charts used to carry their own font-size="..." attributes -- 9.5, 10, 10.5,
   11.5 -- none of them a token, several of them under the 11px floor. These two classes are the
   type scale's chart-facing slots: axis labels, band names and gridline numbers are quieter and
   smaller than an in-chart count, which earns the reader's eye and needs the weight to hold it. */
.svg-label { font-size: var(--text-3xs); fill: var(--ink-3); }
.svg-value { font-size: var(--text-2xs); fill: var(--ink-2); font-weight: var(--weight-medium); }
/* A modifier, not a new size: the histogram's band name sits at the same 11px as the tier/range
   line under it, just heavier, because it's what an analyst reads first. font-weight is a
   design decision the same as font-size or fill, so it belongs here too, not inline on the
   <text> element. */
.svg-label-strong { font-weight: var(--weight-medium); }

/* ============================================================================
   Motion, states, and browser surfaces.

   MOTION THESIS
   Operate surface: motion serves feedback, state and continuity. Nothing here
   makes an analyst wait, and there is no page-load choreography.

   - Focal moment: the entity drawer opening. This is the product's central
     gesture — a screen hit becoming a finding — so the score meter FILLS from
     zero rather than appearing already full. The fill is the act of
     measurement, and it points the eye at the number that decides the action.
     Runs once per open.
   - Continuity: the 30d/7d toggle swaps a dozen figures at once, so the stat
     grid settles to signal that the numbers changed rather than leaving the
     reader to wonder.
   - Feedback: rows, chips, buttons, and the live-read busy state.
   - Everything else stays still, on purpose.
   ============================================================================ */

/* ---- Focal moment: the drawer ---- */
.drawer.open .score-fill {
  animation: meter-fill var(--dur-slow) var(--ease-out) both;
}
@keyframes meter-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
.score-fill { transform-origin: left center; }

/* ---- Continuity: the window toggle changed the numbers ---- */
.stat-grid { animation: settle var(--dur-base) var(--ease-out) both; }
@keyframes settle {
  from { opacity: .5; transform: translateY(-2px); }
  to { opacity: 1; transform: none; }
}

/* ---- Feedback: interactive states ---- */
tbody tr[data-mid],
.row-click { transition: background var(--dur-fast) var(--ease-out); }

.top-btn,
.chip,
.cta,
.lab-chip,
.vantage-btn { transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }

.top-btn:active,
.cta:active,
.vantage-btn:active,
.lab-chip:active { transform: translateY(0.5px); }

/* The primary darkens a step of its OWN colour. It mixed toward the literal keyword `black`
   before, which is the pure neutral spec section 2 retired and the only one the hex-and-rgba
   grep in TokenContrastTests could not see. Mixing toward the theme's own shadow ink keeps the
   hover one step of one colour in both themes. */
.cta:hover { background: color-mix(in oklab, var(--id-fill) 88%, var(--shadow-ink)); }
/* Two cues, not one: the ghost's whole affordance is its outline, so the hover strengthens the
   outline as well as laying down a wash. */
.cta.ghost:hover { background: var(--id-wash-soft); border-color: var(--id-text); }
.lab-chip:hover:not(.active) { background: var(--id-wash); }
.drawer-close:hover { background: var(--id-wash); color: var(--ink-1); }

/* Disabled — the live-read button spends real time in this state. */
.top-btn:disabled,
.cta:disabled,
.chip:disabled {
  opacity: .55;
  cursor: default;
  pointer-events: none;
}
/* A busy control should read as busy, not merely dimmed. */
.top-btn:disabled .ic { animation: spin 900ms linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Busy via [aria-busy="true"] rather than :disabled — the vantage button (Topbar) stays clickable
   while its switcher data loads (opening it shows "Loading vantage points", not a dead control),
   and the sign-in submit (login.js) is disabled separately but needs the same visible "still
   working" read on the button itself, not just on an .ic icon neither of them has. One rule for
   both, tokens only, no new colour. */
.vantage-btn[aria-busy="true"],
.cta[aria-busy="true"] {
  opacity: .7;
}

/* Focus. 2.2 AA wants focus visibly distinct; these are small controls in dense
   tables, so the ring is drawn outside the element where there is room. */
.top-btn:focus-visible,
.chip:focus-visible,
.cta:focus-visible,
.lab-chip:focus-visible,
.vantage-btn:focus-visible,
.tog-btn:focus-visible,
.cell-btn:focus-visible,
.drawer-close:focus-visible,
.vpop button.vi:focus-visible {
  outline: 2px solid var(--id-text);
  outline-offset: 2px;
}
/* On the LIGHT theme's identity ink, --id-text measures 1.37:1 and the ring is invisible.
   Anything focusable inside the rail, the sign-in panel or the report masthead takes the accent,
   which is 5.45:1 there in light and 10.20:1 in dark. Dark's --id-text would in fact clear the
   ink on its own (9.55:1, because dark lifts it to a pale rose) - the override is a light-theme
   fix that does no harm in dark, and one rule for both is better than a theme-scoped ring.
   Re-measured 2026-09-05 with the Datacap palette. The three rules inside the rail that spell
   their own `outline` shorthand (.workspace-switch, .workspace-signout button, .tour-restart)
   name --id-accent directly rather than relying on this one to win a specificity tie. */
.sidebar :focus-visible,
.auth-pitch :focus-visible,
.report-masthead :focus-visible {
  outline-color: var(--id-accent);
}
.field input:focus-visible,
.field select:focus-visible,
.search:focus-visible { outline: none; }   /* these already show a ring via box-shadow */

/* ---- Loading and empty states ---- */
.loading {
  color: var(--ink-2);
  font-size: var(--text-sm);
  padding: var(--space-13) 0;
  display: flex; align-items: center; gap: var(--space-4);
}
.loading::before {
  content: "";
  width: 12px; height: 12px; flex: none;
  border: 2px solid color-mix(in srgb, var(--series-1) 30%, transparent);
  border-top-color: var(--series-1);
  border-radius: var(--radius-pill);
  animation: spin 700ms linear infinite;
}
.empty-state {
  padding: var(--space-13);
  text-align: center;
  color: var(--ink-2);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  max-width: var(--measure);
  margin-inline: auto;
}
.empty-state b { color: var(--ink-1); font-weight: var(--weight-medium); }

/* ---- Staged loading: a checklist that names the stage, not a spinner over content.
   The one loader for every long read (product register: skeletons, not spinners). ---- */
.staged { display: grid; gap: var(--space-8); }
/* Both slots are grid tracks whether or not they hold anything, and an empty track still takes
   its gap: dead space above the checklist before the first band reveals, and below it once the
   last band has and no skeletons are left. */
.staged-revealed:empty, .staged-skeletons:empty { display: none; }
/* A read that STOPPED is not a read in progress. The stage list stays, because which stage it
   stopped at is the useful part, but the current mark must not keep spinning under a failure
   notice -- a spinner says "still working". The colour only reinforces the notice's own glyph and
   heading; it is not carrying the message on its own. */
.staged-failed .stage-current .stage-mark { animation: none; border-color: var(--status-serious); }
.stage-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); max-width: var(--measure); }
.stage-list li { display: flex; align-items: center; gap: var(--space-4); font-size: var(--text-2xs); color: var(--ink-2); }
.stage-mark { width: var(--space-6); height: var(--space-6); border-radius: var(--radius-pill); border: var(--hairline) solid var(--axis); flex: none; }
.stage-done .stage-mark { background: var(--good-text); border-color: var(--good-text); }
.stage-done .stage-label { color: var(--ink-1); }
.stage-current .stage-mark { border-color: var(--series-1); border-top-color: transparent; animation: spin 700ms linear infinite; }
/* Pending stages are differentiated by the empty mark, not by dimmer ink -- explicit so this
   does not silently depend on .stage-list li's base colour. This stays at --ink-2 rather than
   reaching for --ink-3 on compositional grounds (an empty mark says "not yet"; a fainter label
   would be a second, redundant signal for the same fact), not because of a contrast gap -- the
   2026-09-04 pass moved --ink-3 to clear --page and --footer-bg too, so that gap is closed; see
   "Measured contrast" in docs/design-system.md. */
.stage-list li.stage-pending { color: var(--ink-2); }
.staged-expect { margin: 0; }
.skeleton-band {
  height: var(--space-20); border-radius: var(--radius-md); margin-block-start: var(--space-6);
  /* --footer-bg is tuned to sit close to --page, i.e. quiet chrome -- against --surface-1 that
     step is imperceptible in dark mode. color-mix off --ink-1 keeps the shimmer visible in both
     themes without a new token. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--ink-1) 6%, var(--surface-1)),
    var(--surface-1),
    color-mix(in srgb, var(--ink-1) 6%, var(--surface-1)));
  background-size: 200% 100%; animation: shimmer 1200ms var(--ease-out) infinite;
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---- Browser surfaces the craft floor calls out ---- */
.tt {
  /* Positioned by tooltip.js; the .tt base rule above carries the look. */
  pointer-events: none;
}
.tt.show { opacity: 1; }
a { text-underline-offset: 2px; }

/* ---- Reduced motion: honour it for everything, not just transitions ---- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
  .drawer.open .score-fill,
  .stat-grid { animation: none; }
  .top-btn:disabled .ic,
  .loading::before,
  .skeleton-band,
  .stage-current .stage-mark { animation: none; }
}

/* Blazor's built-in error banner. It ships unstyled, so in dark mode its link fell back
   to the browser default (#9e9eff, 2.35:1). This is the surface shown when something has
   already failed; it should not also be illegible. */
#blazor-error-ui {
  background: var(--status-warn);
  color: var(--ink-on-fill);
}
#blazor-error-ui a,
#blazor-error-ui .reload {
  color: var(--ink-on-fill);
  font-weight: var(--weight-medium);
  text-decoration: underline;
  text-underline-offset: 2px;
}
#blazor-error-ui .dismiss { color: var(--ink-on-fill); }

/* ---- Fleet sweep -------------------------------------------------------
   Retail-abuse detection, deliberately NOT styled as the fraud queue: no severity
   chips, no case language. It reuses the score palette so the app still looks like
   one product, but a band here is a commercial finding, not a risk rating. */
.mono-id {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .02em;
}
.sweep-mascot {
  font-size: var(--display-md);
  line-height: 1;
  margin-bottom: var(--space-4);
  /* The one flourish on the page, and only in an empty state — where there is
     nothing to read and a moment of personality costs the operator nothing. */
  animation: mascot-spark 2.4s var(--ease-out) infinite;
}
@keyframes mascot-spark {
  0%, 92%, 100% { transform: none; opacity: .9; }
  95% { transform: rotate(-8deg) scale(1.08); opacity: 1; }
  97% { transform: rotate(6deg) scale(1.04); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .sweep-mascot { animation: none; } }

/* ---- Partner branding -------------------------------------------------
   A partner sees their own mark in the sidebar. Constrained hard so an arbitrary
   remote image cannot break the layout. */
.partner-brand {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-4) var(--space-4) 0;
  margin-top: var(--space-4);
  border-top: var(--hairline) solid color-mix(in oklab, var(--id-ink-hi) 12%, transparent);
}
.partner-brand img {
  max-width: 92px; max-height: 28px; width: auto; height: auto;
  object-fit: contain; flex: none;
}
.partner-brand .pb-label {
  font-size: var(--text-3xs); color: var(--sidebar-ink); opacity: .75; line-height: 1.35;
}

/* ---- Sign-in: split composition ----------------------------------------
   Its own surface: there is no workspace yet, so there is no shell to draw.
   Claim on one side, form on the other. The claim side is the SAME identity ink as the
   nav rail rather than a new marketing palette, so the first screen and the product are
   visibly one thing. This is the identity's one DRENCHED surface (spec 5.2): the console
   is paper with an inked rail, and the panel a visitor meets first is the inverse.

   Deliberately not here: gradient text, a glass card, an eyebrow above the heading.
   Emphasis comes from size and weight, and the one italic is a real semantic stress
   ("your own" is the whole argument), not decoration. */
.auth {
  min-height: 100vh;
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
}
.auth-pitch {
  background: var(--id-ink);
  color: var(--id-ink-hi);
  display: flex;
  align-items: center;
  padding: var(--space-20) clamp(var(--space-20), 6vw, var(--space-40));
  /* Light-on-dark type reads as lighter weight and needs the room (spec section 3). */
  line-height: calc(var(--leading-normal) + 0.05);
  /* Both of these exist ONLY to contain the glow below, and go if it ever goes again. */
  position: relative;
  overflow: hidden;
  /* The same boundary as the rail's, on the second of the three surfaces that carry the identity.
     In dark the panel and the form ground measured 1.14:1 apart and the composition's one
     structural move disappeared (critique P1, the sign-in file). */
  border-inline-end: var(--hairline) solid var(--id-slate);
}
/* One soft light source, from behind the claim. Not a texture, not a pattern.
   Task 6 removed this, reading spec 5.2's "no imagery is fabricated" as covering the light source
   too. Ed, watching the branch on 2026-09-05: "I really liked the original faded blue gradient on
   the home page instead of the green." It is back, at the pre-branch geometry exactly, and the
   only change is that the colour is a token: --id-glow is the original #3987e5 stated in OKLCH,
   mixed to 30% the way the original rgba() was. The blue is the one blue left in the identity
   family and it lives on this panel and nowhere else. */
.auth-pitch::before {
  content: "";
  position: absolute;
  inset: -30% -40% auto -20%;
  height: 90%;
  background: radial-gradient(closest-side, color-mix(in oklab, var(--id-glow) 30%, transparent), transparent);
  pointer-events: none;
}
/* `position: relative`, restored with the glow and load-bearing: an absolutely positioned
   ::before paints above non-positioned in-flow content, so without this the glow would sit over
   the claim rather than behind it. */
.auth-pitch-inner { position: relative; max-width: 38rem; }

/* The mark and the wordmark, both inheriting the panel's ink. Centred rather than
   baseline-aligned now: the wordmark is a two-line stack, so a baseline would hang the mark off
   the name's baseline and leave the attribution dangling below it. The Datacap logo that used to
   lead this block is in the footer; its plate rule went with it.

   THE ONE REVEAL IN THE WHOLE PRODUCT (spec 5.4). It runs once, on the surface a visitor sees
   before they have anything to do, and it is 360ms of opacity and an 8px rise - not a
   choreography. `both` fill mode is load-bearing: without it the block paints at full opacity for
   one frame before the animation's 0% takes hold, which reads as a flicker rather than a reveal. */
.auth-brand {
  display: flex; align-items: center; gap: var(--space-5);
  margin-bottom: var(--space-20); color: var(--id-ink-hi);
  animation: auth-reveal 360ms var(--ease-out) both;
}

@keyframes auth-reveal {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Ruling on the pairing Task 4 flagged: the 24-unit mark IS optically light beside the large
   wordmark, because the wordmark at --text-xl over a --text-2xs attribution line measures a
   44.5px stack and the mark is two 2.4px strokes and a dot. The drawing stays at 24 units - the same
   drawing as the rail's, per Mark.razor - and only its BOX steps up to --space-16 (32px), the
   spacing rung that lands nearest the cap height of the name beside it. A spacing token rather
   than a new px value, and rather than a second Size on the component, which would have made the
   panel a third drawing to keep in step. */
.auth-brand .mark { width: var(--space-16); height: var(--space-16); }

.auth-claim {
  font-size: clamp(var(--display-md), 4.4vw, var(--display-xl));
  line-height: 1.08;
  letter-spacing: -0.02em;
  font-weight: var(--weight-bold);
  color: var(--id-ink-hi);
  text-wrap: balance;
}
.auth-claim em { font-style: italic; color: var(--id-accent); }

/* One line under the claim, set as a quiet caption rather than body copy: it makes the argument
   in plain words and gets out of the way. The DNA definition folded back into this sentence
   2026-09-10 (owner ruling: a separate auth-def paragraph was one line too many for a sign-in
   page) - which brings back the exact bug that split them apart in the first place. Quiet by
   COLOUR rather than by a group `opacity`, and that is not a preference: opacity composites the
   whole subtree, so the accent on "DNA" nested inside would paint at .85 of its own value - the
   auth-def paragraph this replaces measured its own version of that at .78 and found 4.50:1 on
   the light ink, under the 4.5 AA floor PRODUCT.md sets, for a 13px bold run. Mixing the ink's
   high tone toward the ground instead gives the paragraph the same recessive weight and leaves
   the nested run free to state its own colour at full strength. */
.auth-lede {
  margin-top: var(--space-13);
  font-size: var(--text-sm);
  letter-spacing: .04em;
  color: color-mix(in oklab, var(--id-ink-hi) 85%, var(--id-ink));
  max-width: var(--measure);
}
.auth-lede b { color: var(--id-accent); font-weight: var(--weight-bold); letter-spacing: .08em; }

.auth-form-side {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-20) var(--space-16);
  background: var(--surface-1);
}
.auth-card { width: min(360px, 100%); }
.auth-form-title {
  font-size: var(--display-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
  margin-bottom: var(--space-16);
}
.login-form .field { margin-bottom: var(--space-10); }
.login-form .cta { width: 100%; justify-content: center; margin-top: var(--space-8); }

/* The sign-in form as two quiet sections: the merchant lookup first, the workspace fallback
   second. Each choice carries its own description now, so nothing here needs a detached hint. */
.auth-section { margin-bottom: var(--space-13); }
.auth-section-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--ink-1); margin: 0 0 var(--space-2); }
.auth-section-lede { font-size: var(--text-2xs); color: var(--ink-2); margin: 0 0 var(--space-6); max-width: var(--measure); }
.mid-input { text-transform: uppercase; letter-spacing: .04em; }
.auth-choices { display: grid; gap: var(--space-3); }
.auth-choice { display: flex; gap: var(--space-4); align-items: flex-start; padding: var(--space-4) var(--space-5); border: var(--hairline) solid var(--border); border-radius: var(--radius-md); cursor: pointer; min-height: var(--target-min); }
/* The one identity touch on the paper side: the same border/wash pair the selected queue row
   uses, rather than a third selected-state colour - and since 2026-09-05 that means the SLATE,
   because tr.selected's outline moved there and a claret border here left one state speaking two
   colours a click apart. It was --accent-*, the chart blue, before either of those, which is the
   two-colour-system problem on a control. The `:has()` deferral recorded in docs/design-system.md
   stands: no fallback, and a browser without it shows the native radio only, which is not broken. */
.auth-choice:has(input:checked) { border-color: var(--id-slate); background: var(--id-wash); }
/* DRAWN, not tinted (critique P1, 2026-09-05). The native control with only `accent-color` set
   painted backwards in dark: `color-scheme: dark` gives the UNCHECKED radio a solid mid-grey disc
   and the checked one a thin ring around a dark centre, so the three wrong answers carried more ink
   than the right one and the two themes disagreed about which shape means chosen. This is the
   page's actual decision - for the internal analyst the pre-checked default IS the interaction -
   and a glance that returns the wrong answer is worse than no affordance.
   The size is 1em at the label step, so 13px, which is the size the user agent drew and the size
   the critique measured. Stated as type rather than as a px literal because that is what it is: the
   control is sized to the label beside it. The card's slate wash and border stay as the second,
   redundant channel, and the click target is still the whole card - the <label> wraps the input. */
.auth-choice input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  font-size: var(--text-2xs);
  width: 1em;
  height: 1em;
  flex: none;
  margin-top: var(--space-1);
  border: var(--hairline) solid var(--ink-3);
  border-radius: var(--radius-pill);
  background: var(--surface-1);
}
/* The checked state: a filled centre in the identity's paper-side red, with the card's own ground
   held as a ring between the fill and the border by an inset shadow. --id-text and not --id-fill,
   because --id-fill is the brand red in BOTH themes and measures 2.12:1 on the dark card ground -
   a state marker under the 3:1 SC 1.4.11 asks of one. --id-text is the token whose job is the
   identity as a mark on paper, and it is re-valued per theme: claret in light, a lifted rose in
   dark, 9.94:1 and 10.88:1 on their own grounds. */
.auth-choice input[type="radio"]:checked {
  background: var(--id-text);
  border-color: var(--id-text);
  box-shadow: inset 0 0 0 var(--space-1) var(--surface-1);
}
.auth-choice-text { display: grid; gap: var(--space-1); }
.auth-choice-name { font-size: var(--text-sm); color: var(--ink-1); }
.auth-choice-desc { font-size: var(--text-2xs); color: var(--ink-2); }

/* Stacked below the split's break point. The claim keeps its own ground and stays
   FIRST — it is the reason to sign in, and on a phone a login form with no context
   above it is just a wall. Type steps down so it does not become a page of its own. */
@media (max-width: 900px) {
  .auth { grid-template-columns: 1fr; }
  .auth-pitch { padding: var(--space-16) var(--space-10) var(--space-20); }
  /* Stacked, the panel sits above the form rather than beside it, so the boundary turns with it. */
  .auth-pitch { border-inline-end: 0; border-block-end: var(--hairline) solid var(--id-slate); }
  /* Stacked, the panel is short and wide, so the glow has to be re-aimed or it sits off the top
     of it entirely. The pre-branch values, restored with the layer. */
  .auth-pitch::before { inset: -60% -40% auto -30%; height: 140%; }
  .auth-brand { margin-bottom: var(--space-13); }
  .auth-claim { font-size: clamp(var(--display-sm), 7.5vw, var(--display-lg)); }
  .auth-lede { margin-top: var(--space-10); font-size: var(--text-xs); }
  .auth-form-side { padding: var(--space-16) var(--space-10) var(--space-20); }
}
.login-note, .login-error { font-size: var(--text-xs); margin-bottom: var(--space-10); }
.login-error {
  background: color-mix(in srgb, var(--status-critical) 10%, transparent);
  border-color: color-mix(in srgb, var(--status-critical) 35%, transparent);
  color: var(--critical-text);
}
.login-foot {
  margin-top: var(--space-13); padding-top: var(--space-8);
  border-top: var(--hairline) solid var(--grid);
  font-size: var(--text-3xs); color: var(--ink-3); line-height: var(--leading-normal);
  /* The vendor logo that sat here moved up to the hero (2026-09-10): the mark beside the
     wordmark IS the Datacap logo now, so this is text only, no second image. */
}

/* ---- Signed-in workspace, in the sidebar --------------------------------
   Account context belongs at the bottom of the nav rail, which is where a dashboard
   user looks for it — not in the topbar, which already carries the vantage switcher
   and would then hold two different "who/what am I looking at" controls. */
/* ---- Full-height nav rail ----------------------------------------------
   The rail was height:100vh + sticky, so on any page taller than the viewport it
   simply stopped and the page background showed through beneath it. Now it stretches
   to the document height (body is the flex container, so align-self does it) and the
   nav block sticks instead — continuous rail, navigation still reachable. */
/* The rail STRETCHES to the document (so the navy never stops mid-page) but its
   CONTENTS live in a sticky viewport-height column. Two separate jobs that an earlier
   version tried to do with one element:
     - .sidebar        — the continuous surface, as tall as the document
     - .sidebar-inner  — sticky, exactly 100vh, a flex column
   That is what keeps the signed-in workspace visible without scrolling: it is pinned to
   the bottom of the VIEWPORT, not parked at the end of a 3,000px rail. Navigation scrolls
   inside .sidebar-top if it ever outgrows the viewport; the workspace block never does. */
.sidebar {
  position: static;
  height: auto;
  align-self: stretch;
  min-height: 100vh;
}
.sidebar-inner {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  padding: var(--space-10) var(--space-6);
  overflow: hidden;
}
.sidebar-top {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

/* ---- Narrow-viewport shell (C6) -----------------------------------------
   Below 860px the rail becomes a horizontal top bar and the body grid goes single-column.
   This block MUST come after the sticky .sidebar/.sidebar-inner/.sidebar-top rules above:
   at equal specificity the later rule wins, and an earlier version of this block sat near
   the top of the file where the sticky rules silently overrode it — the rail kept a
   100vh minimum and an overflow-hidden sticky inner column, which is exactly open-findings
   C6 (page one screen down, nav clipped). ShellCascadeContractTests pins the ordering. */
@media (max-width: 860px) {
  body {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    grid-template-areas: "rail" "main" "footer";
  }
  /* The rail is a strip across the top here, so its boundary is the bottom edge. An inline-end
     hairline on a full-width bar would draw a 1px mark against the right edge of the viewport. */
  .sidebar { width: 100%; min-height: 0; border-inline-end: 0; border-block-end: var(--hairline) solid var(--id-slate); }
  .sidebar-inner {
    position: static; height: auto; overflow: visible;
    flex-direction: row; align-items: center; gap: var(--space-6);
    padding: var(--space-5) var(--space-8);
  }
  .sidebar-top {
    flex: 1 1 auto; min-width: 0; min-height: 0;
    display: flex; align-items: center; gap: var(--space-3);
    overflow: visible; overflow-y: visible;
  }
  .workspace { flex: none; margin: 0; padding: 0; max-width: 190px; }
  .workspace-signout { display: none; }
  .logo { padding: 0 var(--space-5) 0 0; }
  /* Was .logo-sub. The rail is a horizontal bar at this width and has one line of height for
     the identity block, so the attribution drops out of the RAIL only - the sign-in panel keeps
     it, which is why this is scoped to .logo rather than set on .wordmark-by. */
  .logo .wordmark-by { display: none; }
  .nav { flex-direction: row; margin-top: 0; overflow-x: auto; }
  .nav a { white-space: nowrap; }
  /* The rail is a horizontal strip here, so the indicator turns with it: a rule on the LEFT edge
     of one chip in a row of chips reads as a divider between two of them, not as a marker on one.
     Still inset, still 2px, still inside the item's own box. */
  .nav a.active { box-shadow: inset 0 -2px 0 0 var(--id-accent); }
  .sidebar-foot { display: none; }
  .main { padding: var(--space-8) var(--space-8) var(--space-16); }
  /* The pill's one-line default is a desktop decision; at narrow widths its ~390px nowrap line
     was the one thing pushing the whole document sideways (measured at 375px: body 407/375). */
  .env-pill { white-space: normal; }
}

/* Phone widths: the workspace block's 190px plus the logo left the nav strip ~10px wide inside
   the top bar (measured at 375px). Logo and workspace share the first row; navigation takes a
   full-width scrollable second row. */
@media (max-width: 480px) {
  .sidebar-inner { flex-wrap: wrap; row-gap: var(--space-3); }
  .sidebar-top { flex-wrap: wrap; }
  .nav { flex: 1 1 100%; min-width: 0; }
}

/* ---- Workspace control -------------------------------------------------
   One primary target (switch workspace) plus one quiet secondary (sign out). Two
   equal-weight buttons gave neither any hierarchy. */
.workspace {
  flex: none;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: var(--hairline) solid color-mix(in oklab, var(--id-ink-hi) 12%, transparent);
}
.workspace-switch {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3); border-radius: var(--radius-md);
  text-decoration: none; color: inherit;
  border: var(--hairline) solid color-mix(in oklab, var(--id-ink-hi) 12%, transparent);
  background: color-mix(in oklab, var(--id-ink-hi) 6%, transparent);
  transition: background var(--dur-fast) var(--ease-out);
}
.workspace-switch:hover { background: color-mix(in oklab, var(--id-ink-hi) 12%, transparent); }
.workspace-switch:focus-visible { outline: 2px solid var(--id-accent); outline-offset: 2px; }
.workspace-logo {
  width: 26px; height: 26px; flex: none; border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--id-ink-hi) 10%, transparent); object-fit: contain; padding: var(--space-1);
}
.workspace-initials {
  display: grid; place-items: center;
  font-size: var(--text-3xs); font-weight: var(--weight-bold); color: var(--id-ink-hi);
}
.workspace-text { min-width: 0; flex: 1; }
.workspace-name {
  display: block; font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--id-ink-hi);
  line-height: 1.25;
  /* WRAPS, never truncates. This was ellipsised on one line, and the 2026-08-22 type-scale
     increase pushed "Sonny's Enterprises" past the 208px rail — the workspace card read
     "Sonny's Enterpri…", which is the one string on the page that must not be guessed at.
     A workspace name is identity: two lines of it is right, half of it is not. */
  overflow-wrap: anywhere;
}
.workspace-kind { display: block; font-size: var(--text-3xs); color: var(--sidebar-ink); opacity: .8; }
.workspace-caret { flex: none; color: var(--sidebar-ink); opacity: .6; }
.workspace-signout { margin: var(--space-3) 0 0; }
.workspace-signout button {
  width: 100%; background: none; border: none; cursor: pointer; font-family: inherit;
  font-size: var(--text-3xs); color: var(--sidebar-ink); opacity: .75;
  padding: var(--space-2); border-radius: var(--radius-sm);
  min-height: var(--target-min);
}
.workspace-signout button:hover { opacity: 1; background: color-mix(in oklab, var(--id-ink-hi) 8%, transparent); }
.workspace-signout button:focus-visible { outline: 2px solid var(--id-accent); outline-offset: 2px; }
.tour-restart {
  width: 100%; margin-top: var(--space-3);
  background: transparent; color: var(--id-accent); border: 1px solid color-mix(in oklab, var(--id-accent) 45%, transparent);
  border-radius: var(--radius-md); padding: var(--space-2) var(--space-3);
  font-family: inherit; font-size: var(--text-3xs); font-weight: var(--weight-medium);
  cursor: pointer;
  min-height: var(--target-min);
}
.tour-restart:hover { background: color-mix(in oklab, var(--id-ink-hi) 8%, transparent); }
.tour-restart:focus-visible { outline: 2px solid var(--id-accent); outline-offset: 2px; }


/* ---- Lookback picker ----------------------------------------------------
   Reuses .chip-group so "how far back" looks the same everywhere. The label is a real
   label, not a placeholder inside the control: a range whose meaning you have to infer
   from its options is the kind of thing that gets misread on a screenshot. */
.window-picker {
  display: flex; align-items: center; gap: var(--space-8);
  flex-wrap: wrap; margin-bottom: var(--space-13);
}
.window-picker-label {
  font-size: var(--text-2xs); font-weight: var(--weight-bold);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
}
.window-picker .chip[disabled] { opacity: .5; cursor: progress; }

/* ---- Weight input (Settings) ---- */
.weight-input { display: flex; align-items: center; gap: var(--space-3); flex: none; }
.weight-input input {
  width: 4.5rem; text-align: right;
  font-size: var(--text-sm); font-family: inherit;
  color: var(--ink-1); background: var(--surface-1);
  border: var(--hairline) solid var(--border); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
}

/* ---- App footer -------------------------------------------------------------
   A real page footer: full-bleed across rail and content, on its own ground, below
   everything. It was previously the last block inside <main>, which made it look like
   a caption on the content rather than the end of the page. */
.app-footer {
  grid-area: footer;
  background: var(--footer-bg);
  border-top: var(--hairline) solid var(--footer-line);
  padding: var(--space-10) var(--space-13);
  font-size: var(--text-2xs);
  color: var(--ink-2);
}
@media (max-width: 860px) {
  .app-footer { padding: var(--space-8); }
}
.af-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.af-brand { font-weight: var(--weight-medium); color: var(--ink-1); }
.af-dim { color: var(--ink-2); font-weight: 400; }
.af-sep { color: var(--ink-3); }
.af-spacer { flex: 1; min-width: var(--space-8); }
.af-fine { margin-top: var(--space-3); color: var(--ink-3); font-size: var(--text-3xs); }

/* ---- Onboarding tour ------------------------------------------------------
   The scrim genuinely blocks the page, which is why the card is a real dialog.
   Colour is never the only signal here: progress is stated as text. */
.tour-scrim {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.55);
    z-index: 900;
}

.tour-card {
    position: fixed;
    z-index: 901;
    width: 320px;
    max-width: calc(100vw - var(--space-4));
    background: var(--surface-1);
    color: var(--ink-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
    box-shadow: var(--shadow);
}

.tour-progress {
    margin: 0 0 var(--space-1);
    font-size: var(--text-xs);
    color: var(--ink-2);
}

.tour-title { margin: 0 0 var(--space-2); font-size: var(--text-md); }
.tour-body  { margin: 0 0 var(--space-4); font-size: var(--text-sm); color: var(--ink-2); }

.tour-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.tour-advance { display: flex; gap: var(--space-2); }

/* Skip is no harder to reach than Next. */
.tour-card button {
    min-height: var(--target-min);
    min-width: var(--target-min);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    cursor: pointer;
}

/* The tour's primary, and it takes the SAME pair as .cta: one primary-action
   vocabulary across the product, not a navy button on the tour and a red one
   everywhere else. */
.tour-next { background: var(--id-fill); color: var(--id-fill-ink); border: none; }
.tour-back,
.tour-skip { background: transparent; color: var(--id-text); border: var(--hairline) solid var(--border); }

@media (prefers-reduced-motion: no-preference) {
    .tour-card { transition: top var(--dur-base) var(--ease-out), left var(--dur-base) var(--ease-out); }
}

/* ---- A page that could not load -------------------------------------------
   Built on .empty-state because it is the same job — a surface with no data on
   it — with one difference that matters: an empty state is a fact about the
   data, this is a fault. The glyph and the heading carry that; the colour only
   reinforces it, because severity must never be colour alone. */
.unavailable { color: var(--ink-2); }
.unavailable b { color: var(--serious-text); }
.unavailable-glyph {
  width: 20px;
  height: 20px;
  display: block;
  margin: 0 auto var(--space-4);
  color: var(--status-serious);
}
.unavailable-guidance {
  margin: var(--space-4) auto 0;
  max-width: 46ch;
  line-height: var(--leading-relaxed);
}
/* The retry that belongs to an unavailable notice. The notice above it is centred on its own
   measure, so a block-level .cta stretched to the panel edge reads as a control belonging to the
   panel rather than as the answer to the sentence explaining why it is there. */
.unavailable-retry { width: fit-content; margin-inline: auto; }

/* ---- Partner landing: verdict-first, calm, single-column-first ---- */
.verdict-block { margin: var(--space-13) 0 var(--space-24); }
.verdict {
    font-size: var(--display-sm);           /* the 28 slot — the page's largest type */
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
}
.verdict-sub {
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
    max-width: var(--measure);
    margin-top: var(--space-4);
}
.estate-rest { margin-top: var(--space-24); max-width: var(--measure); }
.estate-rest h3 { font-size: var(--text-lg); margin-bottom: var(--space-8); }
.estate-facts { display: flex; flex-wrap: wrap; gap: var(--space-10) var(--space-16); margin: 0 0 var(--space-10); }
.estate-facts dt { font-size: var(--text-3xs); color: var(--ink-2); }
.estate-facts dd { font-size: var(--text-lg); font-weight: var(--weight-medium); margin: 0; }
/* The at-rest prose. These are one-line facts in <p> elements with the global margin reset, so
   four cohort comparisons ran together as a four-line wall and the provenance line was the fifth
   line of it. A step between the facts, and a larger one before the provenance - which is a
   different KIND of statement (WHEN this was read, not WHAT it says). */
.estate-rest p { line-height: var(--leading-normal); }
.estate-rest p + p { margin-top: var(--space-3); }
.estate-rest > p:last-child { margin-top: var(--space-8); }
.landing-lookup { margin-top: var(--space-24); }
.landing-lookup form { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center; }

/* ---- Merchant landing to-do items ---- */
.todo-item { padding: var(--space-10) 0; border-top: var(--hairline) solid var(--grid); }
.todo-item:first-of-type { border-top: none; }
.todo-do { font-size: var(--text-lg); font-weight: var(--weight-medium); margin: var(--space-4) 0 0; }
.todo-why { font-size: var(--text-sm); max-width: var(--measure); margin: var(--space-2) 0 0; }
.todo-own { margin: var(--space-2) 0 var(--space-4); }

/* ---- Merchant "Your lane's DNA" band (spec 6.4) ---- */
.lane-dna { padding: var(--space-10) 0; border-top: var(--hairline) solid var(--grid); }
.lane-dna:first-of-type { border-top: none; }
.lane-dna .mid-code { display: block; }
.lane-dna .risk-cell { margin: var(--space-4) 0; }
.lane-dna p { max-width: var(--measure); margin: var(--space-2) 0 0; }
.lane-dna p:first-of-type { margin-top: var(--space-4); }

/* ---- The DNA report, on screen and on paper -----------------------------
   The report is the one surface in this product that has to work for a reader who never opened
   the console: a partner's risk owner opening a forwarded document in a back office. So it is a
   DOCUMENT - one column at the prose measure, a masthead that says what this is and when it was
   true, and figures in tabular numerals because a column of numbers that does not line up reads
   as sloppy on paper in a way it does not on screen. */
.report { max-width: 60rem; margin-inline: auto; padding: var(--space-13); }

.report-masthead {
  display: flex; align-items: center; gap: var(--space-13);
  background: var(--id-ink); color: var(--id-ink-hi);
  padding: var(--space-13) var(--space-16);
  border-radius: var(--radius-lg);
  line-height: calc(var(--leading-normal) + 0.05);
}
.report-brand { display: flex; align-items: center; gap: var(--space-5); flex: none; }
.report-ident { min-width: 0; }
.report-title { font-size: var(--text-xl); font-weight: var(--weight-semibold); letter-spacing: -0.01em; }
.report-sub { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-8); font-size: var(--text-2xs); opacity: .85; }
.report-code { font-family: var(--font-mono); font-size: var(--text-2xs); letter-spacing: .02em; }

.report-confidential {
  margin-top: var(--space-10); padding: var(--space-5) var(--space-8);
  border: var(--hairline) solid var(--border); border-radius: var(--radius-md);
  font-size: var(--text-2xs); color: var(--ink-2); max-width: var(--measure);
}
.report-actions { display: flex; gap: var(--space-5); margin-top: var(--space-13); }
.report-band { padding-block: var(--space-16); border-top: var(--hairline) solid var(--border); }
.report-band h2 { font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: var(--space-8); }
.report-band p { max-width: var(--measure); margin-bottom: var(--space-5); }
.report-verdict-line { display: flex; align-items: center; gap: var(--space-8); }
.report-score { font-size: var(--display-md); font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; line-height: 1; }

/* A callout, not a banner: it is the document's own qualification of what it just said.
   The WHOLE border, in --border, and no colour accent at all. It used to draw a --id-text left
   rule, which was wrong twice. A coloured left border is the side-stripe anti-pattern outright
   under the design laws - the whole border or none. And on the printed page that rule landed
   beside the URGENT chip on the one callout whose entire job is to say the merchant is NOT a
   fraud finding, so a claret mark pointing at that sentence argued with the sentence. A callout
   is prose: neither the identity nor the severity ramp has a claim on it, which is the same
   ruling .note already carries. The ground stays --id-wash - a translucent slate, not a red. */
.report-callout {
  padding: var(--space-5) var(--space-8);
  border: var(--hairline) solid var(--border);
  background: var(--id-wash);
  border-radius: var(--radius-xs);
  font-size: var(--text-sm);
}

.report-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.report-caption { text-align: left; font-size: var(--text-2xs); color: var(--ink-3); padding-bottom: var(--space-4); }
.report-table th, .report-table td { padding: var(--space-4) var(--space-5); border-bottom: var(--hairline) solid var(--grid); text-align: left; }
.report-table th[scope="col"] { font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); }
/* Two rules rather than one selector list, so the numeral rule is findable on its own: the render
   contract test looks for `.report-table td.num` as a top-level rule, because tabular numerals in
   the BODY cells are the load-bearing half (a right-aligned header needs no figure alignment). */
.report-table th.num { text-align: right; }
.report-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.report-table .report-code { font-family: var(--font-mono); }
/* A row with no comparable direction says so in words, quietly. It is never blank: a blank cell
   reads as "no movement" instead of "these two figures are not comparable". */
.report-nodir { font-style: italic; color: var(--ink-3); }
.report-note { font-size: var(--text-2xs); color: var(--ink-2); max-width: var(--measure); }
.report-foot { margin-top: var(--space-16); padding-top: var(--space-8); border-top: var(--hairline) solid var(--grid); font-size: var(--text-3xs); color: var(--ink-3); max-width: var(--measure); }

/* ---- Print ---------------------------------------------------------------
   Chrome is hidden rather than restyled: a nav rail on a printed page is a list of links nobody
   can click. The masthead keeps its ink because print-color-adjust: exact overrides the browser
   default of dropping backgrounds, and a masthead that prints as white loses the one thing that
   says whose document this is. Tables do not break across a page mid-row.

   The page box is the one place physical units belong: a margin in rem is a margin measured
   against a font size the printer does not have, and the 2px spacing grid has nothing to say
   about the edge of a sheet of paper. */
@media print {
  @page { margin: 14mm; }
  .sidebar, .topbar, .app-footer, .report-actions, .drawer, .overlay, .toast, .tt, .vpop,
  #blazor-error-ui, .tour-card { display: none !important; }
  body { display: block; background: var(--paper); }
  .main { padding: 0; }
  .report { max-width: none; padding: 0; }
  /* THE PRINT MASTHEAD IS INK ON PAPER, unconditionally, and it does not ask the printer for
     anything. The earlier version asked for the identity ink with print-color-adjust: exact and
     inverted only where that property was unsupported - which reads as detection and is not:
     Firefox honours its own "Print backgrounds" setting over the property, so a browser that fully
     supports it can still drop the ground and leave --id-ink-hi, a light grey chosen to read ON
     the ink, printed on bare white. That loses the title, the MID and the generated-at, which are
     the three things saying what this document is and when it was true. There is no query that
     detects the setting, so the fix is not to depend on it. The horizontal padding goes with the
     ground: without a fill to sit inside, the title would be the one line on the page indented
     away from everything beneath it. --border rather than --ink-1 for the rule: a hairline under a
     heading is a separator, not a second emphasis competing with the title above it. */
  .report-masthead {
    background: transparent; color: var(--ink-1);
    border-radius: 0; border: none;
    border-bottom: var(--hairline) solid var(--border);
    padding: 0 0 var(--space-8);
  }
  /* The wordmark takes currentColor, so it follows the ink above with no print rule of its own.
     The mark does not: since 2026-09-10 it is the Datacap raster tile, so it prints in its own
     colour rather than the masthead's ink - traded deliberately, recorded in DESIGN.md. */
  .report-masthead .report-sub { color: var(--ink-2); opacity: 1; }
  /* Kept, unlike the masthead: the callout's text is dark on a light wash either way, so dropping
     the ground costs nothing a reader needs, and where it is honoured the qualification reads as
     the aside it is. */
  .report-callout { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .report-band, .report-table, .report-table tr { break-inside: avoid; }
  .report-band { padding-block: var(--space-13); }
  a[href]::after { content: ""; }   /* no URL footnotes: every link here is an app route */
  /* There is no keyboard on paper, and FocusTarget focuses the page heading on every navigation -
     so without this the report prints with a ring drawn round its title. */
  :focus-visible { outline: none !important; }
  /* The internal network strip and the queue's teaching copy are chrome ABOUT the console, not
     part of a merchant's document - and this document is the one that leaves the building, so an
     "INTERNAL - not partner-visible" strip printed under a forwardable report is exactly backwards.
     Scoped to a page that IS a report rather than hidden globally: printing any other page must
     still carry its own permanently-visible caveats, which PRODUCT.md principle 2 makes a contract
     rather than an explanation. */
  body:has(.report) .network-strip,
  body:has(.report) .disclaimer,
  body:has(.report) .disclosure { display: none !important; }
}
