/* ==========================================================================
   Pathway Lens — design system tokens, reset, type scale, focus
   Source of truth: docs/superpowers/design/README.md §1

   Structure (do not reorder — a token defined after its use is the
   classic unreadable-artifact bug):
     1. Tokens        (:root custom properties)
     2. Reset & base
     3. Type scale     (utility classes, one per §1's type table row)
     4. Focus          (one treatment, everywhere)

   This file is APPEND-ONLY past this point for every task after this one.
   Sections 1–4 above are owned by this task and must not be edited by
   later tasks — each later task appends its own component block under a
   "/* == <component> == *\/" banner at the bottom of the file, below the
   "COMPONENT BLOCKS BEGIN BELOW" marker.
   ========================================================================== */

/* 1. Tokens */
:root {
  /* Severity hues — semantic, separate from any accent colour. */
  --sev-blocker: #b3261e; --sev-warning: #8a5a00;
  --sev-suggestion: #3a4fb5; --sev-pass: #1f6b4a;

  /* Tint triples: background, border, foreground — drive every chip and notice. */
  --tint-blocker-bg: #fdeceb; --tint-blocker-border: #f3c9c6; --tint-blocker-fg: #8f1e17;
  --tint-warning-bg: #fdf3e0; --tint-warning-border: #edd9b0; --tint-warning-fg: #6f4800;
  --tint-suggestion-bg: #eef1fc; --tint-suggestion-border: #ccd4f3; --tint-suggestion-fg: #2b3c8e;
  --tint-pass-bg: #e8f3ed; --tint-pass-border: #c2e0d1; --tint-pass-fg: #175c3e;
  --tint-neutral-bg: #f5f6fa; --tint-neutral-border: #dfe2e9; --tint-neutral-fg: #4d5361;

  /* Neutrals — five inks, four lines, three surfaces and the canvas. */
  --ink: #14161c; --ink-2: #252933; --ink-3: #4d5361; --ink-4: #6b7180; --ink-5: #8b91a0;
  --line-strong: #cfd3dd; --line: #e4e6ec; --line-soft: #eaecf1; --line-faint: #f2f3f7;
  --surface: #ffffff; --surface-2: #fbfbfc; --surface-3: #f5f6fa; --canvas: #f2f3f7;
  --unassessed-border: #b9bec9;

  /* Type */
  --font-ui: "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Roboto Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Spacing — 4px base */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 56px;

  /* Radius */
  --r-chip: 4px; --r-control: 6px; --r-cat: 7px; --r-finding: 8px; --r-card: 10px; --r-pill: 20px;

  /* Shadow — the one shadow in the system, reserved for a segmented control's raised segment. */
  --shadow-raised: 0 1px 2px #14161c1f;
}

/* 2. Reset and base */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0; background: var(--canvas); color: var(--ink);
  font-family: var(--font-ui); font-size: 14.5px; line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
p, li { text-wrap: pretty; }

/* 3. Type scale — every size in the spec's §1 table, as utility classes */
.t-page   { font-size: 44px;   line-height: 1.02; font-weight: 700; letter-spacing: -0.028em; }
.t-screen { font-size: 26px;   line-height: 1.2;  font-weight: 700; letter-spacing: -0.022em; }
.t-section{ font-size: 24px;   line-height: 1.1;  font-weight: 700; letter-spacing: -0.02em; }
.t-score  { font-size: 60px;   line-height: 0.9;  font-weight: 700; letter-spacing: -0.04em; }
.t-card-n { font-size: 22px;                      font-weight: 700; letter-spacing: -0.02em; }
.t-finding{ font-size: 15.5px; line-height: 1.35; font-weight: 600; }
.t-body   { font-size: 14.5px; line-height: 1.55; }
.t-dense  { font-size: 13.5px; line-height: 1.5; }
.t-caption{ font-size: 12.5px; line-height: 1.5; color: var(--ink-4); }
.mono     { font-family: var(--font-mono); font-size: 12.5px; }
.mono-eyebrow {
  font-family: var(--font-mono); font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-5);
}
.prose { max-width: 68ch; }

/* 4. Focus — one treatment everywhere.

   WP1.12 whole-branch review, Important 3. Two measured defects are fixed HERE, in section 4 itself,
   rather than by appending a second focus rule further down: the bug IS this rule, and a second copy
   of the focus treatment 900 lines away is exactly the drift the append-only rule exists to prevent.

   (a) Specificity. This selector used :where(), which zeroes its own arguments — the whole thing
   scored (0,1,0), on :focus-visible alone. .form-input / .form-btn / .form-select (this file's
   "forms and utilities" block, ~610 lines BELOW) score (0,1,0) too and set the `border` SHORTHAND,
   so on every button, input and select in the product the later, equal-specificity rule won and this
   border-color was silently discarded. Measured in Chromium against the served stylesheet: a focused
   .form-btn / .form-input / .form-select all reported border rgb(207,211,221) — --line-strong, the
   RESTING colour — while a plain <a> correctly reported rgb(58,79,181). :is() takes the highest
   specificity among its arguments (a type selector here), so the selector now scores (0,1,1) and
   wins on specificity rather than on source order. Deliberately not !important, which would lift the
   declaration out of the cascade entirely and make every legitimate future override a fight.

   (b) Contrast. With the border lost, the only surviving indicator was the outline, and
   --tint-suggestion-border (#ccd4f3) measures 1.47:1 on --surface and 1.33:1 on --canvas. WCAG 2.2
   SC 1.4.11 requires 3:1 for a focus indicator — in a product that scores accessibility as one of
   its own nine categories. The dark --sev-suggestion (#3a4fb5) now paints the outline itself:
   7.08:1 on --surface, 6.39:1 on --canvas. The pale #ccd4f3 halo README §1 asks for is not lost, it
   moves outboard to a box-shadow ring, so the rendered treatment is still the spec's own — a dark
   edge on the control with a soft ring outside it. Only which property paints which part has moved,
   and the part that has to carry the 3:1 is now the part that actually survives the cascade. */
:is(a, button, input, select, textarea, summary):focus-visible {
  border-color: var(--sev-suggestion);
  outline: 2px solid var(--sev-suggestion); outline-offset: 0;
  box-shadow: 0 0 0 4px var(--tint-suggestion-border);
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}

/* ==========================================================================
   COMPONENT BLOCKS BEGIN BELOW
   Each later task appends its own block here under "/* == <component> == *\/".
   Do not edit sections 1–4 above from a later task.
   ========================================================================== */

/* == app shell == */
.app-bar {
  display: flex; align-items: center; gap: var(--s5);
  height: 60px; padding: 0 26px;
  background: var(--surface); border-bottom: 1px solid var(--line);
}
.app-bar .brand { display: flex; align-items: center; gap: var(--s2);
  font-size: 14.5px; font-weight: 700; letter-spacing: -0.01em; }
.app-bar .brand::before {
  content: ""; width: 11px; height: 11px; border-radius: 50%;
  background: var(--sev-suggestion);
}
.app-nav { display: flex; gap: var(--s5); margin-left: var(--s5); }
.app-nav a {
  font-size: 13.5px; font-weight: 600; color: var(--ink-3);
  text-decoration: none; line-height: 60px; border-bottom: 2px solid transparent;
}
.app-nav a.is-active { color: var(--ink); font-weight: 700; border-bottom-color: var(--sev-suggestion); }
.app-bar .spacer { margin-left: auto; }
/* 1680px is a DELIBERATE OVERRIDE of the binding spec, which says 1400px (Priority Screens.dc.html
   line 60), recorded here rather than left as a silent divergence — the same treatment .detail-grid's
   24px-for-20px gap got. Requested by the owner 2026-09-08 after viewing the design artboard, which
   renders in a ~1480px frame where 1400px fills 95% of the width and therefore looks full; on a real
   1920px monitor the identical 1400px uses 73% and leaves 260px gutters either side, and at 2560px
   it uses 55%. The design and the code never disagreed about the number, only about the frame.

   1680 is not a taste pick. Review detail's right column is the email preview, whose own toolbar
   claims "600px content width" — the standard email body width. The two-column split is
   1.55fr/1fr with a 24px gap inside 28px page padding, so the preview frame measured 516px at EVERY
   viewport, and a 600px email could not render at 1:1 anywhere. 1640px is the smallest container
   that clears 600px; 1680 gives the frame 625px, so the preview shows a real email at true size
   with a little air. Below 1640 the toolbar's own claim cannot be honoured at any screen size.

   Safe to widen because every prose block already caps its own measure in ch — .finding-desc 70ch,
   .prose/.tests-new-intro 68ch, .journey-note and the score summary 62ch — so line length does not
   grow with the container. What grows is the preview column and the room for finding headers.

   Screens that set their own narrower cap are untouched by design: .reviews-list stays 1180px and
   .tests-new 1000px, both spec'd separately and both single-column. Users, Settings and the Platform
   console inherit this width; they are data tables, which read better wide. */
.app-main { max-width: 1680px; margin: 0 auto; padding: 26px 28px; }

/* Platform: the only structural difference between the two products. */
.app-bar--platform { background: var(--ink); border-bottom-color: #000; }
.app-bar--platform .brand { color: #fff; }
.app-bar--platform .brand::before { background: #8b95e8; }
.app-bar--platform .app-nav a { color: #a8adbc; }
.app-bar--platform .app-nav a.is-active { color: #fff; border-bottom-color: #8b95e8; }
.app-bar--platform .scope-note { font-family: var(--font-mono); font-size: 11px; color: #a8adbc; }

/* Supplementary to the spec's given block above: the mono "Platform" badge beside the wordmark, the
   tenant switcher/name and sign-out group, sized and coloured to sit quietly in the 60px bar. */
.app-bar .badge {
  font-family: var(--font-mono); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-5);
}
.app-bar--platform .badge { color: #a8adbc; }

.app-bar .tenant-switcher { display: flex; align-items: center; gap: var(--s2); }
.app-bar .tenant-switcher label { font-size: 12.5px; color: var(--ink-4); }
.app-bar .tenant-switcher select {
  font: inherit; font-size: 13px; color: var(--ink); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-control); padding: 4px 8px;
}
.app-bar .tenant-switcher button {
  font: inherit; font-size: 13px; color: var(--ink-3); background: none;
  border: 1px solid var(--line); border-radius: var(--r-control); padding: 4px 10px; cursor: pointer;
}
.app-bar .tenant-name { font-size: 13px; color: var(--ink-3); }
.app-bar--platform .tenant-name { color: #a8adbc; }

.app-bar .user-menu { display: flex; align-items: center; gap: var(--s3); }
.app-bar .user-email { font-size: 12.5px; color: var(--ink-4); }
.app-bar .user-menu button {
  font: inherit; font-size: 13px; color: var(--ink-3); background: none; border: none;
  padding: 0; cursor: pointer; text-decoration: underline;
}
.app-bar--platform .user-email { color: #a8adbc; }
.app-bar--platform .user-menu button { color: #fff; }

/* == finding row == */
/* Rule 1: severity drives the left rule and the chip only — never a card-wide fill. Twenty tinted
   backgrounds is unreadable; the border-left is the only place severity touches the card itself. */
.finding {
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong); border-radius: var(--r-finding);
  padding: 16px 18px; display: grid; gap: var(--s3);
}
.finding--blocker    { border-left-color: var(--sev-blocker); }
.finding--warning    { border-left-color: var(--sev-warning); }
.finding--suggestion { border-left-color: var(--sev-suggestion); }
.finding--pass       { border-left-color: var(--sev-pass); }

.finding-head { display: flex; justify-content: space-between; gap: 14px; align-items: flex-start; }
.finding-head-left { display: flex; align-items: flex-start; flex-wrap: wrap; gap: var(--s2); flex: 1 1 auto; min-width: 0; }
.finding-title { overflow-wrap: anywhere; }
.finding-cost-group { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
.finding-cost { flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: 14px; }
.finding-scope { font-size: 11px; color: var(--ink-5); }

/* The chip is the ONE place severity carries a tinted fill — small, not the card. */
.sev-chip {
  flex-shrink: 0; font-size: 11px; font-weight: 700; line-height: 1.4;
  padding: 1px 8px; border-radius: var(--r-chip); border: 1px solid transparent;
}
.sev-chip--blocker    { background: var(--tint-blocker-bg); border-color: var(--tint-blocker-border); color: var(--tint-blocker-fg); }
.sev-chip--warning    { background: var(--tint-warning-bg); border-color: var(--tint-warning-border); color: var(--tint-warning-fg); }
.sev-chip--suggestion { background: var(--tint-suggestion-bg); border-color: var(--tint-suggestion-border); color: var(--tint-suggestion-fg); }
.sev-chip--pass       { background: var(--tint-pass-bg); border-color: var(--tint-pass-border); color: var(--tint-pass-fg); }

.ai-badge {
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  border: 1px dashed var(--unassessed-border); border-radius: var(--r-chip);
  padding: 1px 6px 1px 4px; color: var(--ink-4); flex-shrink: 0;
}
.ai-badge::before {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-right: 4px;
  border-radius: 50%; border: 1px solid var(--ink-5); vertical-align: middle;
}
.ai-note { border-top: 1px solid var(--line-soft); padding-top: var(--s2);
  font-size: 12.5px; color: var(--ink-4); }

.finding-desc { font-size: 14px; line-height: 1.55; color: var(--ink-3); max-width: 70ch; }

.finding-where { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); font-size: 13px; }
.finding-where .locator { font-weight: 600; }
.finding-where .btn-locate { margin-left: auto; }
.finding .muted { color: var(--ink-5); font-size: 12px; }

.finding-rec { background: var(--surface-3); border-radius: var(--r-control);
  padding: 10px 12px; color: var(--ink-2); font-size: 13.5px; }
.finding-rec::before { content: "→ "; font-family: var(--font-mono); color: var(--sev-suggestion); }

/* Colour-swatch variant — one block per failing occurrence, grouped under the colour it replaces
   (not the element), so a multi-element finding never pools one element's candidates under another's. */
.swatches { display: grid; gap: var(--s2); }
/* Fix round 1, Important 2: the header strip shows the offending colour itself, not just its hex —
   only rendered once _FindingRow.cshtml's IsValidHex guard passes. */
.swatch-header { display: flex; align-items: center; gap: var(--s2); }
.swatch-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.swatch-row { display: flex; flex-wrap: wrap; gap: var(--s2); }
.swatch { display: flex; flex-direction: column; gap: 4px; padding: var(--s2);
  border: 1px solid var(--line-soft); border-radius: var(--r-control); background: var(--surface); }
.swatch-top { display: flex; align-items: center; gap: 6px; }
.swatch-chip { display: inline-block; width: 36px; height: 36px; border-radius: var(--r-control);
  border: 1px solid var(--line); vertical-align: middle; }
.swatch-chip--original { width: 15px; height: 15px; border-radius: var(--r-chip); flex-shrink: 0; }
.swatch-top code { font-family: var(--font-mono); font-size: 12.5px; }
.swatch-delta { font-size: 11.5px; }
/* The "N places" count beside a grouped swatch header (owner feedback 2026-09-08). Mono because it
   is a machine-produced number, per this file's type rule. */
.swatch-places { font-size: 11px; color: var(--ink-5); }
.swatch-footnote { font-size: 12px; color: var(--ink-5); margin: 0; }

/* Technical detail disclosure — mono uppercase caret button above a mono evidence panel. */
.finding-detail summary {
  font-family: var(--font-mono); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-4); cursor: pointer; list-style: none;
}
.finding-detail summary::-webkit-details-marker { display: none; }
.finding-detail summary::before { content: "▸ "; }
.finding-detail[open] summary::before { content: "▾ "; }
.detail-panel {
  margin-top: var(--s2); background: var(--surface-2); border: 1px solid var(--line-soft);
  border-radius: var(--r-control); padding: var(--s3);
  font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); overflow-x: auto;
}
.detail-panel .fix-hint { margin: 0 0 var(--s2); }
.detail-panel ul, .detail-panel dl { margin: 0; padding: 0; list-style: none; }
.detail-panel li { display: flex; gap: var(--s2); padding: 4px 0; border-top: 1px solid var(--line-faint); }
.detail-panel li:first-child { border-top: none; }
.detail-panel .line { color: var(--ink-4); }
.detail-panel dt { font-weight: 600; margin-top: var(--s2); }
.detail-panel dt:first-child { margin-top: 0; }
.detail-panel dd { margin: 0 0 var(--s2); color: var(--ink-3); }

/* Rule 5: passes never interleave with problems — one collapsed group at the end. */
.pass-group { border: 1px solid var(--line); border-radius: var(--r-finding); background: var(--surface); }
.pass-group summary {
  display: flex; align-items: center; gap: var(--s2); cursor: pointer; list-style: none;
  padding: 12px 16px; font-size: 13.5px; font-weight: 600; color: var(--ink-2);
}
.pass-group summary::-webkit-details-marker { display: none; }
.pass-group summary::before { content: "▸ "; font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); }
.pass-group[open] summary::before { content: "▾ "; }
.pass-group .pass-hint { font-weight: 400; color: var(--ink-5); }
.pass-list { border-top: 1px solid var(--line-soft); }
.pass-row {
  display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s3);
  padding: 10px 16px; border-top: 1px solid var(--line-faint);
}
.pass-row:first-child { border-top: none; }
/* Fix round 1, Important 1: title + one-line note, category in mono right-aligned. */
.pass-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pass-note { font-size: 12.5px; color: var(--ink-4); }
.pass-row .pass-category { flex-shrink: 0; color: var(--ink-5); }

/* Rule 6: a not-assessed category is a hold placed on its weight, explained rather than scored —
   dashed border, no left rule (it isn't a graded finding), one card per excluded category. */
.not-assessed-group { display: grid; gap: var(--s3); }
.not-assessed {
  border: 1px dashed var(--unassessed-border); background: var(--surface-2);
  border-radius: var(--r-finding); padding: 16px 18px; display: grid; gap: var(--s2);
}
.not-assessed-head { display: flex; align-items: center; gap: var(--s2); }
.chip-not-assessed {
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  border: 1px dashed var(--unassessed-border); border-radius: var(--r-chip);
  padding: 1px 6px; color: var(--ink-4);
}
.not-assessed-category { font-size: 13.5px; font-weight: 600; color: var(--ink-2); }
.not-assessed-note { font-size: 13px; color: var(--ink-4); margin: 0; max-width: 70ch; }

/* == score header == */
/* Six letter bands, three colours (README §3.2): a continuous six-step ramp makes B and C arbitrary
   neighbours, so the letter (band-chip) carries granularity and colour carries the recommendation —
   ship / fix / stop. The number leads at 60px because it is what moves between versions. */
.score-header {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 24px 26px; display: flex; align-items: flex-start; gap: var(--s5); flex-wrap: wrap;
}
.score-header-main { display: flex; flex-direction: column; gap: 10px; }
.score-number-group { display: flex; align-items: baseline; gap: 10px; }
.score-number { font-size: 60px; line-height: 0.9; font-weight: 700; letter-spacing: -0.04em; }
.score-of100 { font-size: 20px; font-weight: 600; color: var(--ink-5); }
.score-was {
  font-family: var(--font-mono); font-size: 14px; color: var(--ink-5); text-decoration: line-through;
}
.score-header-detail {
  border-left: 1px solid var(--line-soft); padding-left: 22px; flex: 1; min-width: 280px;
}
.band-chip {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  border-radius: var(--r-control); padding: 4px 10px;
}
.band-chip strong { font-size: 14px; }
.band-chip.band--good { background: var(--tint-pass-bg); border: 1px solid var(--tint-pass-border); color: var(--tint-pass-fg); }
.band-chip.band--mid  { background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border); color: var(--tint-warning-fg); }
.band-chip.band--bad  { background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border); color: var(--tint-blocker-fg); }
.score-verdict {
  margin: 10px 0 0; font-size: 14.5px; line-height: 1.5; color: var(--ink-2);
  max-width: 62ch; text-wrap: pretty;
}

/* Blocker asymmetry: the header changes SHAPE, not just colour. The cap must read as a hold placed
   on the report — reversible, attributable to one finding — never as a verdict on the whole email.
   (Escaped for the comment-integrity guard: a literal star-slash here would close this comment early
   the same way it did once before — "capped by N blocker(s)" is written out in the markup, not here,
   so there is nothing to escape in practice, but the convention is asterisk-slash — "*\/" — for any
   future example text added to this banner.) */
.score-header--capped {
  border: 1px solid var(--tint-blocker-border); border-top: 3px solid var(--sev-blocker);
}
.score-header--capped .score-number { color: var(--sev-blocker); }
.score-capped-note {
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--sev-blocker);
}
.score-header--capped .score-header-detail { border-left-color: var(--tint-blocker-border); }

/* Fix round 1, Important 2: "On review detail, a right-hand counter group shows blocker / warning /
   pass counts in mono 20px over 12px labels." The flex row above already has one flex:1 child
   (.score-header-detail), so this trailing group is pushed to the far right of the card for free. */
.score-counters { display: flex; gap: var(--s4); align-items: baseline; flex-wrap: wrap; flex-shrink: 0; }
.score-counter-value { font-family: var(--font-mono); font-size: 20px; font-weight: 500; }
.score-counter-label { font-size: 12px; color: var(--ink-4); }
.score-counter--blocker .score-counter-value { color: var(--sev-blocker); }
.score-counter--warning .score-counter-value { color: var(--sev-warning); }
.score-counter--pass .score-counter-value { color: var(--ink-3); }

/* == category chips == */
/* README §3.3: nine chips, wrapping flex, sorted by weight descending (DetailModel.ReadBreakdown does
   the sort) so the expensive categories are met first. Weight is printed on every chip — a reader
   cannot judge a 3-point loss in Technical Integrity (w20) against one in Deliverability Signals (w4)
   without seeing both weights. */
/* A GRID, not a wrapping flex row (owner feedback, 2026-09-07). `display: flex; flex-wrap: wrap`
   with `flex: 1 1 200px` on the chips lets every row divide the available width by its OWN item
   count, so the columns do not line up between rows. Measured in Chromium at 1280px with the nine
   real categories: row 1 held five chips at 232px (left edges 28/272/516/761/1005), row 2 held four
   at 260px — the max-width cap — at 28/300/572/844. Only the first column agreed. These are nine
   scores whose whole purpose is to be scanned and compared against each other, and a reader cannot
   run an eye down a column that moves.

   auto-fill + minmax(232px, 1fr) puts the column definition on the CONTAINER, so every row inherits
   the same tracks and a short last row simply leaves cells empty instead of resizing itself. It
   keeps the responsive intent of the original wrapping row — the column count still falls with the
   viewport — without letting item count drive item width.

   The 232px floor is measured, not guessed, and is the reason this is not simply 200px. auto-fill
   packs as many tracks as fit, so a WIDER container yields MORE and NARROWER columns: at a 200px
   floor the 1680px shell gave seven 222px chips, narrow enough to wrap "Rendering & Compatibility"
   onto a second line and leave that chip 91px tall against its row-mates' 67px. 232px is the widest
   floor that still fits three columns at an 800px viewport (3x232 + 2x12 gap = 720, within 744px of
   content); 236px tips it to two. Measured across three viewports: 6+3 at 1920, 5+4 at 1400 and
   3+3+3 at 800, with no label wrapping at any. (Chips still differ by 3px in height — 70px assessed
   against 67px not-assessed — which is the score bar, not a wrap.)

   max-width is dropped rather than kept: it is what pinned row 2 at 260px, and it would re-break
   the alignment at any width where a track exceeds it. It is also unnecessary now, because
   auto-fill adds columns as the container grows, so tracks self-limit to roughly 200-260px anyway.
   min-width: 0 replaces the old 180px floor for the reason this file has already had to learn three
   times (.detail-main, .fix-list, .diff-groups): a grid item's automatic minimum is its min-content
   size, and a long category name would otherwise push its own track wider than the one it was
   given. The 200px floor now lives in the track, which is where it belongs. */
.cat-chips { display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: var(--s3); }
.cat-chip {
  min-width: 0;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-cat);
  padding: 10px 12px; display: grid; gap: var(--s2);
}
.cat-chip-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
.cat-chip-name { font-size: 13.5px; font-weight: 600; color: var(--ink-2); }
.cat-chip-weight { font-family: var(--font-mono); font-size: 11px; color: var(--ink-5); flex-shrink: 0; }
.cat-chip-body { display: flex; align-items: center; gap: var(--s2); }
.cat-chip-track { flex: 1; height: 4px; border-radius: 2px; background: var(--line-soft); overflow: hidden; }
.cat-chip-fill { height: 100%; border-radius: 2px; }
.cat-chip-fill--pass    { background: var(--sev-pass); }
.cat-chip-fill--warning { background: var(--sev-warning); }
.cat-chip-fill--blocker { background: var(--sev-blocker); }
.cat-chip-score { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink-3); flex-shrink: 0; }

/* Rule 6 applied per-chip: a not-assessed category is a hold on its weight, not a zero. Dashed,
   greyed, and — critically — NO track at all; an empty/zero track would misreport a category that
   was never run as one that scored badly. "not assessed" replaces the track+score row entirely. */
.cat-chip--unassessed {
  border-style: dashed; border-color: var(--unassessed-border); background: var(--surface-2);
}
.cat-chip--unassessed .cat-chip-name { color: var(--ink-4); }
.cat-chip-note {
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-4);
}

/* "Why this score" disclosure — closed by default, mono uppercase caret summary matching the other
   disclosures on this page (.finding-detail, .pass-group). */
.score-breakdown summary {
  font-family: var(--font-mono); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-4); cursor: pointer; list-style: none;
  margin: var(--s4) 0 var(--s2);
}
.score-breakdown summary::-webkit-details-marker { display: none; }
.score-breakdown summary::before { content: "▸ "; }
.score-breakdown[open] summary::before { content: "▾ "; }
.score-breakdown-subhead {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-4); margin: 0 0 var(--s2);
}
.score-breakdown-table {
  width: 100%; border-collapse: collapse; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-control); overflow: hidden;
}
/* Header strip: mono 10-10.5px uppercase on #fbfbfc (var(--surface-2)). */
.score-breakdown-table th {
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.04em; text-align: left; background: var(--surface-2); color: var(--ink-4);
  padding: 8px 12px; border-bottom: 1px solid var(--line);
}
.score-breakdown-table td {
  padding: 10px 12px; border-bottom: 1px solid var(--line-faint); font-size: 13.5px; vertical-align: top;
}
.score-breakdown-table tr:last-child td { border-bottom: none; }
.score-breakdown-table td.num { font-family: var(--font-mono); font-size: 13px; }
.score-breakdown-table tr.score-row--unassessed { background: var(--surface-2); }
.score-breakdown-table tr.score-row--unassessed td { color: var(--ink-4); }
.score-breakdown-table .contributions { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.score-breakdown-table .contributions li { display: flex; justify-content: space-between; gap: var(--s3); }
.score-breakdown-table .muted { color: var(--ink-5); }

/* == journey block == */
/* README §3.4: eyebrow, journey name at 19–22px/700 (t-card-n sits at the top of that range, reused
   rather than a new one-off size), confidence percentage in mono, provenance phrase. All new class
   names are scoped to this component (journey-*) rather than reusing bare .notice/.btn/.error — Task
   6's own postmortem is the reason why: a generic class shared across components silently broke one
   of them, and the fix was a manual-verification catch, not a test. */
/* margin-bottom removed (owner feedback 4.2 + residual R3): spacing between review-detail's
   top-level blocks is now owned in one place by .rd-page's gap, in the "== review detail ==" block.
   This margin was one of three ad-hoc per-component spacings that between them left the score header
   and the category chips flush at 0px. _JourneyBlock is rendered by Detail.cshtml and nowhere else,
   so this margin had exactly one consumer and no second caller to regress. */
.journey {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 20px 22px; display: grid; gap: 6px;
}
.journey-eyebrow { margin: 0; }
.journey-head { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.journey-name { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; color: var(--ink-2); }
.journey-confidence { color: var(--ink-4); }
.journey-provenance { margin: 0; font-size: 13px; color: var(--ink-4); }

/* Unclassified (README §3.4): dashed border, name in --ink-3 (the spec's literal #4d5361) — an honest
   answer, not an error, so the card is a softer hold rather than a broken shape. */
.journey--unclassified { border-style: dashed; border-color: var(--unassessed-border); background: var(--surface-2); }
.journey-name--unclassified { color: var(--ink-3); }
.journey-note { margin: 0; font-size: 13.5px; color: var(--ink-3); max-width: 62ch; text-wrap: pretty; }

/* Stale after correction: Warning-tinted, naming both the journey the score below still reflects and
   the one a re-analysis would apply — the plain tint triple, no left rule (this is a notice, not a
   graded finding). */
.journey-stale {
  margin: 4px 0 0; padding: 10px 12px; border-radius: var(--r-control);
  background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border); color: var(--tint-warning-fg);
  font-size: 13.5px; line-height: 1.5;
}

/* journey-select/journey-btn/journey-btn--reanalyse/journey-error/journey-label used to live here
   (Task 7). Task 11 (README §3.7) folds them into the shared .form-select/.form-btn/.form-btn--accent/
   .form-error/.form-label system appended under "== forms and utilities ==" below — see that block's
   own comment. _JourneyBlock.cshtml now renders those shared classes directly; nothing in this file
   still needs a journey-scoped alias for any of the five. */

.journey-divider { border: none; border-top: 1px solid var(--line-soft); margin: var(--s2) 0; }
.journey-form { display: flex; align-items: flex-end; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s2); }
.journey-form + .journey-form { margin-top: var(--s3); }

/* == email preview == */
/* README §3.5: the iframe is a black box of arbitrary height whose contents cannot be styled,
   measured or reached from outside it, so the chrome around it does the work. One box
   (.preview-box) holds a toolbar (surface-2, bottom border) over the frame area — a segmented
   Desktop / Tablet / Mobile control (track --line-faint, radius --r-cat i.e. 7, 3px padding; the
   system's one shadow, --shadow-raised, on the active segment only, which is the one place it
   belongs), the current width printed in mono, and "sandboxed no scripts" in mono uppercase
   (.mono-eyebrow) right-aligned via margin-left: auto. Frame area backdrop --canvas, centred frame
   at the selected width, white, 1px border, radius --r-chip, min-height in the spec's 300-420px
   range. When there is no HTML, .preview-box and .preview-frame-wrap stay — only their contents
   change to a one-line notice, never an empty grey rectangle. */
.preview-box {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  overflow: hidden; margin-bottom: var(--s5);
  display: flex; flex-direction: column; min-height: 0;
}
.preview-toolbar {
  display: flex; align-items: center; gap: var(--s3);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  padding: var(--s2) var(--s3);
}
.preview-segmented {
  display: flex; gap: 2px;
  background: var(--line-faint); border-radius: var(--r-cat); padding: 3px;
}
.preview-segment {
  font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-3);
  border: none; background: transparent; border-radius: 4px; padding: 5px 12px; cursor: pointer;
}
.preview-segment.is-active { background: var(--surface); color: var(--ink-2); box-shadow: var(--shadow-raised); }
.preview-width { color: var(--ink-4); }
.preview-sandbox-note { margin-left: auto; }
/* flex: 1 + min-height: 0 is what lets the frame ABSORB the column's leftover height rather than
   take a fixed number of pixels — see .detail-preview's own comment for why that matters. */
.preview-frame-wrap {
  background: var(--canvas); padding: var(--s4) var(--s5);
  display: flex; justify-content: center;
  flex: 1 1 auto; min-height: 0;
}
/* Height is viewport-relative, not a fixed number (owner request 2026-09-08 — "can we make the
   previewer taller"). The spec's 360px floor is kept as the floor, but it was ALSO the ceiling: the
   frame rendered 392px inside a 668px column on an 813px viewport, so most of the sticky column was
   empty while the email it exists to show was scrolling inside a letterbox.

   The subtraction is the chrome above and below it, not a guess: .detail-preview is sticky at
   top: 150px, the toolbar is ~48px, .preview-frame-wrap adds 16px of padding top and bottom, and
   ~24px keeps the bottom edge off the viewport floor. max() rather than a bare calc so a short
   window still gets the spec's 360px instead of collapsing — on a 700px laptop viewport the calc
   yields 440px, and below ~620px the floor takes over. Deliberately NOT taller than the visible
   column: this thing is sticky, so a frame exceeding 100vh - 150px would hang its own bottom off
   the screen permanently, which is the opposite of what was asked for. */
.preview-frame {
  width: 600px; max-width: 100%;
  min-height: 360px; height: 100%;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-chip);
}
.preview-frame.tablet { width: 480px; }
.preview-frame.mobile { width: 375px; }
.preview-empty {
  margin: 0; width: 100%; max-width: 600px; padding: var(--s6) var(--s5); text-align: center;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-chip); color: var(--ink-4);
}

/* == diff block == */
/* README §3.8: answers one question — did the fix work — so the delta is the headline and the
   groups are evidence. Header: old score in mono 22px --ink-5, a mono arrow, the new score at
   34px/700, then a tinted delta badge. Three equal columns (a 1fr/1fr/1fr grid), Resolved first,
   then Persisting, then New — that order is fixed in _DiffBlock.cshtml's own markup, never derived
   from whatever order the finding-change data happens to arrive in: someone who fixed four things
   wants to see four things gone before they see what is left. Each group: an 8px square in the
   group's colour, a 13.5px/700 label, a mono count, items as title + severity with a --line-faint
   top border, and a closing note in 12.5px --ink-4. The regression footer is Blocker-tinted and
   states what got worse in words and a real number — never a bare glyph. */
.diff-block {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s5); display: grid; gap: var(--s4);
}
.diff-block + .diff-block { margin-top: var(--s4); }

.diff-header { display: flex; flex-direction: column; gap: 4px; }
.diff-score-row { display: flex; align-items: baseline; gap: var(--s2); }
.diff-score-old { font-size: 22px; color: var(--ink-5); }
.diff-arrow { font-size: 18px; color: var(--ink-4); }
.diff-score-new { font-size: 34px; font-weight: 700; letter-spacing: -0.02em; }
.diff-delta-badge {
  font-family: var(--font-mono); font-size: 13px; font-weight: 600;
  border-radius: var(--r-control); padding: 3px 9px; margin-left: var(--s1);
}
.diff-delta-badge.diff-delta--pass     { background: var(--tint-pass-bg); border: 1px solid var(--tint-pass-border); color: var(--tint-pass-fg); }
.diff-delta-badge.diff-delta--blocker  { background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border); color: var(--tint-blocker-fg); }
.diff-delta-badge.diff-delta--neutral  { background: var(--tint-neutral-bg); border: 1px solid var(--tint-neutral-border); color: var(--tint-neutral-fg); }
.diff-context { margin: 0; font-size: 13px; color: var(--ink-4); }
.diff-target-label { font-weight: 600; color: var(--ink-3); text-transform: capitalize; }

.diff-cats { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.diff-cats li { display: flex; justify-content: space-between; gap: var(--s3); font-size: 13px; }
.diff-cat-name { color: var(--ink-3); }
.diff-cat-change { color: var(--ink-4); }

.diff-groups { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s4); }
.diff-group-head { display: flex; align-items: center; gap: var(--s2); }
.diff-group-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
.diff-group-dot--resolved   { background: var(--sev-pass); }
.diff-group-dot--persisting { background: var(--sev-warning); }
.diff-group-dot--new        { background: var(--sev-blocker); }
.diff-group-label { font-size: 13.5px; font-weight: 700; color: var(--ink-2); }
.diff-group-count { font-size: 12px; color: var(--ink-5); margin-left: auto; }
.diff-items { list-style: none; margin: 0; padding: 0; }
.diff-item {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--s2);
  padding: 8px 0; border-top: 1px solid var(--line-faint);
}
.diff-item-title { font-size: 13.5px; color: var(--ink-2); }
.diff-item-cost { font-size: 11px; color: var(--ink-5); flex-shrink: 0; }
.diff-group-note { margin: var(--s2) 0 0; font-size: 12.5px; color: var(--ink-4); }

.diff-regression-footer {
  display: flex; align-items: flex-start; gap: var(--s3);
  background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border);
  border-radius: var(--r-control); padding: var(--s3) var(--s4);
}
.diff-regression-chip {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-transform: uppercase;
  color: var(--tint-blocker-fg); background: var(--surface); border: 1px solid var(--tint-blocker-border);
  border-radius: var(--r-chip); padding: 2px 8px; flex-shrink: 0; white-space: nowrap;
}
.diff-regression-text { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--tint-blocker-fg); }

/* == data table == */
/* README §3.6: one table serves reviews, users, invitations, organisations, tenants and rotation
   history — built as CSS-grid rows, not a literal <table>, because Rule 1 (a whole row is the
   anchor) has no valid <tr>-as-<a> equivalent, and different consumers need different columns
   anyway. `.dt-row`/`.dt-head` share one grid-template-columns so header and body cells always
   line up; a future consumer (the platform console, at tighter row padding) overrides padding
   alone, never the column template. `role="table"/"row"/"columnheader"/"cell"` restore the table
   semantics a screen reader would otherwise lose from plain divs.

   Rule 1: nothing inside a row may be interactive — the affordance is the hover shift plus a
   PERSISTENT chevron, never an underline, so a nested control can never steal the click. Rule 3:
   every column but subject/thread is sized to its longest possible customer value, not its header
   label, so a page never reflows — hence the fixed `grid-template-columns` and `min-width: 820px`
   living on the scroll container's child, never on the page itself (the *page* must never gain a
   horizontal scrollbar; only `.dt-wrap` may). *\/ appears nowhere in this banner's own example
   text, so nothing here needs escaping beyond this file's usual convention. */
.dt-filter-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s4);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  padding: 14px 18px; margin: 0 0 var(--s4); border-radius: var(--r-control) var(--r-control) 0 0;
}
.dt-filter-label { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.dt-filter-select {
  font: inherit; font-size: 13.5px; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r-control); padding: 6px 9px;
}
.dt-filter-check { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-2); }
.dt-filter-check input[type="checkbox"] { accent-color: var(--sev-suggestion); }
.dt-filter-count { color: var(--ink-4); margin-left: auto; }

/* Rule 2: stated in words, with a way out — rendered ONLY while the request's own needsAttention
   filter is on (never inferred from whether any row happens to match), so it never becomes a
   notice that always renders and therefore tells the reader nothing. */
.dt-filter-notice {
  margin: 0 0 var(--s4); padding: 10px 14px; border-radius: var(--r-control);
  background: var(--tint-suggestion-bg); border: 1px solid var(--tint-suggestion-border);
  color: var(--tint-suggestion-fg); font-size: 13.5px;
}
.dt-filter-reset { color: inherit; font-weight: 600; text-decoration: underline; margin-left: 6px; }

.dt-empty { padding: var(--s5); text-align: center; color: var(--ink-4); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); }
.dt-empty .dt-filter-reset { color: var(--sev-suggestion); }

.dt-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface); }
.dt { min-width: 820px; }
.dt-head, .dt-row {
  display: grid; grid-template-columns: 2.4fr 62px 1.5fr 1fr 1.1fr 22px;
  align-items: center; gap: var(--s3); padding: 14px 18px;
}
.dt-head > div, .dt-row > div { min-width: 0; }
.dt-head {
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-4);
}
.dt-row {
  border-top: 1px solid var(--line-faint); text-decoration: none; color: inherit;
}
.dt-row:first-child { border-top: none; }
/* #f7f8fb matches no existing token (closest, --surface-2 #fbfbfc, is visibly different) — the
   spec names this exact hover value for the row shift, so it is written here rather than adding a
   one-rule :root token for a single consumer. */
.dt-row:hover { background: #f7f8fb; }

.dt-cell-subject { display: flex; flex-direction: column; gap: 2px; }
.dt-subject {
  font-size: 14px; font-weight: 600; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dt-thread { color: var(--ink-5); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.dt-score { display: flex; align-items: center; gap: 6px; }
.dt-score-num { font-size: 14px; }
.dt-score-num.band--good { color: var(--sev-pass); }
.dt-score-num.band--mid  { color: var(--sev-warning); }
.dt-score-num.band--bad  { color: var(--sev-blocker); }
.dt-band {
  font-family: var(--font-mono); font-size: 11px; padding: 1px 6px; border-radius: var(--r-chip);
}
.dt-band.band--good { background: var(--tint-pass-bg); border: 1px solid var(--tint-pass-border); color: var(--tint-pass-fg); }
.dt-band.band--mid  { background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border); color: var(--tint-warning-fg); }
.dt-band.band--bad  { background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border); color: var(--tint-blocker-fg); }
/* Task 12 (README §2.2/§3.6): Running and Failed rows have no score yet, so BOTH cells in the pair
   read "—" — not just the number. band--none is the same dash rendered as a neutral chip (tint-
   neutral-*, matching the mockup's f5f6fa/dfe2e9/6b7180 verbatim) rather than bare text, so the two
   dashes read as one placeholder pair, not a number that lost its badge. */
.dt-band.band--none { background: var(--tint-neutral-bg); border: 1px solid var(--tint-neutral-border); color: var(--ink-4); }
/* README §3.6/§3.10: the marker is red only for a regression or a blocker — a warning count stays
   neutral ink, because "2 warnings" isn't the same urgency as "1 blocker" or "-14 vs v3". */
.dt-marker { font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); white-space: nowrap; }
.dt-marker--red { color: var(--sev-blocker); }

/* Rule 1: a persistent chevron, never an underline — always visible, not a hover reveal. */
.dt-chevron { color: var(--unassessed-border); font-size: 15px; text-align: center; }

.status-pill {
  display: inline-block; font-family: var(--font-mono); font-size: 11.5px;
  padding: 4px 10px; border-radius: var(--r-pill); border: 1px solid transparent;
}
.status-pill--queued    { background: var(--tint-neutral-bg); border-color: var(--tint-neutral-border); color: var(--tint-neutral-fg); }
.status-pill--running   { background: var(--tint-suggestion-bg); border-color: var(--tint-suggestion-border); color: var(--tint-suggestion-fg); }
.status-pill--completed { background: var(--tint-pass-bg); border-color: var(--tint-pass-border); color: var(--tint-pass-fg); }
.status-pill--failed    { background: var(--tint-blocker-bg); border-color: var(--tint-blocker-border); color: var(--tint-blocker-fg); }

.dt-pager { display: flex; align-items: center; gap: var(--s4); padding: var(--s3) 4px; font-size: 13px; }
.dt-pager-link { color: var(--sev-suggestion); text-decoration: none; font-weight: 600; }
.dt-pager-link--disabled { color: var(--unassessed-border); cursor: default; font-weight: 600; }

/* README §3.6/§3.10: Archived (dashed outline) and Suspended (warning tint) — the two review-status
   pill variants Task 10 didn't need for the reviews list. No live caller renders them yet (the
   tenant/organisation status tables that will are Platform screens, out of scope for this pass), but
   they complete the six-variant set the spec names and cost nothing to define now. */
.status-pill--archived  { background: transparent; border-color: var(--unassessed-border); border-style: dashed; color: var(--ink-4); }
.status-pill--suspended { background: var(--tint-warning-bg); border-color: var(--tint-warning-border); color: var(--tint-warning-fg); }

/* == forms and utilities == */
/* README §3.7 (form fields) and §3.10 (utility set).
   .form-label/.form-select/.form-btn/.form-error are a genuinely SHARED system, not a component-scoped
   one — they replace journey-select/journey-btn/journey-btn--reanalyse/journey-error (Task 7 flagged
   the fold-in as an open question when it shipped those) and are used a second time by the destructive
   confirm's grace-period select below, which is what makes "shared" true rather than aspirational.
   Everything else in this block (.quota-meter-*, .copy-field-*, .destructive-confirm-*) stays scoped to
   its own component, per the journey block's own postmortem on reusing bare classes across components
   (see this file's "== journey block ==" comment) — sharing is deliberate here, not the default. */

.form-label {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
}
.form-input, .form-select {
  font: inherit; font-size: 14px; padding: 9px 11px;
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--surface); color: var(--ink);
}
/* .btn shares this rule rather than owning one of its own. WP1.12 Task 1 replaced site.css wholesale
   and dropped the pre-branch base rule (`.btn { background: var(--accent); color: #fff; padding:
   6px 12px; ... }`, old line 53); Task 11 introduced .form-btn for the redesigned forms but nothing
   back-filled the old class. Every remaining `class="btn"` call site therefore rendered as a bare UA
   button — measured in Chromium: Arial 13.3px, 2px outset border, 1px/6px padding, and on the <a>
   variants rgb(0,0,238) underlined link text. That is 22 controls across nine files, including every
   button on all six Platform screens and both Users screens, which were STYLED BEFORE this branch.
   A regression, not a scope gap.

   Folded into this selector rather than appended as a new block at end of file, for a cascade
   reason that a separate block would get wrong: _FindingRow.cshtml composes `class="btn btn-copy"`,
   and .btn-copy is (0,1,0) at line ~800. A .btn block appended below it would tie on specificity and
   win on source order, overriding .btn-copy's mono font, 12px size and 6px/10px padding — so the
   finding row's copy button would silently stop matching _CopyField's bare .btn-copy. Sharing the
   rule here keeps .btn ABOVE .btn-copy, so the modifier still wins. .btn-locate (line ~223) sets
   only margin-left, which this rule does not set, so it is unaffected either way.

   Neutral, not accent-filled: the pre-branch .btn was a solid --accent fill with white text, but
   this design system has no filled button — .form-btn--accent tints border and text. Matching the
   system is the un-regression; restyling those screens is a separate, optional pass. */
.form-btn, .btn {
  font: inherit; font-size: 13px; font-weight: 600; padding: 9px 14px; cursor: pointer;
  text-decoration: none; display: inline-block;
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--surface); color: var(--ink-2);
}
.form-btn--accent      { border-color: var(--sev-suggestion); color: var(--sev-suggestion); }
.form-btn--destructive { border-color: var(--sev-blocker); color: var(--sev-blocker); }
/* Background only, deliberately. A border-color hover would read more strongly on an outline
   button, but this rule sits AFTER .form-btn--accent/--destructive and would override the border
   colour that gives those two their identity. Matches .dt-row:hover, which is the hover idiom this
   stylesheet already chose; cursor: pointer above is the primary affordance the regression removed. */
.form-btn:hover, .btn:hover { background: var(--surface-3); }

/* :disabled is not cosmetic here, it is required BY the fix. Users/Detail.cshtml renders five
   buttons with `disabled="@locked"` / `disabled="@d.IsOwner"` — that greying is the only thing
   telling a reader the protected-Owner and no-self-demote guards are in force. Until now the UA's
   own disabled styling supplied it. Giving .btn a real background and colour above would have
   overridden that and left all five looking fully enabled while being inert: a visible regression
   turned into an invisible lie, which is strictly worse than the bug being fixed. */
/* --ink-4, not --ink-5. Disabled controls are exempt from WCAG 1.4.3, so --ink-5 would not have
   been a violation — but it measures 2.92:1 on --surface-3 and this product scores accessibility as
   one of its own nine categories. --ink-4 measures 4.52:1, clearing the 4.5:1 it is exempt from,
   while staying obviously lighter than an enabled button's --ink-2 at 14.55:1. Three signals carry
   the state, not one: the ink, the --surface-3 fill and the softer --line border. */
.form-btn:disabled, .btn:disabled {
  background: var(--surface-3); color: var(--ink-4); border-color: var(--line);
  cursor: default;
}
.form-btn:disabled:hover, .btn:disabled:hover { background: var(--surface-3); }

/* Costly submits (README §3.7, Rule 2): the cost clause after the em dash renders in a lighter
   weight — a class ready for any button whose label is server-split into "<action>" and "<span
   class='btn-cost-detail'>— <cost clause></span>"; no live control in this pass composes it (see this
   file's destructive-confirm comment below for why), but it costs nothing to have ready. */
.btn-cost-detail { font-weight: 400; color: var(--ink-4); }

/* Error state (README §3.7, Rule 1): border and background move onto the field itself, and the
   sentence below it carries a mono "!" as CSS content — never DOM text, so it can never defeat a
   raw-HTML assertion on the sentence itself — plus a UNIQUE sentence, never a label the form always
   renders. #fffafa matches no existing token; the spec names this exact value for both the error field
   and the destructive-confirm card below, the same one-rule reasoning the data table used for #f7f8fb. */
.form-field--error .form-input,
.form-field--error .form-select {
  border-color: var(--sev-blocker); background: #fffafa;
}
.form-error {
  display: flex; align-items: baseline; gap: 6px;
  margin: 4px 0 0; font-size: 13px; color: var(--tint-blocker-fg);
}
.form-error::before { content: "!"; font-family: var(--font-mono); font-weight: 700; }

/* Read-only fields (README §3.7, Rule 3): label + value as text. No disabled inputs, ever — a greyed
   input still reads as a door. */
.form-readonly { display: flex; flex-direction: column; gap: 4px; }
.form-readonly-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.form-readonly-value { margin: 0; font-size: 14px; color: var(--ink); }

/* Quota meter (README §3.10): names the allowance, the use and the reset date — all three, or the
   number isn't actionable. The fill is a <progress> value/max pair, not an inline style, so a
   server-computed percentage never needs one. */
.quota-meter { display: grid; gap: 6px; }
.quota-meter-head { display: flex; align-items: baseline; gap: var(--s2); }
.quota-meter-of { color: var(--ink-5); }
.quota-meter-track {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  width: 100%; height: 8px; border: none; border-radius: 4px; overflow: hidden;
  background: var(--line-faint); color: var(--sev-suggestion);
}
.quota-meter-track::-webkit-progress-bar { background: var(--line-faint); }
.quota-meter-track::-webkit-progress-value { background: var(--sev-suggestion); }
.quota-meter-track::-moz-progress-bar { background: var(--sev-suggestion); }
/* Pressure crosses into amber past ~80% used, per spec. */
.quota-meter-track--warn { color: var(--sev-warning); }
.quota-meter-track--warn::-webkit-progress-value { background: var(--sev-warning); }
.quota-meter-track--warn::-moz-progress-bar { background: var(--sev-warning); }
.quota-meter-foot { color: var(--ink-4); }

/* Copy-to-clipboard (README §3.10). .btn-copy/data-copy is the existing pair site.js already
   delegates a click handler for (built for the finding row's hex-swatch copy button) — the copy
   field reuses it rather than adding a second mechanism. */
/* Same explicit-zero-minimum fix as .tests-new-aside above, at the component rather than at one of
   its callers, so every _CopyField gets it. A bare `display: grid` here gives .copy-field a single
   implicit `auto` column sized to its content's min-content — and the content is a tenant inbound
   address, one unbreakable 58-character token. .copy-field-row then needs min-width: 0 for the same
   reason one level down: it is a flex container, but as a GRID ITEM its own automatic minimum is
   min-content, so without this it re-widens the track its parent just constrained.
   .copy-field-value already had min-width: 0 + overflow-x: auto so the address scrolls inside its
   own box — that was always the intended containment, and these two rules are what let it be
   reached instead of the whole page widening first. */
.copy-field { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; }
.copy-field-row { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.copy-field-value {
  flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-control);
  padding: 9px 11px; color: var(--ink-2);
}
.copy-field-note { margin: 0; font-size: 13px; color: var(--ink-4); }
.btn-copy {
  font-family: var(--font-mono); font-size: 12px; font-weight: 500; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--surface); color: var(--ink-3); padding: 6px 10px; flex-shrink: 0;
}

/* Destructive confirm (README §3.10): the confirm names the consequence and the cancel restates what
   it preserves — never a bare Yes/No, because that makes the reader reconstruct what they are
   agreeing to. Not yet linked from any live button (see Platform/Tenant.cshtml.cs's
   OnGetRotateConfirmAsync doc comment) so the token-rotation screen can adopt it without a second
   design pass — this pass leaves that screen unrestyled, per scope. */
.destructive-confirm {
  display: grid; gap: var(--s3); max-width: 480px;
  background: #fffafa; border: 1px solid var(--tint-blocker-border); border-radius: var(--r-card);
  padding: 20px 22px;
}
.destructive-confirm-title { margin: 0; font-size: 17px; font-weight: 700; color: var(--ink); }
.destructive-confirm-consequence { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-3); }
.destructive-confirm-form { display: grid; gap: var(--s3); }
.destructive-confirm-actions { display: flex; gap: var(--s2); }

/* == reviews list == */
/* Task 12, README §2.2: the /reviews page shell — narrower than the review detail's 1400px
   (.app-main's own max-width) because this is a table screen, not a two-column report; title + a
   count subtitle; a "New test" primary button, right-aligned. The table, filter bar, active-filter
   strip, both empty states and the regression marker were all already built by Task 10 under the
   "== data table ==" banner above (extended there for the band--none addition, since the score cell
   genuinely IS that same component) — this block owns only the page-level chrome around it.

   Exact pixel values (20px gap, 18px margin, 10px/15px button padding) are taken verbatim from
   Priority Screens.dc.html's REVIEWS LIST block rather than rounded onto the 4px spacing scale,
   matching this file's own precedent for values the base scale doesn't cover (.dt-head/.dt-row's
   14px 18px, .form-input's 9px 11px). */
.reviews-list { max-width: 1180px; margin: 0 auto; }
.reviews-head { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 18px; }
.reviews-subtitle { margin: 7px 0 0; font-size: 14px; color: var(--ink-3); }

/* The button itself is rendered only when IndexModel.CanSubmitTests is true (Reviews/Index.cshtml) —
   ABSENT for the three read-only roles, never disabled, per README's "nav items a role cannot use are
   absent" rule (§3.9) applied to this control too. */
.reviews-new-btn {
  margin-left: auto; background: var(--ink); border: 1px solid var(--ink); border-radius: var(--r-control);
  padding: 10px 15px; font-family: var(--font-ui); font-size: 13.5px; font-weight: 600; color: #fff;
  text-decoration: none; display: inline-block; cursor: pointer;
}
.reviews-new-btn:hover { background: var(--sev-suggestion); border-color: var(--sev-suggestion); }

/* == new test == */
/* Task 13, README §2.3: /tests/new. Two columns, minmax(0,1.6fr) minmax(0,1fr), inside a 1000px shell —
   narrower again than the reviews list, because this is a single form, not a table. Every field
   primitive (.form-label/.form-input/.form-select/.form-field--error/.form-error) and every right-rail
   component (.quota-meter-*, .copy-field-*) is Task 11's shared system, rendered via <partial> — this
   block owns only the page-level grid, the tag note, the dropzone and the over-quota notice, none of
   which existed before this task. */
.tests-new { max-width: 1000px; margin: 0 auto; }
.tests-new-head { margin-bottom: var(--s5); }
.tests-new-intro { margin: 6px 0 0; font-size: 14px; color: var(--ink-3); max-width: 68ch; }

/* Page-level outcome notices (a duplicate thread, a paused tenant, a reached thread cap) — distinct
   from .form-error below, which is a SENTENCE ATTACHED TO ITS FIELD (README §3.7 Rule 3). These are
   not about any one field, so they sit above the form instead. */
.tests-new-notice {
  margin: 0 0 var(--s4); padding: var(--s3) var(--s4);
  background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border);
  border-radius: var(--r-control); color: var(--tint-blocker-fg); font-size: 13.5px;
}
.tests-new-grid {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: var(--s5); align-items: start;
}
.tests-new-form { display: grid; gap: var(--s4); }
/* minmax(0, 1fr) and min-width: 0, for the fourth time in this file (.detail-main, .fix-list,
   .diff-groups were the first three). A bare `display: grid` gives this aside a single IMPLICIT
   `auto` column, which sizes to its content's min-content width — and the content includes the
   tenant's inbound address, a 58-character unbreakable token
   (9daaf936...@inbound.pathwaylens.local). Measured in Chromium: the aside's grid track was correct
   at 375px while everything inside it rendered 518px, overhanging by 143px and pushing the DOCUMENT
   to 1283px against a 1280px viewport — a horizontal scrollbar on the whole page, at every width
   tested (1283/1280, 1151/1024, 1013/800). .copy-field-value already carries its own min-width: 0 +
   overflow-x: auto so the address can scroll inside its own box; that defence was simply never
   reachable, because this container had already been widened to fit it. */
.tests-new-aside { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; gap: var(--s5); }
.tests-new-aside h2 { margin: 0 0 var(--s2); font-size: 15px; font-weight: 700; color: var(--ink); }

/* README §2.3's "live note": count of earlier versions on the thread the typed tag resolves to,
   refreshed via a debounced fetch to OnGetThreadInfoAsync (site.js's tag-note listener) rather than a
   full page reload — the tag itself, and therefore the version count, changes before any submit. */
.tests-new-tag-note { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-4); }

/* .eml upload as a dashed drop zone (README §2.3) — a plain <input type=file> styled to read as a
   target, not a control; no drag/drop JS wired (a native file input already accepts a dropped file
   without one), so the "drop zone" is the visual affordance, not a distinct interaction. */
.tests-new-dropzone {
  display: grid; gap: var(--s1); text-align: center; padding: var(--s5) var(--s4);
  border: 1px dashed var(--line-strong); border-radius: var(--r-control);
  background: var(--surface-2); color: var(--ink-4); font-size: 13px;
}
.tests-new-dropzone strong { color: var(--ink-2); }
.tests-new-dropzone input[type="file"] { font: inherit; font-size: 12.5px; color: var(--ink-3); margin: 0 auto; }

/* HTML paste area: mono, 8 rows, resizable (README §2.3) — reuses .form-input's border/radius/padding
   rather than duplicating them. */
.tests-new-html { font-family: var(--font-mono); font-size: 13px; resize: vertical; }

/* Over quota, the form is REPLACED, never disabled (README §2.3/§3.7 Rule 2) — .tests-new-grid above
   is not rendered at all in this state; this is the sole content in its place. Warning-tinted, not
   blocker: this is an expected, resolvable state (the allowance resets on its own), not a failure. */
.tests-new-quota-notice {
  display: grid; gap: var(--s2); max-width: 560px; margin: 0 auto;
  background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border);
  border-radius: var(--r-card); padding: var(--s5);
}
.tests-new-quota-notice h2 { margin: 0; font-size: 17px; font-weight: 700; color: var(--ink); }
.tests-new-quota-notice p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--tint-warning-fg); }
.tests-new-quota-notice a { color: var(--sev-suggestion); font-weight: 600; }

/* == review detail == */
/* Task 14, README §2.1: the keystone screen's own page-level chrome — order, layout, the breadcrumb
   + subject header, and the two-column grid. Every component below (.journey, .score-header,
   .cat-chips, .finding/.fix-list, .preview-box, .diff-block) was built by an earlier task and is
   unchanged here; this block owns only what sits BETWEEN them. Max width and page padding
   (1400px/26px/28px) come from .app-main (this file's app-shell block) — this page is not narrower
   than that, unlike the reviews list (1180px) and new test (1000px) shells above.

   Values (11.5px breadcrumb, 26px/700 subject, 9px meta-line margin, sticky top:150px, the diff
   summary card's 14.5px title) are taken verbatim from Priority Screens.dc.html's REVIEW DETAIL
   block, the same source every prior priority-screen task in this plan has matched pixel-for-pixel. */
/* Vertical rhythm for the whole screen (owner feedback 4.2, and residual R3 — two symptoms, one
   cause). Before this, nothing owned the spacing BETWEEN Detail.cshtml's top-level blocks: three
   components happened to carry their own margins (.rd-header-row 16px, .journey 24px, .detail-grid
   24px) and the ones that did not — .score-header, .cat-chips, and the partial-review .notice — sat
   flush against their neighbours. Measured in Chromium at 1280px, the gaps down the page read
   16 / 24 / 0 / 24: four blocks, three different spacings, and the 0 is the one the owner reported.

   R3 is the same cause seen from the other side. The partial-review banner measured 0.0px above and
   below, but it had been INVISIBLE since it was written — .notice/.notice-warning had no rules at
   all until this package's final fix wave gave them some, which is what made the missing margin
   show up. It is a top-level child of this wrapper like every other block, so one gap fixes both.

   A gap on the page rather than a margin convention on each component: a component should not have
   to know what sits beneath it, and every block added to this page from now on is spaced correctly
   without anyone remembering to give it a margin. The three ad-hoc margins are removed at their
   definitions rather than overridden here, so the page's rhythm has exactly one source of truth.

   grid-template-columns: minmax(0, 1fr) is not optional — a grid item's automatic minimum width is
   its min-content size, so a bare `display: grid` here would re-open the exact horizontal blowout
   this file has already had to fix three times below (.detail-main, .fix-list, .diff-groups), and
   .detail-grid is a child of this wrapper. */
.rd-page { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); }

.rd-header-row { display: flex; align-items: flex-start; gap: var(--s5); flex-wrap: wrap; }
.rd-header-main { min-width: 0; }
.rd-breadcrumb { font-size: 11.5px; color: var(--ink-4); margin: 0 0 7px; }
.rd-breadcrumb a { color: var(--ink-4); text-decoration: none; }
.rd-breadcrumb a:hover { text-decoration: underline; }
.rd-subject { margin: 0; font-size: 26px; font-weight: 700; letter-spacing: -0.022em; line-height: 1.2; }
.rd-meta {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 9px 0 0; font-size: 13.5px; color: var(--ink-3);
}
.rd-meta-sender { overflow-wrap: anywhere; }
.rd-meta-dot { color: var(--line-strong); }
.rd-meta-version { font-size: 12px; }
.rd-compare { margin-left: auto; flex-shrink: 0; }

/* Two columns, README §2.1 item 6: findings left (wider — 1.55fr), sticky preview + diff-summary
   right (1fr). minmax(0, ...) on BOTH tracks, not plain fr, is what lets either column shrink below
   its content's intrinsic width instead of forcing the page to scroll horizontally — the same reason
   .tests-new-grid above uses it. align-items: start is what makes the sticky right column possible at
   all: with the default stretch, the column's box would already span the full (taller) row height and
   position: sticky would have nothing left to travel. */
/* gap: var(--s5) is 24px where README §2.1 item 6 writes `gap: 20px` — a DELIBERATE snap to the
   scale, recorded here rather than left as a silent divergence (WP1.12 whole-branch review, minor 2).
   §1's own spacing rule is "4px base. Scale: 4, 8, 12, 16, 24, 32, 56", and 20px is not on it; the
   two statements in the spec disagree, and the scale is the one every other measurement in this file
   is built from. Adding a --s4h: 20px token to satisfy one gap would put a value off the declared
   scale into the token layer, which is the more expensive of the two mistakes. 24px it is. */
.detail-grid {
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: var(--s5); align-items: start;
}
/* grid-template-columns: minmax(0, 1fr) — not a bare `display: grid` — on BOTH nested grids below is
   load-bearing, not decorative. A grid item's automatic minimum width defaults to its own
   min-content size, not 0; without an explicit minmax(0, ...) track, .detail-main's single implicit
   column (and, one level deeper, .fix-list's) sizes itself to fit the widest .finding card's
   min-content rather than the width its OWN parent actually gave it. Confirmed via Playwright at
   800px: .detail-main measured exactly its 437.6px grid-track width, but .fix-list and every .finding
   inside it measured 609.9px — 172px of silent overflow spilling, unclipped, straight over the sticky
   preview column sitting at x:489.6. minmax(0, 1fr) on both closes that gap at its source instead of
   papering over it with overflow:hidden somewhere. */
.detail-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-content: start; min-width: 0; }
/* The preview column fills the visible height and the FRAME takes whatever the diff-summary card
   leaves, rather than either one carrying a fixed pixel height (owner request 2026-09-08 — "can we
   make the previewer taller"). The frame previously rendered 392px inside a 668px column on an
   813px viewport: most of the column was empty while the email scrolled inside a letterbox.

   A naive `min-height: calc(100vh - 260px)` on the frame was measured first and REJECTED: it grew
   the column to 840px against 663px of visible sticky space (100vh - top), so the diff-summary card
   underneath became permanently unreachable — a sticky element taller than its viewport slot can
   never be scrolled to. Capping the COLUMN and letting the frame flex absorbs the remainder instead,
   so the card is always reachable and the frame is always as tall as the space genuinely left over.
   That also handles the card being absent entirely (a version-1 review has no VersionDiff, so
   DiffSummary is null) without a second rule: the frame simply gets all of it.

   174px = the 150px sticky offset plus 24px so the bottom edge clears the viewport floor. */
.detail-preview {
  position: sticky; top: 150px; min-width: 0;
  /* height, not max-height: max-height caps without stretching, and .detail-grid sets
     align-items: start, so the column kept its content height and the frame had nothing to absorb
     (measured: frame stuck at its 360px floor on a 1400px-tall viewport). max() keeps a short
     window from squeezing the frame below the spec's floor plus its own chrome. */
  height: max(456px, calc(100vh - 174px));
  display: flex; flex-direction: column;
}
/* The box grows to fill the capped column; the card keeps its natural height. Scoped to
   .detail-preview so .preview-box stays inert wherever else it is composed. */
.detail-preview .preview-box { flex: 1 1 auto; }
.detail-preview .diff-summary-card { flex: 0 0 auto; }

.rd-fix-head { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.rd-fix-heading { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; }
.rd-fix-count { font-size: 11px; color: var(--ink-5); }

/* .fix-list itself carried no rule before this task — one finding card per row, .finding's own
   border/padding does the rest. See the grid-blowout comment on .detail-main above for why this
   nested grid also needs an explicit minmax(0, 1fr) track, not a bare `display: grid`. */
.fix-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s3); }

/* The compact "Versus vN" card (README §2.1 item 6) — resolved/persisting/new COUNTS only, never
   the finding titles themselves; those already live in the full diff block this card's "Full diff"
   link points at (#version-diff, on the .diff-section further down the left column), so repeating
   them here would just be the same fact said twice. Absent entirely when there is nothing to compare
   (DetailModel.DiffSummary is null on a version-1 review with no VersionDiff row) — never an empty
   or zeroed card. */
.diff-summary-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s4) var(--s5); margin-top: var(--s5);
}
.diff-summary-title { font-size: 14.5px; font-weight: 700; }
.diff-summary-counts { display: grid; gap: 7px; margin-top: var(--s3); font-size: 13px; }
.diff-summary-label { font-size: 11px; color: var(--ink-3); }
.diff-summary-label--resolved { color: var(--tint-pass-fg); }
.diff-summary-label--persisting { color: var(--tint-warning-fg); }
.diff-summary-label--new { color: var(--tint-blocker-fg); }
.diff-summary-link {
  display: inline-block; margin-top: var(--s3); font-size: 13px; font-weight: 600;
  color: var(--sev-suggestion);
}

/* Fix: .diff-groups (== diff block ==, Task 9, above) blows out its own .diff-block/.diff-section
   width at narrow viewports for the identical reason .fix-list did — repeat(3, 1fr) carries the same
   missing minmax(0, ...) as a bare `1fr` track, so a long finding title in one of the three
   resolved/persisting/new columns can widen the whole grid past its container's actual width.
   Confirmed via Playwright at 800px: with a diff-heavy review, this pushed the full diff section
   (deep in the left column) out over the sticky preview column stacked beside it — only observable
   once a real diff sits inside this task's two-column composition, which is exactly what its own
   manual-verification pass at 800px is for. Appended here rather than edited in place at Task 9's
   definition, per this file's append-only rule; equal specificity + later source order in the
   cascade is what makes this override win over the original declaration. */
.diff-groups { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Detail.cshtml wraps the "Why this score" partial's <table class="score-breakdown-table"> in this
   div — table-layout:auto (that table's default) can still grow past its own width:100% when a cell
   can't shrink below its content's min width, the same overflow class as the two grid fixes above
   but for a table instead of a grid track. Confirmed via Playwright at 800px: 524.6px table in a
   437.6px column. A horizontal scrollbar on an occasionally-wide table is the composition's problem
   to absorb, so the wrapper lives here rather than inside _ScoreBreakdown.cshtml (Task 6). */
.score-breakdown-scroll { overflow-x: auto; }

/* == wait screen == */
/* Task 16, README §2.4: the review-detail composition's Queued/Running/Failed body — rendered by
   _WaitScreen.cshtml in place of the score header + category chips for any review with no report
   yet. Named stages, not a progress bar: .stage-bar's segments and each .stage-row's own state class
   are the only "how far along" signal anywhere on this screen — nothing here ever prints a
   percentage, because the server genuinely cannot know one (DetailModel.BuildStages/WaitScreenTests). */
.wait-block {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s5); margin-bottom: var(--s4);
}
.wait-status-row { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.wait-sentence { margin: 0; font-size: 14.5px; color: var(--ink-2); }
.wait-refresh-note { margin-left: auto; color: var(--ink-5); }

.stage-bar { display: flex; gap: 2px; margin-top: var(--s5); }
.stage-segment { flex: 1 1 0; height: 6px; border-radius: 3px; background: var(--line-soft); }
.stage-segment--done { background: var(--sev-pass); }
.stage-segment--now  { background: var(--sev-suggestion); }
.stage-segment--next { background: var(--line-soft); }

/* One row per stage in a 1px-gapped grid (README §2.4) — the 1px background shows through as the
   gap, the same trick the data table's own row dividers use. */
.stage-list {
  display: grid; gap: 1px; margin-top: var(--s4); background: var(--line-soft);
  border: 1px solid var(--line-soft); border-radius: var(--r-control); overflow: hidden;
}
.stage-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 10px var(--s4); background: var(--surface);
}
/* WP1.12 whole-branch review, Important 4 — edited in place rather than appended, because the bug
   is that these rules only ever set a COLOUR: _WaitScreen.cshtml chose which of the three labels to
   emit, site.js's advanceStages rewrites only a row's className between polls, and nothing here ever
   hid .stage-next-label, so a row that advanced to done or now kept displaying the literal text
   "next" (measured at 1/6 → 3/6: three rows recoloured, all three still saying "next"). Every row now
   renders all three labels and visibility hangs off the row's state class, so the label follows the
   class automatically. Appending an override could not fix this — display:none on the two unwanted
   labels has to be the default these three rules reverse. */
.stage-found, .stage-badge, .stage-next-label { display: none; }
.stage-row--done .stage-found { display: inline; color: var(--ink-3); }
.stage-row--now  .stage-badge { display: inline; color: var(--sev-suggestion); }
.stage-row--next .stage-next-label { display: inline; }

.stage-row--done .stage-name { color: var(--sev-pass); }
.stage-row--now { background: var(--surface-2); }
.stage-row--now .stage-name { color: var(--sev-suggestion); font-weight: 700; }
.stage-row--next .stage-name,
.stage-row--next .stage-next-label { color: var(--ink-5); }

.wait-actions { display: flex; align-items: center; gap: var(--s4); margin-top: var(--s5); flex-wrap: wrap; }
.wait-actions-note { font-size: 13px; color: var(--ink-4); }

/* README §2.4: "the blocker notice... is the whole point of this screen". Its tint and box now come
   from the shared .notice / .notice-blocker pair in the "== notices ==" block at the end of this file
   (WP1.12 whole-branch review, minor 1) — Task 16 duplicated the whole treatment here because
   .notice/.notice-warning had no rules at all, which was itself the Task 14 gap this comment used to
   record and decline to fix. Two components carrying private copies of one notice treatment is how
   that becomes three. All this rule owns now is the offset to the fix list beneath it, which is
   composition, not notice styling. */
.wait-blocker-notice { margin-bottom: var(--s4); }

.wait-block--failed .wait-failed-reason { margin: var(--s3) 0 0; font-size: 14px; color: var(--ink-2); }

/* == notices == */
/* The page-level notice box, shared. Detail.cshtml's partial-review banner has rendered
   class="notice notice-warning" since Task 14 with no rules behind either class (an unstyled
   paragraph in a bordered product), and Task 16's wait-screen blocker notice then wrote its own
   private copy of the same treatment rather than depend on that gap closing — WP1.12 whole-branch
   review, minor 1. One box, one tint per severity, both callers on it.

   The tint pair is the same triple every chip and pill in this file uses (:root's --tint-*-bg /
   -border / -fg), so a notice and a chip of the same severity cannot drift apart. `border: 1px
   solid` with no colour here, colour in the variant, is what lets a variant set border-color alone
   without restating the width. */
.notice {
  padding: var(--s4) var(--s5); border-radius: var(--r-card); border: 1px solid;
  font-size: 14px; line-height: 1.55;
}
.notice-warning { background: var(--tint-warning-bg); border-color: var(--tint-warning-border); color: var(--tint-warning-fg); }
.notice-blocker { background: var(--tint-blocker-bg); border-color: var(--tint-blocker-border); color: var(--tint-blocker-fg); }
.notice strong { color: inherit; }

/* == category filter == */
/* The chip-driven fix-list filter (owner request 2026-09-15). site.js owns the behaviour; this block
   owns the button reset, the pressed state and the reset strip.

   [hidden] with !important is load-bearing, not defensive habit. The HTML `hidden` attribute works by
   setting display:none in the UA stylesheet, at specificity (0,0,0) — so ANY author rule that sets
   display beats it, and .finding/.not-assessed-group/.pass-group all declare display:grid or
   display:block. Without this, `el.hidden = true` in site.js would set the attribute and change
   nothing on screen, which is the silent kind of failure this codebase keeps paying for. !important
   rather than a higher-specificity selector because this must beat any future display rule too —
   the same reasoning normalize.css uses for the identical rule. */
[hidden] { display: none !important; }

/* A filter chip is a <button>, so the UA's button styling has to be undone before .cat-chip's own
   grid layout survives: font, colour and alignment are all inherited back, and the chip keeps the
   border/background .cat-chip already gives it. width: 100% because a grid item button would
   otherwise shrink-to-fit and break the column alignment the .cat-chips grid exists to guarantee. */
.cat-chip--filter {
  font: inherit; color: inherit; text-align: left; width: 100%;
  cursor: pointer; appearance: none;
}
.cat-chip--filter:hover { border-color: var(--line-strong); background: var(--surface-2); }

/* Pressed state keys off aria-pressed, not a class, so the announced state and the painted state
   cannot drift apart — one fact, read by the screen reader and the stylesheet alike. The left rule
   echoes the finding card's own severity rule: this chip is now the thing selecting that list. */
.cat-chip--filter[aria-pressed="true"] {
  border-color: var(--sev-suggestion);
  box-shadow: inset 3px 0 0 var(--sev-suggestion);
  background: var(--tint-suggestion-bg);
}

/* The reset strip. Suggestion-tinted like the reviews list's own active-filter notice (.dt-filter-
   notice) but scoped to this component rather than reusing that class — per this file's journey-block
   postmortem on sharing bare classes across components. */
.fix-filter {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  margin: var(--s3) 0 0; padding: 10px 14px;
  background: var(--tint-suggestion-bg); border: 1px solid var(--tint-suggestion-border);
  border-radius: var(--r-control); font-size: 13.5px; color: var(--tint-suggestion-fg);
}
.fix-filter-reset {
  font: inherit; font-weight: 600; cursor: pointer; margin-left: auto;
  background: none; border: none; padding: 0;
  color: var(--sev-suggestion); text-decoration: underline;
}

/* == oversized body refusal == */
/* The body-size refusal (Task 2.1) is the first field error to land on the .eml dropzone, and the
   dropzone is neither .form-input nor .form-select, so .form-field--error's existing rule above did
   not reach it: the invalid state rendered with the same grey dashed border as the resting state,
   while the paste door next to it turned red. Verified in the browser, not inferred — the two doors
   refuse the same email and must look equally refused.

   The sentence is also force-aligned left. .tests-new-dropzone centres its content (it is a drop
   affordance), and a centred multi-line error puts the "!" marker on its own axis away from the text
   it belongs to, which is not how .form-error reads anywhere else in the product. */
.tests-new-dropzone.form-field--error {
  border-style: solid; border-color: var(--sev-blocker); background: #fffafa;
}
.tests-new-dropzone.form-field--error .form-error { text-align: left; justify-content: flex-start; }

/* == auth screens == */
/* The centred-card shell for everything shown outside an authenticated session: sign-in, invitation
   acceptance, the no-access dead end, and the rendered 429.

   It is one treatment, not two. The 429 landed first with a private .rl-refusal copy of this exact
   card; folding it in here is the same correction the "== notices ==" block records having had to make
   after two components grew private copies of one notice. .rl-refusal-* survives only as the modifier
   classes the 429 needs on top, because that page is hand-written HTML outside Razor (the rate limiter
   rejects in middleware, before routing picks an endpoint) and cannot use the layout above.

   .brand is re-scoped rather than shared: the foundation rule is .app-bar .brand, and this card has no
   app bar — by design, since the bar carries the tenant name and signed-in address and none of these
   screens has either. */
.auth-shell {
  min-height: 100vh; display: grid; place-items: center;
  padding: var(--s5); background: var(--canvas);
}
.auth-card {
  width: min(400px, 100%); display: grid; gap: var(--s4);
  padding: var(--s6); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.auth-card .brand {
  display: flex; align-items: center; gap: var(--s2);
  font-size: 14.5px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink);
}
.auth-card .brand::before {
  content: ""; width: 11px; height: 11px; border-radius: 50%;
  background: var(--sev-suggestion);
}
.auth-title { margin: 0; font-size: 22px; line-height: 1.25; letter-spacing: -0.02em; color: var(--ink); }
.auth-intro { margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink-3); }
.auth-form { display: grid; gap: var(--s4); margin: 0; }
/* The submit is full width: it is the only action on the card, and a 96px button floating left of a
   400px card reads as one option among several when it is not. */
.auth-form .form-btn { width: 100%; text-align: center; }
.auth-card .form-error { margin: 0; }
/* Same idiom as .tests-new-quota-notice a and .dt-pager-link — this stylesheet has no base `a` rule,
   so every prose link is styled by its component, and an unstyled one falls back to the UA's
   rgb(0,0,238) underline. Seen in a browser on /no-access before this was added. */
.auth-card a { color: var(--sev-suggestion); font-weight: 600; }

/* The 429's own additions on top of the shared card — see the block comment above. */
.rl-refusal { min-height: 100vh; display: grid; place-items: center; padding: var(--s5); background: var(--canvas); }
.rl-refusal-card {
  width: min(480px, 100%); display: grid; gap: var(--s4);
  padding: var(--s6); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-card);
  font-family: var(--font-ui); color: var(--ink);
}
.rl-refusal-card .brand {
  display: flex; align-items: center; gap: var(--s2);
  font-size: 14.5px; font-weight: 700; letter-spacing: -0.01em;
}
.rl-refusal-card .brand::before {
  content: ""; width: 11px; height: 11px; border-radius: 50%; background: var(--sev-suggestion);
}
.rl-refusal-title { margin: 0; font-size: 22px; line-height: 1.25; letter-spacing: -0.02em; }
.rl-refusal-notice { margin: 0; }
.rl-refusal-body { margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink-3); }

/* == page chrome == */
/* .page-head and .muted are the backbone of every screen WP1.12 did not reach: nine pages compose
   .page-head and six compose .muted, and until now neither class had a single rule behind it. That is
   most of what "shell only" meant — correct top bar and typeface, then a raw <h1> and a <span> that
   renders at body colour and body size because nothing says otherwise.

   Shared deliberately, and this is NOT the over-sharing this file warns about elsewhere. The .btn and
   .notice cases were two components growing PRIVATE COPIES of one treatment; this is the opposite —
   nine call sites already naming one class that had no treatment at all. Giving them one is what makes
   them consistent, not what makes them entangled.

   .page-section/.page-card are the section primitives those screens need and none of them had: a
   titled band, and a surface to put content on. Settings, Users and Platform all compose them. */
.page-head {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  margin: 0 0 var(--s5);
}
.page-head h1 {
  margin: 0; font-size: 26px; line-height: 1.2; font-weight: 700; letter-spacing: -0.022em;
}
.muted { color: var(--ink-4); font-size: 13.5px; }
.page-link { color: var(--sev-suggestion); font-weight: 600; text-decoration: none; }
.page-link:hover { text-decoration: underline; }

.page-section { display: grid; gap: var(--s3); margin: 0 0 var(--s6); }
.page-section > h2 {
  margin: 0; font-size: 15.5px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-2);
}
.page-section-intro { margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink-3); }
.page-card {
  display: grid; gap: var(--s4);
  padding: var(--s5); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-card);
}

/* == settings == */
/* Only what the two cards need beyond .page-card. The screen is deliberately thin on its own rules:
   _CopyField and _QuotaMeter were already built and already composed here, and the gap was never
   those components — it was that nothing around them had any structure to sit in. */
/* Its own cap, against .app-main's 1680px. The foundation comment at .app-main grants Settings the
   wide width on the grounds that "they are data tables, which read better wide" — true of Users and
   the Platform console, and not of this screen, which is two cards of prose and a meter. At 1680px a
   40-character sentence sat on a 1250px line. Users and Platform keep the wide cap they were meant to
   have. */
.settings { max-width: 880px; }
.settings-hint { margin: 0; line-height: 1.55; }
.settings-hint code {
  font-family: var(--font-mono); font-size: 12.5px;
  background: var(--surface-3); border-radius: var(--r-chip); padding: 1px 5px;
}

/* == users == */
/* The three user screens, built on .dt and .page-card rather than the raw <table>/<label> they were.
   .dt-head/.dt-row own one grid-template-columns for the reviews list, so each other consumer brings
   its own — which is what the data-table block's own comment anticipates ("different consumers need
   different columns anyway"). Both templates use minmax(0, …) on their flexible tracks: a grid item's
   automatic minimum is its min-content size, not 0, so an unbroken email address would otherwise widen
   the whole row past the wrap's scroll container. */
.dt--users .dt-head, .dt--users .dt-row {
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 2fr) 165px 110px 120px 22px;
}
.dt--invites .dt-head, .dt--invites .dt-row {
  grid-template-columns: minmax(0, 2fr) 120px minmax(0, 1.2fr) minmax(0, 1.2fr) 110px;
}
.users-email { font-weight: 400; color: var(--ink-3); }
.users-head-action { margin-left: auto; }
.users-invites { margin-top: var(--s6); }
.users-action-error, .users-lock-note { margin: 0 0 var(--s5); }
.users-form { display: grid; gap: var(--s4); justify-items: start; }
.users-form .page-card { justify-self: stretch; }
.users-tenant-list { display: grid; gap: var(--s2); }
.users-tenant-check { display: flex; align-items: center; gap: var(--s2); font-size: 14px; color: var(--ink-2); }
.users-tenant-check input[type="checkbox"] { accent-color: var(--sev-suggestion); }
/* Same reasoning as .settings: these two are forms, not tables, so they take a measure rather than
   .app-main's 1680px. The LIST keeps the wide cap — it is a table, which is what that width is for. */
.users-detail, .users-invite { max-width: 880px; }

/* == platform console == */
/* Six internal-only screens, super-admin, never client-facing. Task 4.4 says do them cheaply and do
   not invest in bespoke layout — so this block styles what they ALREADY render rather than rewriting
   six files of documented form logic to carry new classes.

   Scoped under .app-main--platform, a single marker _Layout adds from the same `onPlatform` flag that
   already drives the inverted app bar. That is why element selectors are acceptable here and nowhere
   else in this file: they cannot escape the console. The alternative was editing every control on six
   pages, which is exactly the investment this task is told not to make.

   .data-table is a real <table> here, not the grid-based .dt. Its five call sites are all in this
   console and it had no rules at all; converting them to .dt would mean restructuring thead/tbody/tr
   into divs across five files, and the rotation-confirm and status forms on these screens carry
   semantics worth not disturbing for an internal table. */
.app-main--platform .data-table {
  width: 100%; border-collapse: collapse; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden;
  margin: 0 0 var(--s5);
}
.app-main--platform .data-table th {
  text-align: left; padding: 12px 16px;
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-4); font-weight: 400;
}
.app-main--platform .data-table td {
  padding: 12px 16px; border-top: 1px solid var(--line-faint); font-size: 14px; vertical-align: top;
}
.app-main--platform .data-table tbody tr:first-child td { border-top: none; }
.app-main--platform .data-table a { color: var(--sev-suggestion); font-weight: 600; text-decoration: none; }

.app-main--platform h2 {
  margin: var(--s6) 0 var(--s3);
  font-size: 15.5px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-2);
}
.app-main--platform form { display: grid; gap: var(--s3); justify-items: start; margin: 0 0 var(--s5); }
.app-main--platform label {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
}
.app-main--platform input[type="text"], .app-main--platform input:not([type]),
.app-main--platform input[type="email"], .app-main--platform input[type="number"],
.app-main--platform select {
  font: inherit; font-size: 14px; padding: 9px 11px; min-width: 260px;
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--surface); color: var(--ink);
}
.app-main--platform input[type="checkbox"] { accent-color: var(--sev-suggestion); }
/* The bare <button> call sites — .btn is already styled, this catches the ones with no class. */
.app-main--platform button:not([class]) {
  font: inherit; font-size: 13px; font-weight: 600; padding: 9px 14px; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--surface); color: var(--ink-2);
}
.app-main--platform button:not([class]):hover { background: var(--surface-3); }
/* p.error is live on every screen in this console and had no rules — a red-less sentence in body ink,
   indistinguishable from the prose around it. Reuses the shared blocker tint. */
.app-main--platform .error {
  margin: 0 0 var(--s3); padding: var(--s3) var(--s4);
  background: var(--tint-blocker-bg); border: 1px solid var(--tint-blocker-border);
  border-radius: var(--r-control); color: var(--tint-blocker-fg); font-size: 14px;
}
