/* RiskWire — Ledger design system.
   Hand-written, no build toolchain. One stylesheet, served as a static file.

   Palette, type, and rules are fixed by CLAUDE.md ("Design and accessibility" +
   "Ledger aesthetic"). Contrast ratios below are computed against the relevant
   background and recorded so a future change can be checked against them
   rather than re-derived from scratch. See the session report for the full
   working. */

/* ------------------------------------------------------------------ */
/* Webfonts: self-hosted woff2, no CDN request at runtime (docs/fonts.md).
   font-display: swap - Courier Prime is monospace so swap never shifts the
   register's numeric columns; the other two only reflow body copy. */
/* ------------------------------------------------------------------ */

@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/spectral-400.woff2') format('woff2');
}
@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/spectral-600.woff2') format('woff2');
}
@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/public-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/public-sans-500.woff2') format('woff2');
}
@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/public-sans-600.woff2') format('woff2');
}
@font-face {
  font-family: "Courier Prime";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/courier-prime-400.woff2') format('woff2');
}

:root {
  --paper: #EDEBE3;
  --ink: #23291F;
  --muted: #5E6552;
  --rule: #B9B5A3;
  --red: #7A2E1D;
  --amber: #8A6D1F; /* not used as text on this surface: see report (~4.1:1 on paper, fails small-text AA) */
  --green: #3F6B47;
  --highlighter: #D8D26A; /* background only, paired with --ink text (~9.5:1) */
  --fact-blue: #1D5C8F;

  /* UI vitality phase 1: ink tint ramp. The magnitude-expression primitive
     class this file lacked entirely - a pure Ink/Paper mix at six weights, so
     nothing here can resolve to Amber and nothing Amber becomes usable as
     `color` (the amber guard above and on .badge--signal below is untouched).
     color-mix(in srgb, ...) reads var(--ink)/var(--paper) at the point of use,
     so a single declaration here adapts automatically wherever it is
     consumed - but per this file's own duplication convention (the Phase 5
     banner below) the two dark blocks restate the identical formula
     explicitly against the dark Ink/Paper pair rather than relying on silent
     inheritance, and each restatement carries its own freshly computed ratio:
     the same percentage blend yields a materially different absolute colour,
     and a different ratio, once Ink and Paper swap.

     AA-safe as Ink-text-on-tint (>=4.5:1) through 32%. 48% and 64% fall
     outside that budget in at least one theme (see each step's comment) and
     are reserved here for non-text fills, large/bold text, or borders against
     Paper - not one of the four is consumed yet in this phase; they exist for
     the phase 2 magnitude components (evidence strip, stat row) the plan
     calls for. */
  --ink-4:  color-mix(in srgb, var(--ink) 4%, var(--paper));  /* Ink text on this tint: 11.60:1 */
  --ink-8:  color-mix(in srgb, var(--ink) 8%, var(--paper));  /* Ink text on this tint: 10.75:1 */
  --ink-16: color-mix(in srgb, var(--ink) 16%, var(--paper)); /* Ink text on this tint: 9.24:1 */
  --ink-32: color-mix(in srgb, var(--ink) 32%, var(--paper)); /* Ink text on this tint: 6.58:1 */
  --ink-48: color-mix(in srgb, var(--ink) 48%, var(--paper)); /* Ink text on this tint: 4.49:1 - just under the 4.5:1 body-text AA floor, so large-text/UI-component (3:1) use only. Border against Paper: 2.78:1 - fails 3:1, not usable as a light-mode border. */
  --ink-64: color-mix(in srgb, var(--ink) 64%, var(--paper)); /* Ink text on this tint: 2.91:1 - fails 3:1, fill/border use only, not a text background. Border against Paper: 4.29:1 - clears the 3:1 non-text minimum. */

  --font-display: "Spectral", Georgia, serif;
  --font-mono: "Courier Prime", "Courier New", monospace;
  --font-body: "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --space-6: 4rem; /* UI vitality phase 1: the section-level step the scale was missing. Marks a genuine break - one major section to the next, main content to the footer - never used for ordinary component spacing. The page-header-to-content gap is the smaller --space-5, set on h1 below. */

  --focus-ring: 2px solid var(--ink);
  --focus-offset: 2px;

  /* Phase 4: left-rail width. Slightly over the brief's "~64px" so the
     longest single-word labels (Register, Briefing, Sources) sit on one
     line at the rail's own smaller label size (0.75rem, set below on
     .app-nav__link) rather than wrapping mid-word. Still reads as a slim
     icon rail. */
  --rail-width: 4.5rem;

  /* --------------------------------------------------------------- */
  /* Semantic layer (Phase 2 of the UI overhaul). Flat :root additions, */
  /* no @layer: one stylesheet, nothing to arbitrate.                   */
  /* --------------------------------------------------------------- */

  /* Semantic aliases resolve 1:1 to the primitives above, so rendering
     stays byte-identical and every contrast comment above still holds.
     New work should reach for these; the primitives above stay put for
     the history/comments already written against them. */
  --surface-body: var(--paper);
  --text-primary: var(--ink);
  --text-secondary: var(--muted);
  --border-subtle: var(--rule);
  --border-strong: var(--ink);
  --accent-fact: var(--fact-blue);

  /* Guarded amber split: --signal-border is the ONLY sanctioned use of
     amber. Amber is 4.10:1 on Paper and fails small-text AA (see the
     .badge block below), so no token here may resolve to amber and be
     used as `color` on text - border only, always paired with
     --text-primary for the label. The same rule holds for any future
     health-degraded or severity-medium border: border only, never text. */
  --signal-border: var(--amber);

  /* Type scale: sizes only. Family and weight stay literal declarations
     per selector (this file's existing pattern - see the h1-h4 rule
     below); each token's comment names the face/weight it pairs with.

     UI vitality phase 1 rebuild: assertive-broadsheet altitude (plan decision
     6). --type-h1 is retired - the diagnosis was that a genuine display size
     was used on exactly one selector in the whole file, so the surface h1 now
     takes --type-display directly, the same token the phase 2 stat row will
     pair with Courier Prime. --type-h4 moves from 0.95rem to 1.05rem to fix
     the group-heading inversion (it must never sit below the 1rem body it
     heads - see .band-row__label/.horizon-band__heading below for the small-
     caps furniture built on top of this size). The former 0.85/0.9/0.95/1.05
     sub-body mush consolidates to three purposeful sizes - --type-label,
     --type-supporting, --type-micro (new) - plus --type-h4 moving up into
     heading territory and --type-data staying deliberately body-equal (see
     its own comment). */
  --type-display: 2.75rem;   /* Spectral 600 - surface h1 (page title) and the entry-detail filled header; also the target size for the phase 2 Courier Prime stat row, not built yet */
  --type-h2: 1.5rem;         /* Spectral 600 - section headings */
  --type-h3: 1.25rem;        /* Spectral 600 - sub-section headings, card/item titles, compact heading moments (error summary, entry reference, nav brand) */
  --type-h4: 1.05rem;        /* UI vitality phase 4: no plain <h4> tag remains anywhere in the app - Horizon item titles moved to h3 alongside proposal titles (docs/ui-vitality-plan.md workstream H, "the same object at two sizes on one page"). This token now serves group-heading furniture only: Public Sans 700 small-caps (.band-row__label, .horizon-band__heading, below), above the 1rem body it heads */
  --type-body: 1rem;         /* Public Sans 400 */
  --type-label: 0.85rem;     /* Public Sans 600 - chips, badges, tag-list pills, compact form labels */
  --type-supporting: 0.9rem; /* Public Sans 400 - hints, captions, meta lines, substantive secondary prose */
  --type-micro: 0.75rem;     /* Public Sans 400/600 - true metadata only: rail labels, timestamps, attribution lines, table sub-notes - never primary content */
  --type-data: 1rem;         /* Courier Prime 400 - pair with tabular-nums; deliberately body-equal, not part of the sub-body consolidation above */
}

/* ------------------------------------------------------------------ */
/* Phase 5: dark theme ("night ledger"), additive to the semantic layer */
/* above. Default is auto - the OS preference via prefers-color-scheme, */
/* pure CSS, no cookie/JS required. An explicit choice always beats the */
/* OS: data-theme="dark" applies regardless of OS, and the @media block */
/* is guarded with :not([data-theme="light"]) so a saved "light" choice */
/* overrides a dark OS. The two blocks below carry identical overrides  */
/* for that reason - this file has no mixins/@apply without a build     */
/* step, so the values are duplicated; keep both in sync by hand.       */
/*                                                                       */
/* Every dark value is a genuine second derivation, never a hue flip     */
/* (fact-blue and the severity red/green are lightened to read on a     */
/* dark surface). Ratios are computed the same way as every other ratio */
/* in this file (WCAG relative luminance). Primitives are overridden     */
/* directly, not only their Phase 2 semantic aliases: several component */
/* rules elsewhere in this file (buttons, inputs, .chip/.badge variants, */
/* the band marker, the briefing/horizon accents) reference var(--ink), */
/* var(--red), var(--green), var(--rule), var(--muted) or var(--fact-   */
/* blue) directly rather than through a semantic token. Because the     */
/* semantic aliases are themselves declared once in :root as e.g.       */
/* `--text-primary: var(--ink)` and never redeclared, redefining the    */
/* primitive here is enough for both the alias and every direct use to  */
/* pick up the dark value automatically - no component rule changes.    */
/* --highlighter is the one primitive deliberately left unchanged: it   */
/* is a literal highlighter-yellow accent (the .chip--draft "draft"     */
/* chip), not a themed surface, so it stays the same bright colour in   */
/* both themes - see the .chip--draft override near that rule for why  */
/* its ink pairing is pinned rather than left to invert with --ink.     */
/* ------------------------------------------------------------------ */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #17190F;
    --ink: #E9E6DA;
    --muted: #A6A392;
    --rule: #3C3E31;
    --red: #D9866B;
    --amber: #CBA24A;
    --green: #7FB187;
    --fact-blue: #82B4DD;

    /* Semantic layer, restated per Phase 2's own token list so the dark
       derivation is self-documenting (each already follows from the
       primitives above via the untouched :root alias, e.g.
       --surface-body: var(--paper) - this restates rather than
       reintroduces them). */
    --surface-body: var(--paper);
    --text-primary: var(--ink);         /* 14.2:1 on page */
    --text-secondary: var(--muted);     /* 7.0:1 on page; ~6.4:1 on the iso-chain/proposed-link inset (--ink-4, UI vitality phase 1 - was an inline color-mix formula, now the ramp token; same value, still adapts if either input changes) */
    --border-subtle: var(--rule);       /* ~1.6:1 - decorative divider ONLY, parity with the light --rule; never a control boundary */
    --border-strong: var(--ink);        /* 14.2:1 - control boundaries, 2px rules, the filled-tab edge */
    --accent-fact: var(--fact-blue);    /* 8.0:1 on page, 7.5:1 on the rail */
    --signal-border: var(--amber);      /* 7.4:1 on page, 6.9:1 on the rail - border-only, same guard as light: never used as a text colour (see .badge--signal) */

    /* Rail-only raised surface (no light-mode equivalent - the rail there
       stays --surface-body via the fallback in .app-nav below). Text-
       primary on it: ~13.2:1. */
    --surface-raised: #1E2018;

    /* UI vitality phase 1: ink tint ramp, restated against the dark Ink/
       Paper pair - see the :root comment for why this is duplicated rather
       than left to inherit. Genuinely a second derivation, not a hue flip:
       dark Ink is a light colour on a near-black Paper, so the same
       percentage blend moves the ramp in the opposite visual direction
       (lighter as the percentage rises, not darker) and produces different
       ratios at every step, not just inverted ones - 48% in particular
       clears the 3:1 border minimum against Paper here (4.16:1) where it
       fails in light mode (2.78:1). */
    --ink-4:  color-mix(in srgb, var(--ink) 4%, var(--paper));  /* Ink text on this tint: 13.03:1 */
    --ink-8:  color-mix(in srgb, var(--ink) 8%, var(--paper));  /* Ink text on this tint: 11.76:1 */
    --ink-16: color-mix(in srgb, var(--ink) 16%, var(--paper)); /* Ink text on this tint: 9.23:1 */
    --ink-32: color-mix(in srgb, var(--ink) 32%, var(--paper)); /* Ink text on this tint: 5.51:1 */
    --ink-48: color-mix(in srgb, var(--ink) 48%, var(--paper)); /* Ink text on this tint: 3.41:1 - fails 4.5:1 body text, clears 3:1 large-text/UI-component. Border against Paper: 4.16:1 - clears 3:1 (unlike light mode's 48% step). */
    --ink-64: color-mix(in srgb, var(--ink) 64%, var(--paper)); /* Ink text on this tint: 2.20:1 - fails 3:1, fill/border only. Border against Paper: 6.44:1 - strong; Paper-coloured text on this fill (the inverted pairing .register-row--open already uses elsewhere) is the same 6.44:1 and clears body-text AA. */
  }
}

:root[data-theme="dark"] {
  /* Mirrors the @media block above exactly - see there for the full
     derivation and every ratio; duplicated by hand, no mixins available. */
  --paper: #17190F;
  --ink: #E9E6DA;
  --muted: #A6A392;
  --rule: #3C3E31;
  --red: #D9866B;
  --amber: #CBA24A;
  --green: #7FB187;
  --fact-blue: #82B4DD;

  --surface-body: var(--paper);
  --text-primary: var(--ink);
  --text-secondary: var(--muted);
  --border-subtle: var(--rule);
  --border-strong: var(--ink);
  --accent-fact: var(--fact-blue);
  --signal-border: var(--amber);
  --surface-raised: #1E2018;

  /* Mirrors the @media block above exactly - see there for the full
     derivation and every ratio; duplicated by hand, no mixins available. */
  --ink-4:  color-mix(in srgb, var(--ink) 4%, var(--paper));  /* Ink text on this tint: 13.03:1 */
  --ink-8:  color-mix(in srgb, var(--ink) 8%, var(--paper));  /* Ink text on this tint: 11.76:1 */
  --ink-16: color-mix(in srgb, var(--ink) 16%, var(--paper)); /* Ink text on this tint: 9.23:1 */
  --ink-32: color-mix(in srgb, var(--ink) 32%, var(--paper)); /* Ink text on this tint: 5.51:1 */
  --ink-48: color-mix(in srgb, var(--ink) 48%, var(--paper)); /* Ink text on this tint: 3.41:1 - fails 4.5:1 body text, clears 3:1 large-text/UI-component. Border against Paper: 4.16:1 - clears 3:1 (unlike light mode's 48% step). */
  --ink-64: color-mix(in srgb, var(--ink) 64%, var(--paper)); /* Ink text on this tint: 2.20:1 - fails 3:1, fill/border only. Border against Paper: 6.44:1 - strong; Paper-coloured text on this fill (the inverted pairing .register-row--open already uses elsewhere) is the same 6.44:1 and clears body-text AA. */
}

/* ------------------------------------------------------------------ */
/* Reset (minimal, no framework)                                       */
/* ------------------------------------------------------------------ */

*, *::before, *::after { box-sizing: border-box; }

html { color-scheme: light; }
/* Phase 5: native controls (scrollbars, form widgets) follow the same
   auto-vs-explicit precedence as the token blocks above. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) { color-scheme: dark; }
}
html[data-theme="dark"] { color-scheme: dark; }

body {
  margin: 0;
  background: var(--surface-body);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--type-body);
  line-height: 1.55;
  /* The page itself never scrolls horizontally; only the table region does. */
  overflow-x: hidden;
}

img, svg { max-width: 100%; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
  color: var(--text-primary);
  margin: var(--space-4) 0 var(--space-2);
}
/* UI vitality phase 1: the surface h1 takes --type-display directly (the
   plan's "give each surface a display-size moment") rather than a separate,
   smaller --type-h1 token - see the type scale comment above. margin-bottom
   is the new "page header block to content" structural gap (--space-5,
   deployed here for the first time beyond .horizon-band): every surface's
   page title is a bare h1 (briefing.html, horizon.html, sources.html,
   login.html, register_new.html, horizon_create_entry.html, not_found.html)
   or sits inside .page-heading (register_list.html) - in the flex-row case
   this margin still contributes to that container's cross-axis size, so the
   gap below the whole header block still lands correctly without a template
   change. .entry-heading h1 overrides margin to 0 (register_detail.html):
   that page's spacing is handled by the .entry-heading tab itself, so it is
   unaffected. */
h1 { font-size: var(--type-display); margin-top: var(--space-3); margin-bottom: var(--space-5); }
h2 { font-size: var(--type-h2); border-bottom: 1px solid var(--border-subtle); padding-bottom: var(--space-1); }
h3 { font-size: var(--type-h3); }
h4 { font-size: var(--type-h4); } /* UI vitality phase 4: no template uses a bare <h4> any more (the last one, Horizon item titles, moved to h3); kept as the base style for the tag itself, matching h1-h3's own always-present rules, in case one is added later */

p { margin: 0 0 var(--space-3); }

a {
  color: var(--accent-fact); /* 5.90:1 on Paper, verified */
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
a:hover { color: var(--text-primary); }
a:visited { color: var(--accent-fact); }

/* ------------------------------------------------------------------ */
/* Focus states: visible everywhere, keyboard first.                   */
/* ------------------------------------------------------------------ */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* Fallback for browsers without :focus-visible support: still visible. */
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.skip-link {
  position: absolute;
  left: -999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
  z-index: 100;
}
.skip-link:focus {
  position: fixed;
  left: var(--space-3);
  top: var(--space-3);
  width: auto;
  height: auto;
  padding: var(--space-2) var(--space-3);
  background: var(--paper);
  color: var(--ink);
  border: 2px solid var(--ink);
  outline: none;
  font-family: var(--font-body);
  font-weight: 600;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------------ */
/* Layout / landmarks                                                  */
/* ------------------------------------------------------------------ */

/* Phase 4: left-rail navigation shell. body.app-shell is the grid; the
   --nav modifier (added by base.html only when request.state.user_id is
   set - the same guard the rail's <nav> itself is wrapped in) is what
   reserves the rail column, so the unauthenticated login page still lays
   out as a single column with no dead gutter where the rail would be. No
   @layer: this stays one flat sheet per the Phase 2 guardrail. No JS:
   grid + sticky positioning only. */
body.app-shell {
  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "main"
    "footer";
  min-height: 100vh;
}
body.app-shell--nav {
  grid-template-columns: var(--rail-width) 1fr;
  grid-template-areas:
    "rail main"
    "rail footer";
}

/* The rail spans both grid rows (level with main and, below it, the
   footer) so it reads as permanent shell chrome rather than a page-top
   banner. Sticky + a full viewport height keeps it pinned while the
   column beside it scrolls; overflow-y is a safety net if a very short
   viewport combined with large-text zoom ever makes the rail's own
   content (brand + 4 links + sign out) taller than the screen, so every
   item stays reachable by scrolling the rail itself rather than being
   clipped.

   Sticky guard (WCAG 2.4.11): this is the only sticky element introduced
   by this phase in the wide layout, and it is a LEFT column, not a top
   bar - it sits beside main's vertical flow, never above it, so it cannot
   obscure a keyboard-focused row the way a sticky top bar would, and
   needs no scroll-padding-top. It also never coincides with the
   register's own sticky thead (below, "Register table"): that stays
   sticky to the viewport top exactly as before, since nothing here adds
   overflow-y to any ancestor between <body> and .table-scroll. Under the
   narrow breakpoint the rail becomes a static (non-sticky) horizontal bar
   instead of a sticky one - see that media query - specifically so this
   file never has two elements competing for the same top:0 slot. */
.app-nav {
  grid-area: rail;
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--surface-raised, var(--surface-body)); /* Phase 5: falls back to --surface-body (today's value) when --surface-raised is unset, i.e. light mode - byte-identical there. Dark mode defines --surface-raised as a distinct, slightly lighter tone so the rail reads as raised shell chrome; see the dark token block above. */
  border-right: 2px solid var(--border-strong);
  padding: var(--space-3) 0;
}

.app-nav__brand {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem; /* 44px at the 16px root, the same tap-target floor as every other control in this file */
  text-align: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-h3); /* was a stray 1.3rem; nearest scale step per the phase 1 token audit (1.25rem) - a one-off wordmark, not a document heading, so it borrows the size rather than adding a token for it */
  color: var(--text-primary); /* 12.49:1 on Paper, verified above */
  text-decoration: none;
  margin: 0 var(--space-2) var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
}
.app-nav__brand:hover { text-decoration: underline; }

.app-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.app-nav__list li { margin: 0; }

.app-nav__link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 2.75rem; /* 44px at the 16px root, the Briefing/Horizon tap-target precedent */
  margin: 0 var(--space-2);
  padding: var(--space-2) var(--space-1);
  border-left: 3px solid transparent; /* same left-border-carries-state pattern as .register-row, below */
  color: var(--text-primary); /* 12.49:1 on Paper, verified above */
  text-decoration: none;
  font-family: var(--font-body);
  font-size: var(--type-micro); /* true rail chrome, not a reading size - smaller than --type-label (0.85rem) deliberately: the rail is narrower than any other chip/badge context that token is used in */
  font-weight: 600;
  text-align: center;
  overflow-wrap: break-word;
}
/* Hover and current-page tints now read the ink tint ramp (--ink-4, added UI
   vitality phase 1) rather than an ad-hoc color-mix: 11.60:1 Ink-on-tint,
   the same figure already verified when this was written inline, so no new
   ratio to record. The current-page left border is the one genuinely new
   pairing here: Fact-blue (#1D5C8F) on this same 4% tint (~#E5E3DB) computes
   to ~5.48:1 (down slightly from the 5.90:1 Fact-blue-on-Paper already
   verified above, since the tint is a hair darker than Paper) - comfortably
   past the 3:1 non-text/UI-component minimum this border needs (WCAG
   1.4.11). It is a decoration on top of aria-current + the always-visible
   text label, not the thing carrying the state, so colour is never the sole
   carrier either way. */
.app-nav__link:hover {
  background: var(--ink-4);
}
.app-nav__link[aria-current="page"] {
  border-left-color: var(--accent-fact);
  background: var(--ink-4);
}
/* Phase 5: in dark mode the rail sits on --surface-raised, not
   --surface-body (see .app-nav above), so these two tints mix into
   whichever surface the rail is actually rendered on. The fallback
   reproduces the two rules above exactly where --surface-raised is
   unset, i.e. light mode - no change there. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .app-nav__link:hover,
  :root:not([data-theme="light"]) .app-nav__link[aria-current="page"] {
    background: color-mix(in srgb, var(--text-primary) 4%, var(--surface-raised, var(--surface-body)));
  }
}
:root[data-theme="dark"] .app-nav__link:hover,
:root[data-theme="dark"] .app-nav__link[aria-current="page"] {
  background: color-mix(in srgb, var(--text-primary) 4%, var(--surface-raised, var(--surface-body)));
}

.app-nav__icon { display: block; }
.app-nav__label { display: block; line-height: 1.2; }

.app-nav__logout {
  margin: 0 var(--space-2) 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid var(--border-subtle);
}
/* Reuses the shared button element's border and hover-invert as-is (2px Ink
   border; Ink/Paper invert on hover, 12.49:1, verified above) - only the
   layout changes, so there is no new colour pairing here. */
.app-nav__logout button {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  font-size: var(--type-micro); /* matches .app-nav__link's rail-scoped label size */
  min-height: 2.75rem;
}

/* Phase 5: theme toggle (Light/Dark), placed directly above Sign out at
   the foot of the rail. Carries the auto top-margin that used to live on
   .app-nav__logout alone (moved here, above): this form now absorbs the
   free space in the flex column, pushing itself AND Sign out (which
   follows it in markup, with no auto-margin of its own any more) to the
   bottom together. A real <form method="post" action="/theme">, POSTing
   one of the two buttons' name="theme" value - works with CSS/JS entirely
   absent, exactly like every other form in this rail. theme.js
   (progressive enhancement, /static/theme.js) intercepts the submit for
   an instant, no-reload flip; see that file for the no-JS/JS split. */
.app-nav__theme {
  margin: auto var(--space-2) 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid var(--border-subtle);
}
.app-nav__theme-fieldset {
  border: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
/* Same layout as .app-nav__logout button, above, for the same reason:
   full rail width, icon above label, the same 44px tap-target floor used
   throughout this rail (comfortably over the 24px WCAG 2.5.8 minimum).
   The pressed (active) theme is shown by the existing
   button[aria-pressed="true"] rule (Forms section, above) - the same
   Ink-fill/Paper-text inversion the entry-heading tab and the Confirm
   button already use for an "on"/primary state, applied here for the
   first time; no new colour pairing. */
.app-nav__theme-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  /* The rail is narrower than the global button padding assumes: at
     var(--space-3) a side, these two stack icon-above-label and still
     cannot shrink below their own min-content, so they overflowed the
     rail and raised a stray horizontal scrollbar on it. Side padding buys
     nothing in a centred column layout, so it goes; min-width: 0 lets the
     flex item shrink to the track it is actually given. The 44px
     min-height tap target (WCAG 2.5.8) is untouched. */
  min-width: 0;
  padding-left: var(--space-1);
  padding-right: var(--space-1);
  font-size: var(--type-micro);
  min-height: 2.75rem;
}

/* The shell fills whatever viewport it is given: no centred fixed-width
   column, so a wide monitor gets a wide register rather than bare paper
   down both sides. min-width: 0 is load-bearing - a grid item defaults to
   min-width: auto, which lets the register table's own min-width (52rem,
   "Register table" below) push this column wider than its track and clip
   the last columns off-screen. Zeroing it hands the overflow back to
   .table-scroll, which is the element that is supposed to scroll. */
main {
  grid-area: main;
  min-width: 0;
  padding: var(--space-4);
}

/* Running text keeps a readable measure even though the shell no longer
   caps width; tables, toolbars and panels are free to use the full span.
   Line length is a legibility rule, not a layout one. */
main > p,
main > .field-hint {
  max-width: 78ch;
}

.site-footer {
  grid-area: footer;
  min-width: 0;
  border-top: 1px solid var(--rule);
  margin-top: var(--space-6); /* UI vitality phase 1: bumped from --space-5 to the new section-level step - leaving the main content area is the largest structural break on the page */
}
.site-footer__inner {
  padding: var(--space-4);
  color: var(--muted);
  font-size: var(--type-micro); /* chrome, not content */
}

/* Collapse to a horizontal top bar under a narrow viewport. A plain flex
   reflow rather than a <details> disclosure: <details> would need the
   `open` attribute hardcoded so no-CSS/no-JS visitors and this breakpoint
   both start with every link reachable, at which point it would not
   actually save any space unless a visitor finds and taps the summary -
   an extra control for a benefit nobody gets by default. Reflowing keeps
   every link visible with nothing to discover, wrapping onto a second row
   on the narrowest phones instead. Not sticky (see the guard above): this
   keeps it out of contention with the register's sticky thead entirely,
   matching today's behaviour where the top bar it replaces was not sticky
   either. */
@media (max-width: 48rem) {
  body.app-shell--nav {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
      "rail"
      "main"
      "footer";
  }

  .app-nav {
    position: static;
    height: auto;
    overflow-y: visible;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--space-1) var(--space-2);
    border-right: none;
    border-bottom: 2px solid var(--border-strong);
    padding: var(--space-2);
  }
  .app-nav__brand {
    margin: 0 var(--space-2) 0 0;
    padding: 0 var(--space-2) 0 0;
    border-bottom: none;
    border-right: 1px solid var(--border-subtle);
  }
  .app-nav__list { flex-direction: row; flex-wrap: wrap; justify-content: center; }
  .app-nav__link { margin: 0; }
  .app-nav__logout { margin: 0; padding: 0; border-top: none; }
  .app-nav__logout button { width: auto; padding-left: var(--space-2); padding-right: var(--space-2); }
  /* Phase 5: same reset as .app-nav__logout, immediately above, for the
     same reason - this becomes one more wrapped item in the horizontal
     bar rather than a bottom-pushed column item. The fieldset switches to
     a row (matching .app-nav__list's own column-to-row switch here) so
     the two buttons sit side by side instead of stacked. */
  .app-nav__theme { margin: 0; padding: 0; border-top: none; }
  .app-nav__theme-fieldset { flex-direction: row; }
  .app-nav__theme-btn { width: auto; padding-left: var(--space-2); padding-right: var(--space-2); }
}

/* ------------------------------------------------------------------ */
/* Forms                                                               */
/* ------------------------------------------------------------------ */

fieldset {
  border: 1px solid var(--rule);
  padding: var(--space-3) var(--space-4) var(--space-4);
  margin: 0 0 var(--space-4);
}
legend {
  font-family: var(--font-display);
  font-weight: 600;
  padding: 0 var(--space-2);
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: var(--space-1);
}

.field-hint {
  color: var(--text-secondary); /* 5.09:1 on Paper, verified */
  font-size: var(--type-supporting);
  margin: 0 0 var(--space-2);
}

.field-error {
  color: var(--red); /* 7.87:1 on Paper, verified */
  font-weight: 600;
  font-size: var(--type-supporting);
  margin: 0 0 var(--space-2);
  border-left: 3px solid var(--red);
  padding-left: var(--space-2);
}

/* app/briefing.py::_worded(): the worded 404/409/400 an htmx confirm/dismiss
   can get back (most reachably the "already decided" 409 the two-doors race
   produces - red-team, fix iteration 1). Same red 7.87:1 pairing as
   .field-error above, restated under its own class rather than reused
   verbatim: .field-error is a specific field's inline validation message,
   this is a whole action's outcome, swapped into an existing
   aria-live="polite" region (#briefing-inbox-region /
   #register-awaiting-region) - which is also why the template carries no
   role="alert" of its own (see _worded()'s docstring): a nested alert region
   risks announcing the same text twice. */
.worded-alert {
  color: var(--red); /* 7.87:1 on Paper, verified above */
  font-weight: 600;
  border-left: 3px solid var(--red);
  padding: var(--space-2) 0 var(--space-2) var(--space-2);
  margin: 0;
}

input[type="text"],
input[type="date"],
select,
textarea {
  font-family: var(--font-body);
  font-size: var(--type-body);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ink);
  padding: var(--space-2);
  width: 100%;
  max-width: 40rem;
}
textarea { min-height: 5rem; resize: vertical; }

.form-row { margin-bottom: var(--space-3); }

.filter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-3) var(--space-4);
  align-items: end;
}
.filter-grid select { max-width: none; }

/* Phase 3: the register list's filter toolbar reads as a compact row of
   bordered controls, not a plain fieldset box. Overrides the shared
   fieldset border above with a single 2px ink rule beneath the whole
   toolbar; each control keeps its own thin border so the row reads as
   controls, not bare labels. UI vitality phase 4 (workstream H): Horizon's
   two filter fieldsets (_horizon_sections.html) now carry this same class,
   so both surfaces' filter forms read as the same generation of control -
   nothing here was ever register-specific, the class hook had just only
   been added to one template. */
.filter-toolbar {
  border: none;
  border-bottom: 2px solid var(--border-strong);
  padding: 0 0 var(--space-4);
  margin: 0 0 var(--space-4);
}
.filter-toolbar legend { padding: 0 0 var(--space-1); }
.filter-toolbar .filter-grid { margin-top: var(--space-2); }
.filter-toolbar .form-row {
  border: 1px solid var(--border-subtle);
  padding: var(--space-2);
  margin-bottom: 0;
}
.filter-toolbar .form-row label {
  font-size: var(--type-label);
  margin-bottom: var(--space-1);
}
.filter-toolbar .form-row select {
  border: none;
  padding: var(--space-1) 0 0;
  width: 100%;
}
.filter-toolbar .button-row { margin-top: var(--space-3); }

button,
.button {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--type-body);
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--ink);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}
button:hover,
.button:hover { background: var(--ink); color: var(--paper); }

button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.error-summary {
  border: 2px solid var(--red);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
  background: var(--paper);
}
.error-summary h2 {
  margin-top: 0;
  border-bottom: none;
  color: var(--red); /* 7.87:1 on Paper, verified */
  font-size: var(--type-h3); /* was a stray 1.1rem; now the token that already matched that value */
}
.error-summary ul { margin: 0; padding-left: 1.2em; }

/* ------------------------------------------------------------------ */
/* Chips: colour is never the only carrier; text is always present.    */
/* ------------------------------------------------------------------ */

/* UI vitality phase 1 alignment pass: chip and badge (below) both go
   inline-flex with align-items:center and the same explicit min-height, so
   the two render at an identical box height despite the badge's thicker 2px
   border and different padding/line-height (both deliberate: badges carry
   the FACT/SIGNAL classification and are meant to read as heavier). Without
   this, a chip and a badge sitting on the same line - the FACT/SIGNAL badge
   next to a watch-flag chip - visibly stepped by a few pixels. tabular-nums
   here because several chip variants carry a count (.chip--overdue's
   "N consecutive failures", .band-row__count below); Courier Prime cells
   elsewhere in this file are already monospace and are left alone. */
.chip {
  display: inline-flex;
  align-items: center;
  min-height: 1.6rem;
  font-family: var(--font-body);
  font-size: var(--type-label);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--rule);
  color: var(--ink);
  padding: 0.1em 0.55em;
  white-space: nowrap;
  line-height: 1.5;
}

.chip--close-watch { border-color: var(--red); color: var(--red); } /* 7.87:1 */
.chip--normal { border-color: var(--ink); color: var(--ink); } /* 12.49:1 */
.chip--background { border-color: var(--rule); color: var(--muted); } /* 5.09:1 */

.chip--confirmed { border-color: var(--green); color: var(--green); } /* 5.17:1 */

/* "Returning to attention" (docs/ui-vitality-plan.md workstream F, phase 5
   frontend; the full colour argument is in _returning_chip.html, which every
   call site includes). Reuses .chip--confirmed's exact pairing rather than
   inventing a new one - evidence resuming reads as confirmed activity, not
   an alarm - so the light-mode ratio above (5.17:1) already covers it. The
   dark-mode ratio is computed fresh here because it had not previously been
   recorded against Paper as a TEXT colour (only against --ink-8, for
   .band-row__marker--healthy below): dark Green #7FB187 on dark Paper
   #17190F measures 7.22:1, comfortably AA. */
.chip--returning { border-color: var(--green); color: var(--green); }

.chip--draft {
  border-color: var(--ink);
  background: var(--highlighter);
  color: var(--ink); /* 9.47:1 on Highlighter, verified */
}
/* Phase 5: --highlighter is not redefined in dark (see the dark token
   block) - it stays the same bright accent in both themes - so the ink
   border/text paired with it must not follow --ink to its dark-mode
   (light) value, or both collapse to ~1.3:1 against the still-bright
   fill. Pinned to the light-mode ink hex directly: keeps the 9.47:1
   pairing above exactly as verified, unchanged, in both themes. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chip--draft {
    border-color: #23291F;
    color: #23291F;
  }
}
:root[data-theme="dark"] .chip--draft {
  border-color: #23291F;
  color: #23291F;
}

/* Two-class information badges (FACT / SIGNAL). Always carry the word, never
   colour alone. FACT uses Fact-blue as both border and text (5.90:1 on Paper,
   the same pairing already verified for links above, so it passes AA on its
   own). SIGNAL is different: Amber text on Paper measures ~4.10:1 and Ink text
   on an Amber fill measures ~3.04:1 - neither clears the 4.5:1 body-text
   threshold with this fixed token set, only the 3:1 large-text/UI-component
   one. So Amber is used for the border only (the state cue) and the label
   itself is set in Ink on Paper (12.49:1, comfortably AA), matching the
   border-carries-colour / text-stays-legible pattern already used by
   .chip--background elsewhere in this file.

   Phase 2 hardens this split into the token names themselves: --signal-border
   resolves to Amber and must only ever be used as a border; the label stays
   --text-primary (Ink). Nothing in this file defines a token that resolves
   to Amber and is usable as `color`. Phase 3 firms up the badge itself
   (bigger padding, tighter letter-spacing) - still square-cornered and
   bordered, never filled or shadowed. */
.badge {
  display: inline-flex; /* UI vitality phase 1: matches .chip's box model above so the two sit at an identical rendered height - see that rule's comment */
  align-items: center;
  min-height: 1.6rem;
  font-family: var(--font-body);
  font-size: var(--type-label);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 2px solid;
  padding: 0.15em 0.7em; /* was 0.25em; tightened so the natural box (with the 2px border) lands at the same ~1.6rem floor as .chip's 1px border instead of rendering visibly taller */
  line-height: 1.3; /* was 1.4, same reason as the padding change above */
}
.badge--fact { border-color: var(--accent-fact); color: var(--accent-fact); } /* 5.90:1 */
.badge--signal { border-color: var(--signal-border); color: var(--text-primary); } /* 12.49:1; Amber is the border cue only, see note above */

.value-mono { font-family: var(--font-mono); }

.tag-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}
.tag-list li {
  border: 1px solid var(--rule);
  padding: 0.1em 0.5em;
  font-family: var(--font-mono);
  font-size: var(--type-label);
}
.tag-list--empty {
  font-family: var(--font-body);
  color: var(--muted);
  font-style: italic;
}

/* ------------------------------------------------------------------ */
/* UI vitality phase 2: shared quantity/time components. Used across    */
/* Briefing, Register and Horizon - see docs/ui-vitality-plan.md phase  */
/* 2, "Build these" 1, 2, 3, 5, 7 and docs/ui.md's component section.   */
/* Every colour here is an EXISTING token (the ink tint ramp, the       */
/* semantic aliases), so nothing below needs a new dark-theme override: */
/* the tokens already carry their own dark derivation.                  */
/* ------------------------------------------------------------------ */

/* --- Evidence strip: twelve weekly cells, fixed absolute scale ---
   (docs/ui-vitality-plan.md section 6: never per-row normalised, so this
   cannot read as a score). The generic `table` rule earlier in this file
   sets width:100%/min-width:52rem for the register/sources tables; both
   are wrong for a 12-cell strip and are overridden here. Every cell keeps
   a real 1px border regardless of fill, so the grid survives forced-colors
   mode unaided (background is stripped there; border is not) - no entry is
   needed in the forced-colors block below for that reason. The count
   itself is never colour-only: a visually-hidden span per cell states it
   in words (_evidence_strip.html), and a worded summary line always
   renders beneath the strip (or in its place, below n=5). */
.evidence-strip {
  width: auto;
  min-width: 0;
  table-layout: fixed;
  border-collapse: collapse;
  margin: 0 0 var(--space-1);
}
.evidence-strip__cell {
  width: 0.8rem;
  height: 0.8rem;
  padding: 0;
  border: 1px solid var(--border-subtle);
}
.evidence-strip__cell--0 { background: transparent; }
.evidence-strip__cell--1 { background: var(--ink-16); } /* fill vs Paper: 1.35:1 light / 1.54:1 dark - under the 3:1 non-text minimum; acceptable because the same count is also stated in a visually-hidden span per cell (WCAG 1.4.11 does not apply where a text alternative already carries the information), and every cell's 1px border is the structural carrier regardless of fill */
.evidence-strip__cell--2 { background: var(--ink-32); } /* fill vs Paper: 1.90:1 light / 2.58:1 dark - same note as above */
.evidence-strip__cell--3 { background: var(--ink-64); } /* fill vs Paper: 4.29:1 light / 6.44:1 dark - clears the 3:1 non-text minimum on its own; the same ratio already recorded for --ink-64 in docs/ui.md's ramp table */

.evidence-summary {
  margin: 0;
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
  font-size: var(--type-micro);
}

/* Beside the salience note on the entry detail page and the row-detail
   fragment; zeroed to nothing extra inside .row-detail, where the flex
   `gap` already spaces every reused component (see that rule, above). */
.entry-evidence { margin: 0 0 var(--space-3); }
.row-detail .entry-evidence { margin: 0; }

/* --- Provenance strip: the Techmeme cluster (_provenance.html) ---
   Every badge inside reuses .badge--fact/.badge--signal as-is (no new
   colour), so the only new pairing here is the muted relative-age text. */
.provenance {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
  display: grid;
  gap: var(--space-1);
  font-size: var(--type-supporting);
}
.provenance__line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
}
/* The Techmeme cluster shape: the lead source (evidence[0]) sits flush;
   every corroborating line indents beneath it with its own rule, so a
   four-source item is visibly heavier than a one-source item before a
   word is read (docs/ui-vitality-plan.md section 2: "cluster size is the
   signal"). */
.provenance__line:not(:first-child) {
  padding-left: var(--space-4);
  border-left: 2px solid var(--border-subtle);
}
.provenance__source { font-weight: 600; }
.provenance__relative { color: var(--text-secondary); } /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
.provenance-empty {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-style: italic;
  margin: 0 0 var(--space-2);
}

/* --- Watch flag: chip for the exceptional state, plain text for the
   ordinary two (_watch_flag_chip.html; docs/ui-vitality-plan.md phase 2,
   "Build these" item 7, citing GOV.UK's tag-component research). Reuses
   .chip--close-watch as-is; these two are new but reuse the exact colour
   pairings .chip--normal/.chip--background already carried, so neither
   is a new ratio. */
.watch-flag-text { font-weight: 600; color: var(--text-primary); } /* 12.49:1 light / 14.2:1 dark, same pairing as the former .chip--normal */
.watch-flag-text--background { font-weight: 400; color: var(--text-secondary); } /* 5.09:1 on Paper, same pairing as the former .chip--background */

/* Wraps the Watch flag cell's flag text/chip and the "Returning to
   attention" chip (docs/ui-vitality-plan.md workstream F, phase 5) so the
   two never crowd against the column edge: wraps onto its own line rather
   than forcing the column wider or the text to truncate. No layout change
   for the overwhelming majority of rows, where returning is false and this
   wraps a single child exactly as the bare text did before it. */
.watch-flag-cell {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

/* --- Stat row: figures at display size, small-caps labels beneath ---
   A <dl>: dt (label) precedes dd (figure) in the DOM, a real name/value
   pair for assistive tech, and column-reverse only swaps the VISUAL order
   so the figure sits on top (docs/ui-vitality-plan.md phase 2, "Build
   these" item 3). Hairline rules between groups via border-right. */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  margin: 0 0 var(--space-4);
}
.stat-row__item {
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-start;
  gap: var(--space-1);
  padding-right: var(--space-4);
  margin-right: var(--space-4);
  border-right: 1px solid var(--border-subtle);
}
.stat-row__item:last-child { padding-right: 0; margin-right: 0; border-right: none; }
.stat-row__figure {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--type-display);
  font-weight: 400;
  line-height: 1;
}
.stat-row__figure a { color: var(--text-primary); } /* 12.49:1 light / 14.2:1 dark on Paper, the same Ink-on-Paper pairing verified above; overridden from the default Fact-blue link colour so the figure reads as data, not a stray blue number - the underline inherited from the global `a` rule is what still marks it as a link, so colour is never the only cue */
.stat-row__label {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 700;
  font-variant-caps: small-caps;
  text-transform: lowercase;
  letter-spacing: 0.03em;
  font-size: var(--type-micro);
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
}

/* --- Dateline: the cheapest signal of life, under every surface h1 ---
   (_dateline.html; docs/ui-vitality-plan.md phase 2, "Build these" item
   5). */
.page-dateline {
  margin: 0 0 var(--space-4);
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
  font-size: var(--type-supporting);
}
.page-dateline__figure { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* --- Briefing band grouping: register table band-row's grammar, adapted
   to a heading line for the card list rather than a table row
   (_briefing_inbox.html; docs/ui-vitality-plan.md phase 2, "Build these"
   item 7). .band-row__bar/__marker/__label/__count are reused verbatim
   from the register table - none of them depend on table markup - so
   there is no new colour pairing to record here. */
.briefing-band { margin: 0 0 var(--space-5); }

/* --- Horizon date/magnitude phrasing ("overdue by N days", "due in N
   days") - docs/ui-vitality-plan.md phase 2, "Build these" item 6. */
.horizon-item__magnitude { color: var(--text-secondary); } /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */

/* --- The materiality fold (Step 2; migration 0012_item_materiality_band).
   One divider component shared verbatim by all three call sites -
   _briefing_inbox.html (within each watch_flag band), _horizon_watch.html
   (within each urgency band) and _horizon_proposals.html (within each age
   band) - so the fold reads as one idea wherever it appears rather than three
   near-identical ad hoc rules. A <p>, not a heading: see each template's own
   comment for why (Horizon's item titles and band headings already both sit
   at h3; a new subheading there has no clean slot without either colliding
   with that level or pushing row titles out of hierarchy, and the Briefing
   equivalent stays a <p> too, for consistency across the two surfaces rather
   than for its own hierarchy reason). Dashed rather than solid top border so
   it never reads as a second .horizon-band__heading/.band-row__bar rule at a
   glance - this divides WITHIN a band, not between two bands.

   Colour is never the sole carrier: __label and __hint are both words (the
   fold's existence and its reason - "background materiality" - are stated,
   not implied by position or shade alone), and __count restates the number
   the two <ol>s either side of it already make countable, so a screen-reader
   user landing on this paragraph without having read the list first still
   gets the figure. No new colour pairing, but note WHICH existing pairing:
   .fold-divider sits directly on the page's own Paper background, not on
   .band-row__bar's --ink-8 tint, so __label/__count reuse the file's base
   --text-primary/Ink-on-Paper text (12.49:1 light / 14.2:1 dark - the same
   ratio as ordinary body text and the h1-h4 default, not .band-row__label's
   own 10.75:1/11.76:1, which is that same Ink colour measured against its
   tinted background instead). __hint reuses --text-secondary/Muted-on-Paper
   (5.09:1 light / 7.0:1 dark, verified above at .page-dateline, which is
   also on plain Paper). */
.fold-divider {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
  margin: var(--space-1) 0 var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px dashed var(--rule);
}
.fold-divider__label {
  flex: none;
  font-family: var(--font-body);
  font-weight: 700;
  font-variant-caps: small-caps;
  text-transform: lowercase;
  letter-spacing: 0.03em;
  font-size: var(--type-h4);
  color: var(--text-primary); /* 12.49:1 light / 14.2:1 dark: plain Ink-on-Paper (this sits on the page background, not .band-row__bar's --ink-8 tint) */
}
.fold-divider__hint {
  font-size: var(--type-supporting);
  color: var(--text-secondary); /* 5.09:1 light / 7.0:1 dark, reused .page-dateline pairing (also plain Paper) */
}
.fold-divider__count {
  margin-left: auto;
  flex: none;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--type-label);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-primary); /* 12.49:1 light / 14.2:1 dark: plain Ink-on-Paper, same note as __label above */
}
/* The folded list itself carries no different box model from its unfolded
   sibling (.briefing-list/.horizon-list, both further down this file): the
   fold changes WHERE a row sits, never how the row or the list around it is
   built, so .briefing-list--folded/.horizon-list--folded are template hooks
   (for tests and any future rule) rather than a styled difference today. */

/* ------------------------------------------------------------------ */
/* Register table                                                      */
/* ------------------------------------------------------------------ */

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--rule);
}

table {
  border-collapse: collapse;
  width: 100%;
  min-width: 52rem; /* forces the wrapper's scrollbar rather than the page's */
}

caption {
  text-align: left;
  font-family: var(--font-display);
  font-weight: 600;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--rule);
}

/* UI vitality phase 1 alignment pass: this file has two deliberately
   different row densities, not an accidental inconsistency. This base rule
   (0.5rem/1rem vertical/horizontal padding) is Sources' density - a shorter,
   less dense board where comfortable reading beats scan density, and it is
   also the register table's own header row and the fallback for any table
   without the .register-table hook. .register-table below tightens the
   vertical padding to 0.4rem deliberately: the register can run to many rows
   per watch_flag band, so it is a denser scanning table by purpose, and the
   tighter padding is what lets a band's rows read as a group at a glance. */
th, td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

thead th {
  position: sticky;
  top: 0;
  background: var(--paper);
  border-bottom: 2px solid var(--ink);
  font-family: var(--font-body);
  font-weight: 700;
}

.th-note {
  display: block;
  font-weight: 400;
  font-size: var(--type-micro);
  color: var(--muted); /* 5.09:1 on Paper, verified */
}

tbody th[scope="row"] a {
  font-family: var(--font-mono);
}

/* Phase 3: band bar. UI vitality phase 1 migrates the background from a
   hardcoded 6% Ink-in-Paper rgba() literal (which could not follow the
   theme on its own, hence the two dark-only override rules that used to
   live here) onto the ink tint ramp's nearest step, --ink-8 - "consolidate
   to nearest, don't add a token per stray value", the same rule this phase
   applies to every hardcoded rem size, applied here to a hardcoded colour.
   --ink-8 is a var()-based color-mix, so it now adapts to dark mode
   automatically through the ramp's own dark restatement (see the :root
   token block); the two dark-only overrides that used to sit below this
   rule are gone, nothing else needed to change for dark mode to render
   correctly.

   Ink text on --ink-8: 10.75:1 light / 11.76:1 dark - safely AA, so the
   band label stays Ink (see .band-row__label below for the small-caps
   furniture added this phase to fix the group-heading inversion). Muted
   text on --ink-8 measures 4.38:1 light: clearly under the 4.5:1 AA text
   floor (it was already a marginal ~4.55:1 on the old 6% tint, and 8%
   pushes it further under, not closer), which is why the entry count stays
   Ink rather than Muted, as before. Only the marker square below still
   carries the per-band colour, and only as a non-text, aria-hidden
   decoration alongside the always-visible text label - colour is never the
   sole carrier. The bottom edge is intentionally left to the generic th/td
   rule above (1px Rule), so only the top border is set here. */
tr.band-row th {
  background: var(--ink-8);
  color: var(--text-primary);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-h4);
  text-transform: none;
  border-top: 1px solid var(--border-strong);
}

/* The bar is a flex row INSIDE the th (not the th itself): a th with
   display:flex loses its colspan and collapses to one column, so the flex
   must live on a child element that fills the full-width table cell. */
.band-row__bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.band-row__marker {
  display: inline-block;
  flex: none;
  width: 9px;
  height: 9px;
}
.band-row__marker--close_watch { background: var(--red); border: 1px solid var(--red); }
.band-row__marker--normal { background: transparent; border: 2px solid var(--border-strong); }
.band-row__marker--background { background: transparent; border: 2px solid var(--text-secondary); } /* border only: needs 3:1 non-text, not 4.5:1 text - measures 4.38:1 on --ink-8, see note above */

/* UI vitality phase 1: the group-heading fix. --type-h4 alone (0.95rem, now
   1.05rem) is not the whole answer here - this is the actual "broadsheet
   answer" the plan calls for: small caps + letterspacing on lowercase text
   (never uppercase - GOV.UK found uppercase tags harder to read) stands in
   for size, with the hairline rule each of these two selectors already has
   (this row's own border-top above; .horizon-band__heading's border-bottom
   below) doing the rest of the "this is a group, not a paragraph" work.
   Public Sans, not the Spectral serif h1-h4 use elsewhere, because small-
   caps synthesis (neither face ships real small-cap glyphs) reads more
   cleanly on a sans face, and this is furniture, not a headline. text-
   transform:lowercase runs every letter through the small-caps synthesis
   evenly, rather than leaving an oversized initial capital from the source
   copy's own sentence case ("Close watch band", "Overdue"). Colour is
   --text-primary, the same 12.49:1/14.2:1 Ink-on-Paper pairing verified
   above - both selectors also sit on their own backgrounds (the --ink-8 band
   row above; plain Paper for .horizon-band__heading), already covered by
   the ratios stated at each. */
.band-row__label,
.horizon-band__heading {
  flex: none;
  font-family: var(--font-body);
  font-weight: 700;
  font-variant-caps: small-caps;
  text-transform: lowercase;
  letter-spacing: 0.03em;
  font-size: var(--type-h4);
  color: var(--text-primary);
}

.band-row__count {
  margin-left: auto;
  flex: none;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--type-label);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* --- Briefing band heading: the "N new" note (phase 3 workstream C
   frontend follow-up; _briefing_inbox.html). Reuses .band-row__count's own
   Ink-on-ink-8 pairing verbatim (10.75:1 light / 11.76:1 dark, verified
   above at tr.band-row th) - no new colour pairing, since nothing here sets
   its own color. What makes it read as a distinct, secondary note rather
   than a second total is typographic, matching .band-row__label's own
   "furniture, not a headline" convention: small-caps on lowercase text
   instead of .band-row__count's own uppercase tabular figures. Middle dot
   separator in the template, never an em-dash (CLAUDE.md copy rule). */
.band-row__count-new {
  font-variant-caps: small-caps;
  text-transform: lowercase;
}

/* Phase 3: register-table-specific density and per-band lanes. Scoped to a
   class hook on <table> (_register_table.html) so Sources' table
   (app/templates/sources.html), which shares the same base table/th/td
   rules and also has a th[scope="row"] index column, is unaffected. UI
   vitality phase 1: this deliberately stays a different density from
   Sources' - see the rationale on the base th/td rule above. */
.register-table td,
.register-table th {
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}

.register-row > th[scope="row"] {
  border-left: 3px solid transparent;
  padding-left: calc(var(--space-3) - 3px);
}
.register-row--close_watch > th[scope="row"] { border-left-color: var(--red); }
.register-row--normal > th[scope="row"] { border-left-color: var(--border-strong); }
.register-row--background > th[scope="row"] { border-left-color: var(--border-subtle); }

.register-table tbody th[scope="row"] a {
  font-weight: 600;
}

/* Row expansion ("uncollapse into the info"): the reference cell's <a>
   doubles as an accordion toggle (JS: register-expand.js). Icon + text sit
   in a flex row so the chevron and the mono reference align on one line;
   colour/weight are unchanged from the rule above - this only adds layout
   and the rotating icon. No-JS visitors never see aria-expanded flip: the
   href still navigates straight to the full entry. */
.register-row__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.register-row__toggle[aria-busy="true"] { cursor: progress; }

.register-row__chevron { flex: none; }
.register-row__toggle[aria-expanded="true"] .register-row__chevron {
  transform: rotate(90deg);
}

/* Open row: the clicked row becomes its own filled "tab", directly above
   the detail panel _register_row_detail.html inserts below it (see
   .register-detail-row further down). This reuses the exact Ink-fill /
   Paper-text inversion already verified for .entry-heading and
   button[aria-pressed="true"] elsewhere in this file (12.49:1 light /
   14.2:1 dark, both already stated above) - not a new pairing, and because
   it is expressed purely in the semantic tokens, dark mode inverts it
   automatically with no extra rule, exactly like .entry-heading.

   Five things inside the row have their OWN explicit colour, tuned for a
   Paper background, and would otherwise go illegible or vanish against
   this Ink fill, so each gets a narrow override to the same reused
   Paper-on-Ink pairing:
     - the reference link itself (normally --accent-fact, Fact-blue);
     - the Title cell's link, added for the open-affordance fix (also
       --accent-fact by default - measured at 2.12:1 on this Ink fill,
       nowhere near AA, so it needs the same override as the reference link);
     - the watch-flag cell (UI vitality phase 2: close_watch's chip needs
       the same override as any other .chip, above; normal/background are
       now plain text, .watch-flag-text/.watch-flag-text--background,
       which would be just as invisible on this fill without their own
       matching override, further below);
     - the review date cell (tbody tr:not(.band-row) td:last-child sets
       --text-secondary/Muted, ~2.45:1 on this fill - fails even the 3:1
       non-text minimum);
     - the focus ring on either of the two links above: --focus-ring is
       `2px solid var(--ink)`, and this row's fill is --text-primary
       (also var(--ink)), so an unmodified ring is Ink-on-Ink - 1:1, no
       visible indicator at all - and --focus-offset sits the ring inside
       the filled cell, so it never escapes onto Paper. Same Paper-on-Ink
       swap as the two link rules below, so still 3:1+ (in fact 12.49:1
       light / 14.2:1 dark, comfortably clear of the 2.4.11 minimum)
       against the adjacent Ink fill.
   Every other cell (Level, Directorate, the Domains tag-list, the Evidence
   strip) sets no colour of its own, so it already inherits the Paper text
   colour from the cell rule below with nothing extra needed; the tag-list's
   border (Rule on Ink, ~7.2:1) stays comfortably clear of the 3:1 non-text
   minimum too, and the evidence-strip cells' borders are the same
   --border-subtle/--rule token against the same fill, so the same ~7.2:1
   figure covers them without a separate computation. */
.register-row--open > th,
.register-row--open > td {
  background: var(--text-primary);
  color: var(--surface-body);
}
.register-row--open > th a,
.register-row--open > th a:hover,
.register-row--open > th a:visited,
.register-row--open > td a,
.register-row--open > td a:hover,
.register-row--open > td a:visited {
  color: var(--surface-body); /* Paper on Ink: 12.49:1 light / 14.2:1 dark, same pairing as .entry-heading above - not new */
}
/* Same Paper-on-Ink swap as the colour override above, applied to the
   focus ring instead of the link text: --focus-ring is `2px solid
   var(--ink)` and this row's fill is Ink too, so left alone the ring is
   invisible (1:1) against its own cell. Width/offset are untouched -
   only the outline colour changes - and :focus-visible/:focus are paired
   the same way as the global rule at the top of this file, for browsers
   without :focus-visible support. */
.register-row--open > th a:focus-visible,
.register-row--open > td a:focus-visible,
.register-row--open > th a:focus,
.register-row--open > td a:focus {
  outline-color: var(--surface-body); /* Paper on Ink: 12.49:1 light / 14.2:1 dark, same pairing as above - not a new ratio */
}
.register-row--open .chip {
  border-color: var(--surface-body);
  color: var(--surface-body); /* Paper on Ink: 12.49:1 light / 14.2:1 dark, same pairing as .entry-heading above - not new */
}
/* UI vitality phase 2: the Watch flag column's "normal"/"background" states
   are now plain text (.watch-flag-text, above), not a .chip - the rule
   above does not reach them. Left unhandled, the default Ink-on-Paper text
   colour would sit on this row's own Ink fill (1:1, invisible), the exact
   bug the .chip override above already exists to prevent for close_watch's
   chip. Same Paper-on-Ink swap, same 12.49:1 light / 14.2:1 dark pairing -
   not a new ratio. */
.register-row--open .watch-flag-text,
.register-row--open .watch-flag-text--background {
  color: var(--surface-body);
}
/* Same gap, same fix, for the Evidence column's summary line
   (.evidence-summary, above): it sets its own explicit Muted text colour,
   which would read at roughly the same ~2.45:1 failure already noted for
   the review-date column below without this override. The evidence-strip
   TABLE needs no equivalent override - its cells set their own fixed-scale
   background regardless of ancestor, so they are unaffected by the row's
   fill either way. */
.register-row--open .evidence-summary {
  color: var(--surface-body); /* Paper on Ink: 12.49:1 light / 14.2:1 dark, same pairing as .entry-heading above - not new */
}

.register-table tbody tr:not(.band-row) td:last-child {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-data);
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
}
/* Matches the specificity of the rule immediately above (3 classes, 3
   types via .register-table/tbody/tr/:not(.band-row)/td/:last-child) so
   this wins by coming later in source order rather than needing
   !important - it must stay below that rule for that reason. The only
   other place this file relies on !important is the reduced-motion
   override further down, kept for a genuine global guarantee rather than
   a routine component override like this one. */
.register-table tbody tr.register-row--open td:last-child {
  color: var(--surface-body); /* Paper on Ink: 12.49:1 light / 14.2:1 dark, same pairing as .entry-heading above - not new */
}

/* :not(.register-row--open) - an open row's own Ink fill (below) would
   otherwise be fought over by this rule whenever the pointer rests on it
   (higher specificity than the fill rule, since :hover adds a class); an
   already-expanded row reads as "active" without a hover tint on top, and
   staying stable under the pointer is the more legible behaviour anyway.
   :not(.register-detail-row) - the inserted detail panel is a plain
   content row, not a link-like affordance, so it never took this tint
   either; excluded explicitly now that a second new row type exists. */
tbody tr:not(.band-row):not(.register-detail-row):not(.register-row--open):hover {
  background: color-mix(in srgb, var(--highlighter) 35%, var(--paper));
}

.result-count {
  margin: 0 0 var(--space-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums; /* UI vitality phase 1: these counts swap via htmx (register filters, Horizon's two sections) and jittered without this */
}

/* ------------------------------------------------------------------ */
/* Empty states (UI vitality plan phase 3, workstream G). A bordered      */
/* panel with a Spectral heading, body copy and (where honest) a next-   */
/* step link, replacing the old single italic line on all four surfaces  */
/* plus register_detail.html's own unclassed "No changes recorded yet."  */
/* Per NN/g's three rules (quoted in docs/ui-vitality-plan.md workstream */
/* G): communicate system status, provide a learning cue, provide a       */
/* direct path to the key task. Every colour here is an EXISTING token   */
/* (the semantic aliases, --green), so nothing below needs a new dark-   */
/* theme override - each already carries its own dark derivation. Never  */
/* rendered as the pre-swap htmx default: every instance is an actual    */
/* server response (see each fragment template), per the plan's own      */
/* warning about htmx and empty states.                                  */
/* ------------------------------------------------------------------ */
.empty-state {
  border: 1px solid var(--border-subtle);
  padding: var(--space-4);
  margin: 0 0 var(--space-4);
}
/* Briefing's quiet-day state only: a completed task, not an absence, so
   it earns the same green accent .briefing-item__announcement already
   uses for a successful verdict outcome - reusing that 5.17:1-on-Paper
   pairing, not a new one. */
.empty-state--success { border-left: 3px solid var(--green); }
.empty-state__heading {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-h3);
  margin: 0 0 var(--space-2);
  color: var(--text-primary);
}
.empty-state__body { margin: 0 0 var(--space-2); }
/* The register empty state's format illustration (_register_table.html,
   included by register_list.html):
   small-caps furniture, the same visual language .stat-row__label already
   uses for "this is furniture, not content" (5.09:1 on Paper, verified
   above), so the worked example reads as clearly labelled rather than as
   a seeded entry. */
.empty-state__example-label {
  margin: var(--space-2) 0 var(--space-2);
  font-family: var(--font-body);
  font-weight: 700;
  font-variant-caps: small-caps;
  text-transform: lowercase;
  letter-spacing: 0.03em;
  font-size: var(--type-micro);
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
}
.empty-state__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  margin: var(--space-2) 0 0;
}
.empty-state .iso-chain-panel { margin: 0 0 var(--space-2); }

/* UI vitality phase 1: "register filter block vs table" structural gap
   (plan workstream A). Targets the persistent aria-live wrapper directly
   (register_list.html) rather than the form that precedes it, so it applies
   regardless of what sits above it in the DOM and needs no template change;
   the aria-live region itself is untouched (still never the swap target -
   see docs/ui.md section 6). */
#register-table-region {
  margin-top: var(--space-6);
}

/* ------------------------------------------------------------------ */
/* Register row expansion: the accordion detail panel                  */
/* (_register_row_detail.html, inserted by register-expand.js directly  */
/* under the clicked row; app/register.py's GET /register/{ref}/row is  */
/* the only source of this markup). Bracketed top and bottom by         */
/* --border-strong rather than the table's usual 1px --rule, so it      */
/* reads as an inset panel distinct from an ordinary row - both are real */
/* `border` properties, so the bracket survives forced-colors mode      */
/* unaided (unlike the fill-only states above and .entry-heading, which  */
/* both need an explicit forced-colors border - see that media query).  */
/* Surface/text stay the plain body pairing (Paper/Ink, 12.49:1 light /  */
/* 14.2:1 dark, already verified above) - not a new pairing, just        */
/* restated explicitly since nothing else on this row would otherwise    */
/* set it. The <td> itself is never display:flex (that drops its        */
/* colspan, as the band-row bug already showed) - .row-detail, a plain   */
/* child <div>, carries the flex layout instead.                        */
/* ------------------------------------------------------------------ */

.register-detail-row > td {
  background: var(--surface-body);
  color: var(--text-primary);
  border-top: 2px solid var(--border-strong);
  border-bottom: 2px solid var(--border-strong);
  padding: var(--space-4);
}

.row-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Reused components (.entry-meta-line, .iso-chain-panel, .salience-note)
   each carry their own bottom margin for the full detail page's vertical
   flow; zeroed here so the single flex `gap` above is the only spacing
   between them, rather than the two stacking. Purely spacing - no colour
   or semantic change, so this does not affect how these classes render on
   register_detail.html itself (scoped under the .row-detail ancestor). */
.row-detail .entry-meta-line,
.row-detail .iso-chain-panel,
.row-detail .salience-note {
  margin: 0;
}

.row-detail__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
}

/* Groups the heading with the "Returning to attention" chip
   (docs/ui-vitality-plan.md workstream F, phase 5) as one flex item, so
   .row-detail__head's space-between above still pins "Open full entry" to
   the right rather than spreading three items evenly across the row. This
   panel is plain Paper (.register-detail-row > td below), so the chip keeps
   its green with no ink-fill override needed, unlike the full detail page's
   filled .entry-heading tab (see .entry-status-line above). */
.row-detail__heading-group {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
}

/* Overrides the shared h1-h4 rule's margin/border-bottom/size for this one
   compact, transient heading; family/weight/colour (Spectral 600, Ink) are
   inherited unchanged, so this is a layout-only override, not a new text
   colour. tabindex="-1" (set in the template) is what register-expand.js
   focuses after inserting the row, so the outline below is not
   decorative - it is the visible focus indicator WCAG requires for that
   focus move, and it reuses --focus-ring (2px Ink), the same token every
   other focus state in this file uses. */
.row-detail__heading {
  margin: 0;
  padding-bottom: 0;
  border-bottom: none;
  font-size: var(--type-h3);
}
.row-detail__heading:focus-visible,
.row-detail__heading:focus {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* Fix 3 (register-entry-open-affordance): was --type-supporting and easy to
   miss. Now reads as the deliberate way out to the full page - body-size,
   semibold, with a trailing chevron (aria-hidden; the word "Open full
   entry" still carries the meaning on its own, so colour/icon are never the
   sole signal). Still a real <a>, not a button, and still the same
   --accent-fact link colour every other link in this file uses (5.90:1 on
   Paper, verified above) - no new colour pairing. */
.row-detail__open-link {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-body);
  font-size: var(--type-body);
  font-weight: 600;
  white-space: nowrap;
}
.row-detail__open-link-chevron { flex: none; }

/* ------------------------------------------------------------------ */
/* Entry detail                                                        */
/* ------------------------------------------------------------------ */

.back-link { display: inline-block; margin-bottom: var(--space-2); }

/* Phase 3: the entry header as a filled "tab" - background Ink, text Paper.
   This reuses the Ink/Paper pairing already verified at 12.49:1 above (the
   same pairing button:hover / button[aria-pressed="true"] already use
   inverted), just applied to a new component, so there is no new ratio to
   record. No left-edge spine border by design: an earlier version tried one
   and it read as an indent rule in review, so the fill itself is the "tab". */
.entry-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  background: var(--text-primary);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
}
.entry-heading h1 {
  margin: 0;
  color: var(--surface-body);
  font-size: var(--type-display);
}
.entry-heading__ref {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--type-h3); /* was a stray 1.1rem; the token that already matched that value, now a touch larger (1.25rem) alongside the bigger h1 beside it */
  color: var(--surface-body); /* Paper on Ink: 12.49:1, see note above */
}

/* "Returning to attention" (docs/ui-vitality-plan.md workstream F, phase 5):
   the entry detail's own status line, straight below the dateline and
   deliberately OUTSIDE .entry-heading above - see the template comment at
   its call site (register_detail.html) for why: anything nested in that
   filled Ink tab needs a Paper-on-Ink override to stay legible (compare
   .register-row--open .chip further down), which would strip this chip of
   the green .chip--returning is built on. Sitting on the page's own Paper
   keeps that colour meaningful. */
.entry-status-line { margin: 0 0 var(--space-4); }

dl.entry-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
}
dl.entry-meta dt { font-weight: 600; }
dl.entry-meta dd { margin: 0; }
dl.entry-meta dd.value-mono { font-family: var(--font-mono); }

/* Phase 3: register-detail-only one-line meta strip (level, directorate,
   owner, status, review date). A new class, not a change to dl.entry-meta
   above: that class is shared with horizon_create_entry.html and
   sources.html and stays exactly as it rendered before this change. */
dl.entry-meta-line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  margin: 0 0 var(--space-4);
}
dl.entry-meta-line > div {
  display: flex;
  gap: var(--space-1);
  margin: 0;
}
dl.entry-meta-line dt { font-weight: 600; margin: 0; }
dl.entry-meta-line dd { margin: 0; }
dl.entry-meta-line dd.value-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.iso-chain {
  display: grid;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
}

/* Phase 3: inset bordered panel for the ISO chain on the register detail
   page only (a new wrapper class; .iso-chain / .iso-chain--compact below
   are untouched, so Horizon's compact chain - _horizon_proposal_row.html -
   is unaffected). UI vitality phase 1 migrates the background onto the ink
   tint ramp's --ink-4 step, the same formula this rule already used inline
   - no visual change, just a named token. Ink text on it measures 11.60:1
   (safely AA, same method as the 12.49:1 figure above). .iso-chain__connector
   is Muted elsewhere (5.09:1 on Paper, verified above) but Muted on this
   tint measures only ~4.73:1: too close to the 4.5:1 text minimum to rely on
   given this file's "verify before use" standard for Muted, so it is set to
   Ink here, scoped to this panel only - the shared rule Horizon uses is
   untouched. */
.iso-chain-panel {
  background: var(--ink-4);
  border: 1px solid var(--border-subtle);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
}
.iso-chain-panel .iso-chain { margin-bottom: 0; }
.iso-chain-panel .iso-chain__connector { color: var(--text-primary); }

.salience-note {
  color: var(--text-secondary); /* 5.09:1 on Paper, verified above (same pairing as .field-hint) */
  font-size: var(--type-supporting);
  margin: var(--space-3) 0 0;
}
.salience-note__label { font-weight: 600; color: var(--text-primary); }
.iso-chain__step {
  border-left: 3px solid var(--rule);
  padding: var(--space-1) 0 var(--space-1) var(--space-3);
}
.iso-chain__label {
  font-family: var(--font-display);
  font-weight: 600;
  margin: 0 0 var(--space-1);
}
.iso-chain__connector {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-size: var(--type-micro); /* a connector word ("leads to"), not content */
  margin: 0 0 var(--space-2) var(--space-3);
}
.iso-chain__value { margin: 0; }

/* ------------------------------------------------------------------ */
/* Watch-flag control: a labelled inline select, one change to apply.  */
/* Phase 3 converts this from three buttons to a <select> (still one    */
/* tap/change to apply, still no rationale, still band-not-score); see   */
/* _watch_flag_control.html for the no-JS submit-button fallback.       */
/* The Horizon watch-window control keeps the earlier layout: four      */
/* states as buttons in a fieldset, one tap, always a visible           */
/* current-value chip in text.                                          */
/* ------------------------------------------------------------------ */

.watch-window-control fieldset {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  border: none;
  padding: 0;
}
.watch-window-control legend {
  padding: 0;
  width: 100%;
  margin-bottom: var(--space-2);
}
.watch-flag-current,
.watch-window-current {
  font-weight: 600;
  margin: 0 0 var(--space-3);
}
.watch-flag-control__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
}
.watch-flag-control__row label { margin: 0; }
.watch-flag-select { max-width: 14rem; }

/* ------------------------------------------------------------------ */
/* Update history                                                       */
/* ------------------------------------------------------------------ */

.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.history-item {
  border: 1px solid var(--rule);
  padding: var(--space-3);
}
.history-item__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
}
.history-item__timestamp {
  font-family: var(--font-mono);
  font-size: var(--type-micro); /* a timestamp, not content */
  color: var(--muted); /* 5.09:1 on Paper, verified */
}
.history-item__rationale { margin: 0 0 var(--space-2); }
.history-item__changes {
  margin: 0 0 var(--space-2);
  padding-left: 1.2em;
  font-family: var(--font-mono);
  font-size: var(--type-supporting); /* what actually changed - substantive, unlike the timestamp above */
}
.history-item__changes li { margin-bottom: var(--space-1); }

/* ------------------------------------------------------------------ */
/* Briefing (daily triage inbox)                                        */
/* ------------------------------------------------------------------ */

.briefing-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
}

/* Phase 3 (UI vitality plan workstream D item 1): collapsed by default,
   modelled verbatim on register_detail.html's .linked-item__toggle - read
   that implementation and its CSS before changing this block. Padding now
   lives on .briefing-item__toggle/.briefing-item__body below rather than on
   the outer border, mirroring .linked-item's own box model, so the border
   hugs just the summary line collapsed and grows to include the body once
   open. */
.briefing-item {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink);
}

.briefing-item__announcement {
  border-left: 3px solid var(--green);
  padding: var(--space-2) var(--space-3);
  margin: var(--space-3) var(--space-4) 0;
  font-weight: 600;
  color: var(--green); /* 5.17:1 on Paper, verified above */
}

/* The collapsed toggle row: badge, title, right-aligned meta cluster (lead
   source, relative published time) - same flex layout, focus handling and
   marker-hiding as .linked-item__toggle, just under the .briefing-item__
   namespace this file already uses. This is a REAL <details>/<summary>
   disclosure, not a JS accordion: unlike the register table (whose rows
   cannot nest inside <details>, hence register-expand.js), a <li> can, so
   the no-JS path needs no script at all here. Multiple items may be open
   at once - there is no accordion constraint, because nothing inside an
   item's body carries a fixed, unsuffixed ID a second open copy could
   collide with (contrast _watch_flag_control.html). */
.briefing-item__toggle {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
}
.briefing-item__toggle::-webkit-details-marker { display: none; }
.briefing-item__chevron { flex: none; }
.briefing-item__details[open] > .briefing-item__toggle .briefing-item__chevron {
  transform: rotate(90deg);
}
/* Same reasoning as .linked-item__toggle:focus-visible above: a plain
   <summary> does not inherit the global a/button/input focus rule, so the
   ring needs stating explicitly here too. */
.briefing-item__toggle:focus-visible,
.briefing-item__toggle:focus {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}
/* --- "New since your last visit" marker (phase 3 workstream C frontend
   follow-up; _briefing_item.html, before the FACT/SIGNAL badge). Reuses
   .band-row__marker's exact 9px square rather than inventing a second
   marker size, but filled Ink, not any band or tier colour: Fact-blue is
   reserved for the FACT badge (invariant 1 - a newness marker in Fact-blue
   would read as a tier signal), and none of the three watch_flag colours
   mean "new" either. No contrast ratio to record: this is a solid
   aria-hidden decoration, not text, and WCAG 1.4.11 non-text contrast does
   not apply where a text alternative already carries the information - the
   adjacent visually-hidden "New since your last visit" span is that
   alternative, so colour/shape is never the sole carrier. align-self:
   center corrects for .briefing-item__toggle's align-items:baseline, which
   would otherwise sit a plain filled square on its own bottom edge rather
   than centred against the text beside it. */
.briefing-item__new-marker {
  display: inline-block;
  align-self: center;
  flex: none;
  width: 9px;
  height: 9px;
  background: var(--ink);
}

.briefing-item__toggle-badge { flex: none; }
.briefing-item__toggle-title { font-weight: 600; }
.briefing-item__toggle-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-left: auto;
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-size: var(--type-micro); /* peripheral meta on the collapsed summary row, matching .linked-item__toggle-meta's own reasoning */
  font-variant-numeric: tabular-nums;
}

.briefing-item__body {
  padding: 0 var(--space-4) var(--space-3);
}

/* The clickable title, kept OUT of <summary> above and repeated here in
   full - same reasoning as .linked-item__title/.linked-item__toggle-title:
   interactive content inside a <summary> is HTML-spec-discouraged and would
   leave a genuine click ambiguity (follow the link, or toggle the
   disclosure?). */
.briefing-item__title { font-weight: 600; margin: 0 0 var(--space-1); }

.briefing-item__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  color: var(--text-secondary); /* 5.09:1 on Paper, verified */
  font-size: var(--type-supporting);
}

/* P4 digest (docs/pipeline-p4.md; app/digests.py; _briefing_item.html):
   the item's MAIN BODY prose, given the featured-content treatment rather
   than one more plain paragraph - so it reads as what the item is about, not
   an addendum bolted underneath. Matches, rather than invents: the exact
   background/border pairing .proposed-link and .iso-chain-panel already use
   for "this block is the substantive content, not peripheral meta"
   (--ink-4 tint + --border-subtle border). Ink text on --ink-4 is 11.60:1
   (verified once, above, for .iso-chain-panel; the same pairing, not a new
   ratio). Absent entirely when there is no digest (_briefing_item.html), so
   an item with none renders exactly as it did before this block existed. */
.briefing-item__digest {
  background: var(--ink-4);
  border: 1px solid var(--border-subtle);
  padding: var(--space-3);
  margin: 0 0 var(--space-3);
}
.briefing-item__digest-text { margin: 0 0 var(--space-2); }
/* Ink, not Muted, for the rationale line on this tint - the same choice
   .iso-chain-panel makes for its own connector text on the identical
   background, rather than the closer-to-the-4.5:1-floor Muted-on-tint
   pairing used elsewhere in this file. Distinguished from the digest text
   above by size and the bold label, not by a dimmer colour. */
.briefing-item__digest-rationale {
  margin: 0;
  font-size: var(--type-supporting);
  color: var(--text-primary);
}
.briefing-item__digest-rationale-label { font-weight: 600; }

.briefing-item__summary { margin: var(--space-2) 0; }

.briefing-item__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  margin: 0 0 var(--space-3);
  font-size: var(--type-supporting); /* matches .briefing-item__meta above - both substantive secondary prose */
}
.briefing-item__tags-label { font-weight: 600; }
.briefing-item__relevance { color: var(--muted); } /* 5.09:1 on Paper, verified */
.briefing-item__areas { color: var(--muted); } /* 5.09:1 on Paper, verified; same pairing as .briefing-item__relevance */

.proposed-links {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

/* Phase 3: each proposed link as a distinct block (was a left-border-only
   accent). Same near-paper inset-panel treatment as the register detail's
   ISO chain panel, now the same --ink-4 ramp token (UI vitality phase 1;
   11.60:1 Ink-on-tint; this block's own text - target, rationale - is all
   Ink or Fact-blue already, Fact-blue-on-this-tint measuring ~5.49:1, still
   clear of 4.5:1 - no Muted text lives inside it, so no override is needed
   here the way the ISO chain panel needed one). */
.proposed-link {
  background: var(--ink-4);
  border: 1px solid var(--border-subtle);
  padding: var(--space-3);
}

.proposed-link__target { font-weight: 600; margin: 0 0 var(--space-1); }
.proposed-link__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-2);
}
.proposed-link__chips:empty { display: none; margin: 0; }
.proposed-link__rationale { margin: 0 0 var(--space-2); }

/* Returned-link history (docs/ui-vitality-plan.md workstream F, phase 5;
   _briefing_item.html carries the "why this may not render yet" note at its
   call site). Muted secondary prose, matching every other quiet meta line in
   this file - not a new colour pairing, the same --text-secondary-on-Paper
   5.09:1 already verified for .field-hint, applied here against this card's
   --ink-4 background where the ramp table (docs/ui.md section 2) already
   documents that pairing as safe. */
.proposed-link__returned-history {
  margin: 0 0 var(--space-2);
  color: var(--text-secondary);
  font-size: var(--type-supporting);
}

.verdict-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2) var(--space-3);
}
.verdict-form { margin: 0; }
.verdict-form .form-row { margin-bottom: var(--space-2); }
.verdict-form input[type="text"] { max-width: 22rem; }
.verdict-controls > form,
.verdict-controls > details { flex: 1 1 14rem; min-width: 14rem; }

/* Phase 3: Confirm is the primary action - filled Ink by default, inverting
   on hover. This reuses the Ink/Paper pairing button:hover and
   button[aria-pressed="true"] already use elsewhere in this file (12.49:1),
   just applied by default instead of on :hover, so there is no new ratio to
   record. Edit-rationale (the details/summary disclosure, styled as a text
   link above) and Dismiss (the plain outlined button, unchanged) read as
   secondary by contrast with this, with no extra styling needed on either. */
.verdict-form--confirm button {
  background: var(--text-primary);
  color: var(--surface-body);
  border-color: var(--text-primary);
  padding: var(--space-2) var(--space-4);
}
.verdict-form--confirm button:hover {
  background: var(--surface-body);
  color: var(--text-primary);
}

/* Comfortable tap targets throughout Briefing, and the same verdict controls
   reused on the register entry page's Awaiting triage section: both are read
   on a phone. 2.75rem is 44px at the 16px root font. */
.briefing-item button,
.awaiting-item button,
.disclosure--watch button { min-height: 2.75rem; }

/* details/summary: keyboard-operable and focusable natively; styled as a
   plain text toggle so it reads as an action, not a card. */
.disclosure summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--fact-blue); /* 5.90:1 on Paper, verified */
  padding: var(--space-1) 0;
}
.disclosure summary:hover { color: var(--ink); }
.disclosure[open] summary { margin-bottom: var(--space-2); }
.disclosure--watch {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}
.disclosure--watch summary { font-family: var(--font-display); font-size: var(--type-h4); } /* was a stray 1.05rem; the token that already matched that value exactly */
.disclosure--watch[open] form { margin-top: var(--space-2); }

.briefing-item--cleared {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--green);
  padding: var(--space-3) var(--space-4);
  color: var(--ink);
}
/* A 409 whose re-read finds the item already cleared (_briefing_item_response.html;
   red-team Finding 3, fix iteration 2): the accent alone flips to red - text
   stays Ink, matching .briefing-item--cleared's own choice not to colour the
   whole message - so the outcome is never shown in the success (green)
   colour. var(--red) is 7.87:1 on Paper, already verified elsewhere in this
   file, not a new pairing. */
.briefing-item--cleared-error { border-left-color: var(--red); }

/* Triage read-back (docs/ui-vitality-plan.md workstream F, phase 5;
   briefing.html, app/triage_readback.py). A second, separate .stat-row
   below the inbox - see the template comment at its call site for why it is
   not folded into #briefing-stat-row's OOB cycle. Section-level spacing
   matches .briefing-band's own margin so it reads as its own block, not a
   trailing appendage to the last band. */
.triage-readback { margin: var(--space-6) 0 0; }
.triage-readback .stat-row { margin-top: var(--space-3); }

/* The classifier-quality view, inside the disclosure above (.disclosure,
   already styled). A plain list, not a table: each entry is one sentence's
   worth of "what and why", not tabular data. Left rule matches
   .history-item's own quiet-metadata treatment rather than inventing a new
   one. */
.dismissal-reasons {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}
.dismissal-reasons__item {
  border-left: 2px solid var(--rule);
  padding-left: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
}
.dismissal-reasons__title { font-weight: 600; }
.dismissal-reasons__reason { color: var(--text-secondary); } /* 5.09:1 on Paper, same pairing as .field-hint */
.dismissal-reasons__time {
  color: var(--text-secondary); /* 5.09:1 on Paper, same pairing as .field-hint */
  font-size: var(--type-micro);
}

/* Step 1, dismissal analytics: the domain/lane breakdown tables inside the
   second disclosure (briefing.html). These reuse the plain `table`/
   `.table-scroll`/`caption`/`.value-mono` rules as-is - no new colour
   anywhere in either table, deliberately: this is a read-back of the user's
   own decisions, never a score, and giving a "noisy" row its own alarm
   colour would read as exactly the risk-style severity marker CLAUDE.md
   rules out here. `.breakdown-table` only narrows the generic `table` rule's
   52rem min-width (sized for the register/sources tables' many columns) down
   to something proportionate for these five plain columns, so the table does
   not stretch into empty space it has no content for. */
.breakdown-table { min-width: 30rem; }

/* ------------------------------------------------------------------ */
/* Extra chip variants (Horizon, Sources). Same verified pairings as the   */
/* chips above; only the class names are new, so no new ratio to record.  */
/* ------------------------------------------------------------------ */

.chip--tag { border-color: var(--ink); color: var(--ink); } /* 12.49:1, same pairing as chip--normal */
.chip--neutral { border-color: var(--rule); color: var(--muted); } /* 5.09:1, same pairing as chip--background */
.chip--overdue { border-color: var(--red); color: var(--red); } /* 7.87:1, same pairing as chip--close-watch / field-error, rechecked for this use */
.chip--review-due { border-color: var(--amber); color: var(--ink); } /* 12.49:1; Amber is the border cue only, same pairing as chip--health-degraded / badge--signal */

/* Source health. healthy/silent reuse the green/red text pairings verified
   above; degraded reuses the badge--signal pattern (Amber is a border cue
   only, the label itself is Ink on Paper); unknown reuses the muted pairing. */
.chip--health-healthy { border-color: var(--green); color: var(--green); } /* 5.17:1, same pairing as chip--confirmed */
.chip--health-degraded { border-color: var(--signal-border); color: var(--text-primary); } /* 12.49:1; Amber is the border cue only, same pattern as badge--signal */
.chip--health-silent { border-color: var(--red); color: var(--red); } /* 7.87:1, same pairing as chip--close-watch, rechecked for this use as instructed */
.chip--health-unknown { border-color: var(--rule); color: var(--muted); } /* 5.09:1, same pairing as chip--background */

/* ------------------------------------------------------------------ */
/* Horizon (watch items and entry proposals)                            */
/* ------------------------------------------------------------------ */

/* UI vitality phase 1: "Horizon's two sections" structural gap (plan
   workstream A). Targets the real landmark <section id> from
   _horizon_sections.html directly, so no template change is needed; the
   proposals section needs no matching rule since nothing follows it but the
   footer, which already gets its own --space-6 break above. */
#horizon-watch-section {
  margin-bottom: var(--space-6);
}

.horizon-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
}

.horizon-item {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink);
  padding: var(--space-3) var(--space-4);
}

/* Phase 2 review fix 3: the proposal row's restored item-level info_class
   badge. Same one-liner as .briefing-item__badge-row above; the badge inside
   carries its own verified colours. */
.horizon-item__badge-row { margin: 0 0 var(--space-1); }

/* UI vitality phase 4: this is an <h3> on both watch rows and proposal rows
   (it always was h3 on proposals; watch rows moved up from h4) - layout only
   here, the size/family/weight come from the shared h1-h4 rule and h3's own
   font-size above, so nothing else in this file needed to change. */
.horizon-item__title { margin: 0 0 var(--space-2); }

.horizon-item__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-2);
}

.horizon-item__due { margin: 0 0 var(--space-2); }
.horizon-item__due-none {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-style: italic;
}

.horizon-item__review { margin: 0 0 var(--space-2); }

/* Watch-item bands (overdue / next 30 days / this quarter / 3 to 12 months /
   beyond 12 months / needs a date): a grouping for the eye only, computed from
   due_date and expected_window server-side. Never a score, never salience. */
.horizon-band { margin: 0 0 var(--space-5); }
/* Typography (family, weight, small caps, size, colour) comes from the
   shared .band-row__label/.horizon-band__heading rule above, which fixes
   the group-heading inversion this phase targets; only layout lives here.
   UI vitality phase 2 adds the count ("Overdue - 3 items", "Build these"
   item 6): the heading becomes a flex row so the count can sit at the
   trailing edge, matching the register table's own band-row bar. */
.horizon-band__heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--rule);
  padding-bottom: var(--space-1);
  margin: 0 0 var(--space-3);
}
/* .horizon-band__label needs no rule of its own: it inherits the shared
   small-caps typography from its parent .horizon-band__heading above (all
   of font-family/weight/variant-caps/transform/letter-spacing/size/colour
   are inherited properties), the same way plain text directly in the
   heading did before this phase added the count. */
.horizon-band__count {
  flex: none;
  font-variant-caps: normal;
  text-transform: uppercase;
  font-weight: 600;
  font-size: var(--type-label);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
} /* colour inherited from the heading (--text-primary, 12.49:1/14.2:1, already verified) - not a new ratio */

.horizon-item__rationale { margin: 0 0 var(--space-2); }

.horizon-item__source { margin: 0 0 var(--space-2); }
.horizon-item__hand-created {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-style: italic;
}

/* P4 digest (docs/pipeline-p4.md; app/digests.py; _horizon_watch_row.html,
   _horizon_proposal_row.html): the same featured-content treatment as
   .briefing-item__digest below (read that rule's own comment for the full
   argument) - not a new visual language for the one new prose block Horizon
   rows gain, and not a new colour pairing: the identical --ink-4 tint +
   --border-subtle border .proposed-link and .iso-chain-panel already use for
   "this block is substantive, not peripheral meta". Ink text on --ink-4 is
   11.60:1 (verified once, above, for .iso-chain-panel). Absent entirely when
   a row has no digest, so a row with none renders exactly as it did before
   this block existed. */
.horizon-item__digest {
  background: var(--ink-4);
  border: 1px solid var(--border-subtle);
  padding: var(--space-3);
  margin: 0 0 var(--space-2);
}
.horizon-item__digest-text { margin: 0 0 var(--space-2); }
/* Ink, not Muted, for the rationale line - the same choice
   .briefing-item__digest-rationale and .iso-chain-panel's own connector text
   make on the identical background; distinguished from the digest text above
   by size and the bold label, not by a dimmer colour. */
.horizon-item__digest-rationale {
  margin: 0;
  font-size: var(--type-supporting);
  color: var(--text-primary);
}
.horizon-item__digest-rationale-label { font-weight: 600; }

.horizon-item__provenance {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-size: var(--type-micro); /* "drafted by model X, prompt Y" - attribution, not content */
  margin: 0 0 var(--space-2);
}

.horizon-item__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-2);
}
.horizon-item__tags-label { font-weight: 600; }

.horizon-item__matching-terms {
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-size: var(--type-supporting); /* substantive - explains why the item matched */
  margin: 0 0 var(--space-2);
}

.iso-chain--compact { gap: var(--space-2); margin-bottom: var(--space-3); }

.horizon-item__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}
.horizon-item__actions > form,
.horizon-item__actions > details,
.horizon-item__actions > p { margin: 0; flex: 1 1 12rem; min-width: 12rem; }
.horizon-item__promote { font-weight: 600; }

.horizon-item button { min-height: 2.75rem; } /* 44px at the 16px root */

.horizon-row--updated {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--green);
  padding: var(--space-2) var(--space-3);
  color: var(--green); /* 5.17:1 on Paper, verified above */
  font-weight: 600;
}
/* A 404/409 row action, retargeted off the row (app/horizon.py::_row_error;
   red-team Finding 3, fix iteration 2): same shape as the success ack above,
   red instead of green so a conflict is never shown in the success colour -
   var(--red) is 7.87:1 on Paper, the same pairing already verified for
   .field-error/.worded-alert elsewhere in this file, not a new ratio. */
.horizon-row--updated--error {
  border-left-color: var(--red);
  color: var(--red);
}

.horizon-source {
  border: 1px solid var(--rule);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
}
.horizon-source h2 { margin-top: 0; border-bottom: none; font-size: var(--type-h3); } /* was a stray 1.1rem; the token that already matched that value */

/* ------------------------------------------------------------------ */
/* Page heading with an action link (Register list "New entry").       */
/* Same Ink-on-Paper .button pairing already verified above; no new     */
/* colour ratio introduced.                                             */
/* ------------------------------------------------------------------ */

.page-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}
.page-heading .button-row { margin: 0; }

/* ------------------------------------------------------------------ */
/* Linked intelligence (register detail): confirmed evidence behind an  */
/* entry. Same list/border pattern as .history-list and .proposed-link; */
/* badge/chip colours reuse .badge--fact / .badge--signal / .chip,      */
/* already verified above, so no new ratios are introduced here.       */
/* ------------------------------------------------------------------ */

.linked-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.linked-item {
  border: 1px solid var(--rule);
}

/* Collapsed-by-default disclosure (register_detail.html): <details> is the
   card, so its border (above) hugs just the summary line when collapsed and
   grows to include the body once opened, rather than a fixed-height <li>
   wrapping empty space. list-style:none plus the -webkit- rule below hide
   the browser's own marker triangle in every engine (Firefox honours
   list-style on summary; older WebKit/Blink need the pseudo-element rule
   too); .linked-item__chevron replaces it with the SAME chevron glyph and
   rotation pattern already used for .register-row__chevron, so collapsed vs
   expanded is a shape change, not colour alone, and reads consistently with
   the register table's own row-expansion affordance elsewhere on the site. */
.linked-item__toggle {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
  cursor: pointer;
  list-style: none;
}
.linked-item__toggle::-webkit-details-marker { display: none; }
.linked-item__chevron { flex: none; }
.linked-item__details[open] > .linked-item__toggle .linked-item__chevron {
  transform: rotate(90deg);
}
/* Summary has no default underline/colour of its own to override; only the
   focus ring needs stating explicitly so it matches every other interactive
   element in this file (same --focus-ring/--focus-offset tokens as, for
   example, .row-detail__heading above), since a plain <summary> does not
   inherit the global a:focus-visible rule. */
.linked-item__toggle:focus-visible,
.linked-item__toggle:focus {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}
.linked-item__toggle-badge { flex: none; }
.linked-item__toggle-title { font-weight: 600; }
.linked-item__toggle-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-left: auto;
  color: var(--muted); /* 5.09:1 on Paper, verified */
  font-size: var(--type-micro); /* peripheral meta on the collapsed summary row, not the row's own content */
  font-variant-numeric: tabular-nums;
}

.linked-item__body {
  padding: 0 var(--space-3) var(--space-3);
}

.linked-item__title { font-weight: 600; margin: 0 0 var(--space-1); }
.linked-item__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-2);
}
.linked-item__summary { margin: 0 0 var(--space-2); }
.linked-item__rationale { margin: 0 0 var(--space-2); }
.linked-item__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-2);
}
.linked-item__confirmed {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--type-micro); /* an attribution line ("Confirmed: date"), not content */
  color: var(--muted); /* 5.09:1 on Paper, verified */
}

/* ------------------------------------------------------------------ */
/* Awaiting triage (register detail): the second door onto the          */
/* Briefing's own pending decisions, keyed by entry                     */
/* (_register_awaiting.html; docs/register-surface.md, "Triage from the */
/* entry page"). Deliberately its own namespace rather than reaching    */
/* into .briefing-item*, even where a rule below is identical to one    */
/* there - the same convention .chip--returning and                     */
/* .band-row__marker--healthy already use elsewhere in this file: a     */
/* shared visual treatment gets its OWN class per surface, so a future  */
/* change to one surface's card cannot silently reach into the other's. */
/* .proposed-link / .verdict-controls / .disclosure / .chip are reused  */
/* verbatim, unlike these - they were already generic, non-namespaced   */
/* classes before this section existed, not scoped under .briefing-item */
/* to begin with. No new colour pairing anywhere below: every rule      */
/* restates a ratio already verified elsewhere in this file.            */
/* ------------------------------------------------------------------ */

.awaiting-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

/* Same plain-border-plus-accent treatment as .briefing-item (12.49:1 Ink
   border, unchanged token), restated under this surface's own class per the
   note above. A draft's card is a plain border, never the tinted --ink-4
   fill .proposed-link uses below for the decision block nested inside it -
   that contrast is deliberate: the outer card is the item's own facts, the
   inner .proposed-link is the specific pending relationship to THIS entry. */
.awaiting-item {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink);
}
.awaiting-item__body { padding: var(--space-3) var(--space-4); }
.awaiting-item__title { font-weight: 600; margin: 0 0 var(--space-2); }

/* Same --ink-4 tint + --border-subtle border pairing .briefing-item__digest,
   .proposed-link and .iso-chain-panel already use for "this block is
   substantive content, not peripheral meta" (11.60:1 Ink-on-tint, verified
   once for .iso-chain-panel, not a new ratio). Ink, not Muted, for the
   rationale line for the same reason .briefing-item__digest-rationale
   chooses Ink on this tint. */
.awaiting-item__digest {
  background: var(--ink-4);
  border: 1px solid var(--border-subtle);
  padding: var(--space-3);
  margin: 0 0 var(--space-3);
}
.awaiting-item__digest-text { margin: 0 0 var(--space-2); }
.awaiting-item__digest-rationale {
  margin: 0;
  font-size: var(--type-supporting);
  color: var(--text-primary);
}
.awaiting-item__digest-rationale-label { font-weight: 600; }

.awaiting-item__summary { margin: 0 0 var(--space-3); }

.awaiting-item__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  margin: 0 0 var(--space-3);
  font-size: var(--type-supporting);
}
.awaiting-item__tags-label { font-weight: 600; }
.awaiting-item__relevance { color: var(--muted); } /* 5.09:1 on Paper, verified */

/* The aria-live announcement line: no role="status" in the template (see
   _register_awaiting.html's own comment - it already sits inside
   #register-awaiting-region's aria-live="polite"). Same green accent and
   5.17:1 pairing .briefing-item__announcement already uses for the same
   "successful verdict outcome" message, restated under this surface's own
   class. */
.awaiting-announcement {
  border-left: 3px solid var(--green);
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
  font-weight: 600;
  color: var(--green); /* 5.17:1 on Paper, verified above */
}

/* ------------------------------------------------------------------ */
/* Register table: Evidence column. UI vitality phase 2 replaces the    */
/* bare count with the evidence strip (.evidence-strip/.evidence-summary, */
/* defined with the other shared quantity/time components above); the    */
/* cell now holds a small table plus a worded summary line, so it can no */
/* longer force single-line nowrap the way the old bare count did.       */
/* ------------------------------------------------------------------ */

.col-evidence .evidence-summary { white-space: nowrap; }

/* Triage from the entry page (docs/register-surface.md): the pending count
   beside the evidence strip in the register list's Evidence column and in
   the row-detail fragment (_register_row_detail.html). Plain text and the
   page's already-verified link colour (5.90:1, the global `a` rule) - no new
   chip, no new colour: a proposal is machine output nobody has agreed with
   yet (invariant 9), not a status this surface classifies by colour. */
.awaiting-count-note {
  margin: var(--space-1) 0 0;
  font-size: var(--type-micro);
}

/* ------------------------------------------------------------------ */
/* Sources                                                               */
/* ------------------------------------------------------------------ */

.source-inactive-flag { display: inline-block; margin-left: var(--space-2); }

.source-secondary-row td {
  border-top: none;
  padding-top: 0;
  padding-bottom: var(--space-3);
}
.source-secondary-row .entry-meta { margin: var(--space-2) 0 0; }

/* UI vitality plan workstream H item 6: health-band marker squares, one
   variant per health_status, sitting inside tr.band-row th - the same
   --ink-8 cell background the register's own band markers sit on, so the
   ratios below are computed against --ink-8, not Paper, matching
   .band-row__marker--background's own convention above. Silent is the one
   filled square, the same "most urgent band is filled" rule
   .band-row__marker--close_watch already uses; degraded, unknown and
   healthy are border-only squares in their band's own chip colour (see
   .chip--health-* below), never a filled amber square (the amber guard:
   amber is a border cue only, never a fill anyone could read as solid
   colour meaning something on its own). */
.band-row__marker--silent { background: var(--red); border: 1px solid var(--red); } /* 6.79:1 on --ink-8, same colour pairing as .chip--health-silent (7.87:1 on Paper) */
.band-row__marker--degraded { background: transparent; border: 2px solid var(--signal-border); } /* 3.54:1 on --ink-8 - clears the 3:1 non-text minimum, the narrowest margin of the four; amber stays a border cue only, as .chip--health-degraded requires */
.band-row__marker--unknown { background: transparent; border: 2px solid var(--text-secondary); } /* 4.38:1 on --ink-8, the same pairing already verified at .band-row__marker--background above */
.band-row__marker--healthy { background: transparent; border: 2px solid var(--green); } /* 4.46:1 on --ink-8, same colour pairing as .chip--health-healthy (5.17:1 on Paper) */

/* UI vitality plan workstream H item 8: throughput column ("Items (30
   days)", "which feeds are earning their place"). Right-aligned like every
   other numeric column; the font is Courier Prime via the existing
   .value-mono class already on this cell, so no font-family rule is needed
   here, only the alignment. */
.col-throughput { text-align: right; }
.source-throughput-zero { color: var(--text-secondary); } /* 5.09:1 on Paper, same pairing as .field-hint - a zero is the honest count, not an exceptional state, so it de-emphasises rather than alarms */

/* UI vitality plan workstream H item 9: the relative age beside Last
   success's absolute timestamp, reusing the .horizon-item__magnitude idiom
   (secondary text trailing a fact) rather than inventing a new one. For a
   silent source the duration since last success is the diagnostic fact of
   that row - the whole reason it sorts into the Silent band - so it takes
   the same weight as the row's primary content instead of staying
   secondary furniture. */
.source-relative-age { color: var(--text-secondary); } /* 5.09:1 on Paper, same pairing as .field-hint/.horizon-item__magnitude */
.source-relative-age--silent { color: var(--text-primary); font-weight: 600; } /* 12.49:1 on Paper, same pairing as .watch-flag-text - not a new ratio */

/* Step 1, dismissal analytics: the Triage quality column
   (app/triage_readback.py::decisions_by_source, wired in app/sources.py).
   Deliberately plain text, no chip and no colour: this is a count of the
   user's own decisions, not a health state or a score, so it takes none of
   the bordered/coloured treatment Health or Failures use elsewhere on this
   row - a "noisy source" chip in Red would read as exactly the risk-style
   severity marker CLAUDE.md rules out for this data. The two spans reuse the
   Ink-on-Paper body-text default (12.49:1, unchanged) and the Muted-on-Paper
   pairing already verified above (5.09:1) - no new ratio. */
.triage-quality { font-weight: 600; }
.triage-quality__count {
  font-weight: 400;
  color: var(--text-secondary); /* 5.09:1 on Paper, same pairing as .field-hint */
}
.triage-quality-empty {
  color: var(--muted); /* 5.09:1 on Paper, verified - same pairing as .source-throughput-zero */
  font-style: italic;
}

/* ------------------------------------------------------------------ */
/* Reduced motion                                                       */
/* ------------------------------------------------------------------ */

@media (prefers-reduced-motion: no-preference) {
  [aria-live] { transition: background-color 0.2s ease; }
  .register-row__chevron,
  .linked-item__chevron,
  .briefing-item__chevron { transition: transform 0.15s ease; }
}
@media (prefers-reduced-motion: reduce) {
  * { scroll-behavior: auto !important; }
}

/* ------------------------------------------------------------------ */
/* Forced colors (Windows High Contrast). Overrides both themes - a     */
/* forced-colors palette wins over whichever of light/dark is active.  */
/* Most of this file is already safe by construction: every focus state */
/* uses `outline` (never box-shadow, which forced-colors strips), and   */
/* every chip/badge/band marker sets a real `border` (never a colour-   */
/* only background or box-shadow), so their shape survives the colour   */
/* remap; state is always paired with a visible text label too ("Close  */
/* watch", "FACT", aria-current plus the nav link's own text), so       */
/* colour is never the sole carrier here either. Icons are stroke=      */
/* currentColor throughout (_app_nav.html, including the theme toggle),  */
/* so they inherit the remapped text colour automatically. The one gap: */
/* the entry-heading "filled tab" carries its boundary by background    */
/* fill alone (the Confirm button uses the same fill but already has a  */
/* real 2px border from the shared `button` rule, so it needs nothing   */
/* extra) - forced-colors flattens background-color to Canvas, so       */
/* without a border entry-heading would lose its edge entirely and      */
/* leave only the heading text, which is why it gets one explicitly.    */
/* ------------------------------------------------------------------ */
@media (forced-colors: active) {
  a:focus-visible,
  button:focus-visible,
  input:focus-visible,
  select:focus-visible,
  textarea:focus-visible,
  summary:focus-visible,
  a:focus,
  button:focus,
  input:focus,
  select:focus,
  textarea:focus {
    outline: 2px solid;
    outline-offset: var(--focus-offset);
  }

  .entry-heading {
    border: 1px solid;
  }

  /* Same gap as .entry-heading above: an Ink-fill-only state loses its
     edge when forced-colors flattens background-color to Canvas. The
     rotated chevron and the bordered detail panel beneath already show the
     row is open even here, but this keeps the open row's own boundary
     consistent with every other fill-based state in this file. */
  .register-row--open > th,
  .register-row--open > td {
    border-top: 1px solid;
    border-bottom: 1px solid;
  }
}

/* ------------------------------------------------------------------ */
/* Small screens                                                        */
/* ------------------------------------------------------------------ */

@media (max-width: 40rem) {
  dl.entry-meta { grid-template-columns: 1fr; }

  /* Briefing is read on a phone: comfortable 44px-minimum tap targets, full
     width single-column forms, no side-by-side controls to mis-tap. Extended
     to .awaiting-item (register entry page, Awaiting triage) since it reuses
     the same .verdict-controls block verbatim; Register's OWN pre-existing
     button layout (the edit form, watch-flag control) is deliberately left
     alone here, unchanged. */
  .briefing-item button,
  .awaiting-item button,
  .disclosure--watch button {
    min-height: 2.75rem; /* 44px at the 16px root */
    width: 100%;
  }
  .verdict-controls { flex-direction: column; }
  .verdict-controls > form,
  .verdict-controls > details { width: 100%; min-width: 0; }
  .briefing-item input[type="text"],
  .briefing-item input[type="date"],
  .briefing-item select,
  .briefing-item textarea,
  .awaiting-item input[type="text"],
  .awaiting-item textarea { max-width: none; }

  /* Horizon: same comfortable-tap-target treatment as Briefing. */
  .horizon-item__actions { flex-direction: column; }
  .horizon-item__actions > form,
  .horizon-item__actions > details,
  .horizon-item__actions > p { width: 100%; min-width: 0; }
  .horizon-item button { width: 100%; }
  .horizon-item input[type="text"],
  .horizon-item select { max-width: none; }
}

/* ------------------------------------------------------------------ */
/* Sign in / sign out (session 8 auth hardening)                        */
/* ------------------------------------------------------------------ */

/* Email and password inputs: the login form is the first surface to need
   them, so the text/date/select/textarea rule above did not cover type.
   Same declarations as that rule (Ink on Paper, no new colour pairing);
   kept as its own additive block rather than editing that shared selector
   list while this file is under concurrent edit elsewhere. */
input[type="email"],
input[type="password"] {
  font-family: var(--font-body);
  font-size: var(--type-body);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ink);
  padding: var(--space-2);
  width: 100%;
  max-width: 40rem;
}

.login-panel { max-width: 26rem; margin: 0 auto; }
.login-panel__note { margin-top: var(--space-3); }

/* Mobile-first 44px tap target on the sign-in button, matching the
   Briefing/Horizon precedent above; full width up to a small tablet, then a
   normal inline button once there is room either side. */
.login-form button { min-height: 2.75rem; width: 100%; }
@media (min-width: 30rem) {
  .login-form button { width: auto; }
}
