/* ==========================================================================
   The Morning Journal - one stylesheet, one design.
   ==========================================================================

   This file replaces three appended layers that were fighting each other (two
   :root blocks in light, two in dark, twelve tokens defined twice with the
   later one silently winning). There is now exactly ONE light token block, one
   dark block under prefers-color-scheme, and one mirror under
   :root[data-theme="dark"] for a future manual toggle. No colour has its only
   definition in a dark block.

   The design it implements:

   THREE VOICES. Serif is what the journal reports (headlines, deks, summaries,
   key facts, quotes, the sentence on a failure). Uppercase sans at 11px is the
   journal grading itself (badges, state words, datelines, labels, counts, the
   folio, table heads). Mono is a machine fact, verbatim (ids, timestamps in
   tables, domains, gate tiers, logs, tracebacks). Uppercase belongs to the sans
   voice alone: no serif is ever uppercased and no sans furniture is ever set in
   sentence case. That one rule is what makes the voices readable AS voices, so
   a caveat can never again be mistaken for body copy.

   THE STANDING RULE. Every unit of content carries a left stroke whose PATTERN
   states how well it stands up - solid, dashed, dotted, double, hatched.
   Pattern is primary, colour only reinforces, and the text label is never
   removed. It survives greyscale, forced-colors and a printout, and it costs no
   query, no field and no script.

   COLOUR MEANS STATE. The accent is cancellation-ink violet (#2E2A86), used
   sparingly. Red is reserved for failures. Section colour is a leading tick and
   chip text only - never a filled pastel pill, never a card or row background.

   NO SHADOW ANYWHERE, in either theme. Content is bounded by rules. Radius is
   2px on inputs and buttons and 0 on every card, table, slab and panel.

   Every class name that existed before this rewrite still exists here: 28
   templates and 782 black-box assertions depend on them. This is a re-skin of
   the same markup contract, not a rename.
   ========================================================================== */

/* --------------------------------------------------------------------------
   00 - The editorial serif, self-hosted
   --------------------------------------------------------------------------
   The CSP is default-src 'self' with no CDN, so these woff2 files are fetched
   at build time and committed under static/fonts/. They are Newsreader (OFL,
   see fonts/OFL.txt), subset to latin and instanced down from the full variable
   font so both files together are 110,240 bytes:

     roman   72,264 B   axes wght 400-600, opsz 16-36  (the optical-size axis is
                                                        why this face was chosen)
     italic  37,976 B   axis  wght 400-600, opsz pinned at 18

   font-display: swap plus a real fallback stack, so a font that fails to load
   degrades to the previous appearance rather than to Times. The serif subset
   carries latin only: it has no check mark, cross or arrow glyphs, so those
   characters must be set in the sans or mono voice (which is where the design
   puts them anyway). -------------------------------------------------------- */

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/newsreader-roman-v26.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/newsreader-italic-v26.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   01 - Tokens: the single definition of every value
   -------------------------------------------------------------------------- */

:root {
  /* --- surfaces: cold press ---------------------------------------------- */
  --bg:          #E9EBEE;   /* the ground behind the paper */
  --paper:       #FFFFFF;   /* what a story sits on */
  --paper-2:     #F4F6F8;   /* inset panels, table heads, pre, standing notes */

  /* --- ink --------------------------------------------------------------- */
  --ink:         #0F1216;   /* 18.78:1 on paper */
  --ink-2:       #33393F;   /* 11.68:1 */
  --muted:       #5C646C;   /*  6.01:1 - datelines, metadata */

  /* --- rules: hairlines, deliberately quiet ------------------------------ */
  --rule:        #D6DAE0;   /* 1.40:1 vs paper */
  --rule-strong: #A8AEB6;   /* 2.24:1 - masthead and section rules */

  /* --- the one accent ---------------------------------------------------- */
  --accent:      #2E2A86;   /* cancellation-ink violet, 11.72:1 on paper */
  --link:        #2E2A86;
  --focus:       #1668E3;   /* deliberately NOT the accent: a focus ring that
                               matched the accent would stop reading as focus */

  /* --- state: the only colour allowed to signal state -------------------- */
  --ok-fg:   #0E5132; --ok-bg:   #DDEEE4;   /* 7.77:1 */
  --warn-fg: #6B4A05; --warn-bg: #F6E8CC;   /* 6.65:1 */
  --err-fg:  #8A1C1C; --err-bg:  #F6DEDE;   /* 7.25:1 */
  --info-fg: #1D3F7A; --info-bg: #DEE7F5;   /* 8.24:1 */
  /* There is no --accent-soft and no --muted-bg. Both were declared by the
     layers this file replaced and neither was ever painted: this design fills
     no surface with the accent (section identity is a 2px tick and chip text, a
     badge is never filled) and the only washes are the four state washes above.
     A token nothing reads is the previous design still sitting on the page. */

  /* --- section identity: foreground only, chips are never filled ---------- */
  /* Nine colours, and nothing else. The eighteen --cN-bg / --cN-fg aliases the
     replaced layers carried are gone: every one of them was transparent or an
     alias of the colour beside it, and nothing in this file or in the 28
     templates ever read one. A token nothing reads is not part of the design -
     it is the previous design still on the page. */
  --c0: #7A2D16; --c1: #23456D; --c2: #33571F; --c3: #552C68;
  --c4: #654809; --c5: #1B5751; --c6: #702D49; --c7: #464239;
  --cx: #55514A;

  /* --- the Standing Rule: stroke pattern first, colour second ------------- */
  --stand-w:  3px;   /* a story, a resource, a row */
  --stand-w4: 4px;   /* a lead, a failure, a slab, a standing note */
  --stand-confirmed: var(--ink);
  --stand-lead:      var(--accent);
  --stand-news:      var(--ink-2);
  --stand-headline:  var(--muted);
  --stand-archive:   var(--muted);   /* the spec said --rule-strong; measured at
                                        2.24:1 (light) and 2.14:1 (dark) that is
                                        below the 3:1 a non-text state carrier
                                        needs, so the archive stroke takes
                                        --muted (6.01:1 / 5.96:1) and stays
                                        distinct from headline-only by PATTERN
                                        (double vs dotted), which is the rule
                                        this design is built on. */
  --stand-fail:      var(--err-fg);
  --stand-warn:      var(--warn-fg);
  --stand-hidden:    var(--muted);
  --stand-none:      var(--rule-strong);   /* no claim made either way */

  /* --- families ---------------------------------------------------------- */
  --serif: "Newsreader", "Iowan Old Style", Charter, "Bitstream Charter",
           "Palatino Linotype", Palatino, Georgia, serif;
  --sans:  system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
           Arial, sans-serif;
  --mono:  ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
           monospace;

  /* --- type scale: base 16, ratio ~1.2, harder jumps at display sizes ----- */
  --t-xs:   0.6875rem;   /* 11px - the sans furniture voice, always uppercase */
  --t-sm:   0.8125rem;   /* 13px - datelines, help text, mono meta */
  --t-base: 1rem;        /* 16px - UI text, table cells, inputs (never smaller) */
  --t-md:   1.125rem;    /* 18px - serif body: summary, dek, lede, brief, quote */
  --t-lg:   1.375rem;    /* 22px - section titles, card headlines, failure titles */
  --t-xl:   1.75rem;     /* 28px - page h1, empty-day leader */
  --t-2xl:  clamp(1.75rem, 4.4vw, 2.5rem);  /* lead headline, story h1 */
  --t-3xl:  clamp(2rem, 7vw, 3.5rem);       /* the nameplate */

  /* --- tracking: the larger the type, the tighter it wants to be ---------- */
  --track-furniture: 0.08em;
  --track-lg:  -0.005em;
  --track-xl:  -0.012em;
  --track-2xl: -0.02em;
  --track-3xl: -0.022em;

  /* --- geometry: rules and hairlines, not boxes and shadows --------------- */
  --radius:    2px;   /* inputs and buttons ONLY */
  --radius-0:  0;     /* cards, tables, slabs, panels */
  --shadow: none;     /* declared once, never overridden, in either theme */

  --s1: 0.25rem; --s2: 0.5rem;  --s3: 0.75rem;
  --s4: 1.25rem; --s5: 2rem;    --s6: 3.25rem;

  --topbar-h: 3rem;    /* the sticky condensed bar; everything that sticks
                          BELOW it offsets by this, so it is stated once */
  --gutter:  1.5rem;   /* 1rem below 40rem - never less than a 16px side gutter */
  --measure: 66ch;     /* running serif text */
  --maxw:    1180px;
  --rail:    20rem;

  --hatch: repeating-linear-gradient(135deg,
             var(--stand-hidden) 0 1px, transparent 1px 6px);
}

/* --- dark: night desk ----------------------------------------------------
   Values only. Every token above still has its definition in :root, so nothing
   is defined for the first time here. The :not([data-theme="light"]) guard
   leaves room for a future manual light toggle. --------------------------- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:          #0A0C0F;
    --paper:       #121519;
    --paper-2:     #181C21;
    --ink:         #E8EBEE;   /* 15.30:1 on paper */
    --ink-2:       #C2C8CE;   /* 10.85:1 */
    --muted:       #8C949C;   /*  5.96:1 */
    --rule:        #2C323A;
    --rule-strong: #454D57;
    --accent:      #A9A4F5;   /*  8.12:1 */
    --link:        #B3AEF7;   /*  8.99:1 */
    --focus:       #7FB4FF;
    --ok-fg:   #8FD5A9; --ok-bg:   #12301F;
    --warn-fg: #E8C67C; --warn-bg: #332708;
    --err-fg:  #F0A8A3; --err-bg:  #3A1414;
    --info-fg: #9FBDE8; --info-bg: #131F33;
    --c0: #F0BBA9; --c1: #ABC6E8; --c2: #B5D8A2; --c3: #D6B6E4;
    --c4: #E7CC8C; --c5: #9FD8D1; --c6: #EAB4CA; --c7: #CFC9BD;
    --cx: #C2BCB0;
  }
}

/* The same values again for an explicit toggle. Kept as a literal copy rather
   than a shared block because CSS cannot alias a group of declarations, and a
   wrong dark value must be findable in exactly two places. */
:root[data-theme="dark"] {
  --bg:          #0A0C0F;
  --paper:       #121519;
  --paper-2:     #181C21;
  --ink:         #E8EBEE;
  --ink-2:       #C2C8CE;
  --muted:       #8C949C;
  --rule:        #2C323A;
  --rule-strong: #454D57;
  --accent:      #A9A4F5;
  --link:        #B3AEF7;
  --focus:       #7FB4FF;
  --ok-fg:   #8FD5A9; --ok-bg:   #12301F;
  --warn-fg: #E8C67C; --warn-bg: #332708;
  --err-fg:  #F0A8A3; --err-bg:  #3A1414;
  --info-fg: #9FBDE8; --info-bg: #131F33;
  --c0: #F0BBA9; --c1: #ABC6E8; --c2: #B5D8A2; --c3: #D6B6E4;
  --c4: #E7CC8C; --c5: #9FD8D1; --c6: #EAB4CA; --c7: #CFC9BD;
  --cx: #C2BCB0;
}

/* --------------------------------------------------------------------------
   02 - Reset and base elements
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: 4rem;   /* an anchored heading is never under the top bar */
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--t-base)/1.5 var(--sans);
  font-synthesis-weight: none;
  /* NOT overflow-x: hidden. Nothing here is allowed to exceed 100vw in the
     first place; hiding the overflow would hide the bug instead. */
}

a {
  color: var(--link);
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius);
}
/* A container that clips - a scroll port, or a wrapped row whose neighbours sit
   8px away - cuts a ring drawn 5px OUTSIDE the focused element's border box. In
   these three the link fills its box exactly, so the ring had nowhere to be
   drawn and was cut off - MEASURED, before this rule: 0px of room on three
   sides of every .nav-read link, on the top and bottom of every .nav-ops link,
   and on both sides of every .settings-nav link, for a ring needing 5px. The
   two nav rows have since stopped being scroll ports and now wrap, which does
   not give the ring its 5px back: the row gap is var(--s3), and .settings-nav
   is still a real scroll port on a tall desktop.

   The ring is drawn INSIDE instead: same 3px, same colour, fully visible, and
   it costs no layout at all, where padding plus a negative margin would have
   pulled two flex rows into each other. outline-offset does nothing unless an
   outline is being drawn, so it is set unconditionally rather than on a
   pseudo-class. */
.nav-read a, .nav-ops a, .settings-nav a { outline-offset: -3px; }

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 600;
  line-height: 1.2;
  margin: 0 0 var(--s2);
  text-wrap: balance;
  hyphens: none;
}
h1 { font-size: var(--t-xl); letter-spacing: var(--track-xl); }
h2 { font-size: var(--t-lg); letter-spacing: var(--track-lg); }
h3 { font-size: var(--t-lg); letter-spacing: var(--track-lg); }
h4 { font-size: var(--t-base); }

p { margin: 0 0 var(--s3); text-wrap: pretty; }
ul, ol { margin: 0 0 var(--s3); }
hr { border: 0; border-top: 1px solid var(--rule); margin: var(--s4) 0; }
img, svg { max-width: 100%; }

/* --------------------------------------------------------------------------
   03 - The three voices
   --------------------------------------------------------------------------
   Everything below assigns one of these three. A selector that is in none of
   these lists inherits the sans UI voice, which is the right default for
   furniture. -------------------------------------------------------------- */

/* Voice 1 - SERIF: what the journal is reporting. */
.masthead-title, .story-headline, .lead-headline, .card-headline, .front-headline,
.rail-headline, .section-title, .failure-title, .subhead, .brief p, .lede,
.story-summary, .card-dek, .lead-dek, .front-dek, .facts li, .evidence p,
.resource-title, .edition-link, .revision-headline, .revision-summary,
.case-title, .empty h2, .empty p, .failure-what, .page-head h1, .side-subtitle,
.slab-text, .cat-new-title, .brief-cleared p {
  font-family: var(--serif);
  font-variant-numeric: proportional-nums;
}

/* Voice 2 - SANS FURNITURE: the journal grading itself. Always uppercase,
   always tracked, never sentence case. If one of these ever reads as a
   sentence, the voice rule has been broken. */
.kicker, .brief-title, .rail-title, .by-section-title, .side-title,
.settings-nav-title, .catnav-label, .badge, .chip, .ai-label, .revision-label,
.section-count, .folio, .showing, .note-state, .slab-state, .ledger-label,
.section-off-label, .ops-eyebrow, .pill, .pill-label, .topbar-page,
.table caption, .table thead th, .filters label:not(.check) {
  font-family: var(--sans);
  font-size: var(--t-xs);
  line-height: 1.4;
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

/* Voice 3 - MONO: a machine fact, verbatim. If it came out of the database it
   is mono; if it came out of the model it is serif; if the app is explaining
   itself it is sans. */
.mono, .log, .log-traceback, .resource-url, .ledger-value, .failure-when,
.timeline-date, .gate-tier, code, pre, samp, kbd {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.mono { font-size: 0.9em; }

/* Figures that sit in a column or are compared against another are tabular
   everywhere. Dates in running prose stay proportional. */
.table, .kv, .pills, .pill, .failure-count, .held-count, .ledger, .showing,
.folio, .timeline-date, .section-count {
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   04 - The Standing Rule
   --------------------------------------------------------------------------
   PATTERN is the signal; colour reinforces; the text label is never removed.
   Implemented with border-left-style plus, for `hidden`, one repeating gradient
   painted into a transparent border - no inline style attribute anywhere (six
   routes assert that ' style=' does not appear in the body).

   Two ways in:

   (a) EXPLICIT - a template adds .stand-confirmed / .stand-news / ... These win.
   (b) AUTOMATIC - for the frozen macros in _macros.html, which no builder owns
       this round, the pattern is derived from the badge the macro already
       emits, using :has(). Precedence, most severe first:
         hidden > headline-only > archive > official source > news only.
       A browser without :has() keeps the neutral default stroke; the badge and
       the note still say everything, so no disclosure is lost.
   -------------------------------------------------------------------------- */

.card, .lead-story, .front-card, .resource, .failure, .revision,
.standing-note, .slab, .action-card, .side-box, .rail-item,
.settings-form, .empty {
  border-left: var(--stand-w) solid var(--stand-none);
}
/* .timeline-item is deliberately NOT in that list. A timeline node states its
   standing with its DOT, not with a left stroke - the rail itself is the line.
   It used to be listed here and then zeroed again by `border-left: 0` a
   thousand lines later: two rules fighting to produce nothing. */

/* (b) automatic, derived from the frozen macro markup. */
.card:has(.badge-muted), .lead-story:has(.badge-muted),
.front-card:has(.badge-muted), .resource:has(.badge-muted) {
  border-left-style: dashed;
  border-left-color: var(--stand-news);
}
.card:has(.badge-ok), .lead-story:has(.badge-ok),
.front-card:has(.badge-ok), .resource:has(.badge-ok) {
  border-left-style: solid;
  border-left-color: var(--stand-confirmed);
}
.card:has(.badge-archive), .lead-story:has(.badge-archive),
.front-card:has(.badge-archive), .rail-item:has(.badge-archive) {
  border-left-style: double;
  border-left-color: var(--stand-archive);
}
.card.is-lead, .lead-story.is-lead, .front-card.is-lead, .rail-item.is-lead {
  border-left-style: dotted;
  border-left-color: var(--stand-headline);
}
.failure.failure-err  { border-left-color: var(--stand-fail); border-left-width: var(--stand-w4); border-left-style: solid; }
.failure.failure-warn { border-left-color: var(--stand-warn); border-left-width: var(--stand-w4); border-left-style: dashed; }

/* The held-back and given-up rows on /runs. They CANNOT state their standing in
   the markup: tools/vv/test_web.py parses them with <li class="resource"[^>]*>,
   so a second class name in that attribute makes every held-back assertion miss
   (it did, and cost eight assertions). The stroke is derived from the badge the
   row already carries. Ordered by severity - held by the injection guard, or
   given up after N attempts (.health-bad), is worse than unverified
   (.badge-warn) - and at equal specificity the later rule wins. Only these rows
   carry those two badges: a story-page resource is Primary (.badge-ok) or News
   (.badge-muted), which the two rules above already cover. */
.resource:has(.badge-warn) {
  border-left-style: dashed;
  border-left-color: var(--stand-warn);
  border-left-width: var(--stand-w4);
}
.resource:has(.health-bad) {
  border-left-style: solid;
  border-left-color: var(--stand-fail);
  border-left-width: var(--stand-w4);
}

/* A quote the pipeline could not tie to a source link. The story page states it
   in words beside the quote ("Recorded before quotes were tied to sources");
   this is the same fact as a pattern, so it survives greyscale. It cannot be a
   class on the blockquote either - the suite matches <blockquote class="evidence">
   literally. */
.evidence:has(.note-state) {
  border-left-style: dotted;
  border-left-color: var(--stand-none);
}

/* (a) explicit, and the hidden treatment, both live at the END of this file
   (section 19b) - not here. They have to beat every component rule that sets a
   `border` or `background` shorthand later in the cascade, and at equal
   specificity that is decided by source order, not by intent. The measured
   symptom of getting this wrong was a hidden story losing its hatch because
   `.card { background: var(--paper) }` cleared the background-image. */


/* The stroke scales to the edition itself: the folio band takes the edition's
   pattern on its own two rules. */
.folio-ok   { border-block-style: solid; }
.folio-warn { border-block-style: dashed; }
.folio-err  { border-block-style: dotted; }

/* --------------------------------------------------------------------------
   05 - Page frame
   -------------------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

main.wrap { padding-bottom: var(--s6); }

.muted { color: var(--muted); }

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

.skip { position: absolute; left: -9999px; top: 0; z-index: 40; }
.skip:focus {
  left: var(--gutter); top: var(--s2);
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--ink);
  padding: var(--s2) var(--s3);
}

/* SHOWING N OF M - the standing truncation disclosure. It is printed ABOVE the
   list it bounds, never at the foot, because a cap read after the list has
   already read as completeness. */
.showing {
  color: var(--muted);
  margin: 0 0 var(--s2);
  display: block;
}

/* --------------------------------------------------------------------------
   06 - Header, navigation, status chip
   --------------------------------------------------------------------------
   Two groups because they answer two different questions: what is there to
   read, and is the journal actually working. The status chip carries the second
   answer on EVERY page - the owner chose in-app visibility over e-mail, so a
   broken source must never wait for someone to open a status page.

   The header is NOT sticky. Five strips plus a sticky bar was more header than
   a front page can carry, and a sticky header covers the element the keyboard
   just focused. .topbar below is the one sticky element, for the condensed bar.
   -------------------------------------------------------------------------- */

.site-header {
  background: var(--paper);
  border-bottom: 1px solid var(--rule-strong);
}

.header-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
  padding-block: var(--s4) var(--s3);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  color: var(--ink);
  text-decoration: none;
  line-height: 1.1;
  margin-right: auto;
}
.brand-mark { flex: none; color: var(--accent); width: 36px; height: 36px; }
.brand-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.brand-name {
  font-family: var(--serif);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.brand:hover .brand-name { text-decoration: underline; text-underline-offset: 0.18em; }
.brand-tagline {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
}

/* The search field is a real form and works with JS off.

   `min-width: 0` on the FORM is load-bearing, not tidying. A flex item's
   automatic minimum size is its min-content size, and this form's min-content
   is the input's own intrinsic width (the `size` attribute, ~226px) plus the
   gap plus the Search button - MEASURED at 310px. At a 320px viewport the row
   has 288px, so the form refused to shrink and pushed the document 6px wide on
   EVERY page (WCAG 1.4.10 reflow). The input already carried `min-width: 0`;
   that lets the input shrink INSIDE the form and does nothing for the form's
   own minimum. */
.search { display: flex; gap: var(--s2); flex: 1 1 18rem; max-width: 26rem; min-width: 0; }
.search input[type="search"] { flex: 1; min-width: 0; }
.search-big { max-width: none; flex-wrap: wrap; align-items: center; }
.search-big input[type="search"] { font-size: var(--t-md); padding-block: var(--s2); }

/* The status chip. Its leading glyph differs in SHAPE as well as colour, so it
   still distinguishes its states in greyscale and under forced-colors:
   filled circle = ok, notched ring = warn, square = err. It may shorten to a
   dot plus a count when green; it may NEVER shorten when amber or red. */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  min-height: 2.75rem;            /* a 44px tap target on a phone */
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  text-decoration: none;
  /* `flex: none` pinned the chip at its max-content width - MEASURED at 242px
     for "15 things need attention". On /story at 320px the condensed bar then
     read 16 + 88 (running head) + 12 + 242 = 358px against a 320px viewport,
     which is where the widest reflow failure on the site came from. It may now
     SHRINK and let its label wrap onto a second line; it may never truncate,
     abbreviate or drop the label, because a shortened amber chip is the state
     the reader must not miss. Its min-content is 121px, so the bar fits at
     320px with the running head beside it. */
  flex: 0 1 auto;
  min-width: 0;
}
.status-chip:hover { border-color: currentColor; text-decoration: underline; }
.status-dot {
  width: 0.6rem; height: 0.6rem;
  background: currentColor;
  border: 2px solid currentColor;
  border-radius: 50%;
  flex: none;
}
.status-ok      { color: var(--ok-fg);   border-color: var(--ok-fg); }
.status-warn    { color: var(--warn-fg); border-color: var(--warn-fg); }
.status-err     { color: var(--err-fg);  border-color: var(--err-fg); }
.status-unknown { color: var(--muted);   border-color: var(--rule-strong); }
/* warn: a notched ring, not a filled dot. err: a square. */
.status-warn .status-dot { background: transparent; border-style: double; border-width: 3px; }
.status-err  .status-dot { border-radius: 0; }
.status-unknown .status-dot { background: transparent; }

/* Nav: Reading first and larger, Operations quieter and to the right. Both are
   in the DOM at every width; nothing is behind a hamburger, a dropdown or a
   script. The literal string "What failed" is in the served HTML of every page
   at every width. */
.mainnav {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--s2) var(--s4);
  border-top: 1px solid var(--rule);
}
/* Both groups WRAP; neither is a horizontal scroller. They were scrollers, and
   at 320px the Operations group needed 392px in a 288px row, so - MEASURED on
   the rendered page - Sources and Runs were fully visible, "What failed" was
   59% visible and "Settings" was 0% visible: two of the four destinations were
   off the end of a strip with no visible affordance that it scrolled. A
   navigation item the reader cannot see is a navigation item they do not have,
   and the failure surface must never be the clipped one. Wrapping costs one
   extra row at 320px and keeps every destination on the page at every width. */
.nav-read, .nav-ops {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3) var(--s4);
  min-width: 0;
}
.nav-read { font-size: 0.9375rem; }
.nav-ops  { font-size: var(--t-sm); }
.mainnav a {
  color: var(--ink-2);
  text-decoration: none;
  padding-block: var(--s3);
  border-bottom: 2px solid transparent;
  flex: none;
}
.nav-ops a { color: var(--muted); }
.mainnav a:hover { color: var(--ink); text-decoration: underline; }
/* Both values are marked the same way on purpose. A link whose href IS this page
   carries aria-current="page"; a link that names the SECTION this page sits in
   (Settings while /settings/tools is open, the story's section in the Sections
   row) carries aria-current="true", which is the ARIA value for "the current item
   in a set" and does not claim to be the page. The reader sees one treatment
   either way - you are here - while a screen reader is told exactly one page. */
.mainnav a[aria-current] {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--accent);
}
/* The OPERATIONS eyebrow: form, not a second accent colour, is what separates
   the back of the book from the front. It is a REAL ELEMENT that base.html
   writes - <span class="ops-eyebrow">Operations</span>, the same class the
   operations page heads use - not CSS ::before content. It was ::before, which
   made it the only group label on the page the server did not write (the
   SECTIONS label beside it always has been a real element), invisible to the
   browser's own find-in-page and gone from a printout. One word, one class, one
   mechanism. */
.nav-ops .ops-eyebrow {
  display: inline-flex;
  align-items: center;
  margin: 0;
  padding-right: var(--s3);
  border-right: 1px solid var(--rule);
  flex: none;
  /* The eyebrow used to carry `scroll-snap-align: start`, because the row was a
     scroller and without a snap position at scrollLeft 0 Chrome snapped to the
     first link and the row opened reading "RATIONS | Sources ...". The row
     wraps now, so there is no scroll port and no snapping: the rule went with
     the mechanism it belonged to rather than being left behind to be found. */
}

/* Sections: a real horizontal scroller with a sticky label, never a dropdown
   that hides where the reader is. A 2px coloured leading tick plus coloured
   chip text - never a filled pastel pill.

   :focus-within is the keyboard's escape hatch, and it is here because of a
   measured browser behaviour, not a preference. At 1280px the row is 1248px
   wide in a 1180px port, and Chrome DOES NOT scroll this container when a chip
   inside it takes focus: measured scrollLeft 0 of the 68px needed, with the
   last chip 70% visible, both with a real Tab walk and with element.focus(),
   and unchanged with scroll-snap off, with the label removed from the snap set,
   with scroll-padding-inline-end and with scroll-margin (six variants, all
   measured). scrollIntoView() reaches 68px, so the port can scroll - focus just
   does not move it. Rather than leave a focused link half off the end, the row
   stops being a scroller for as long as a chip inside it has focus: measured
   100% visible, at every width. A mouse reader keeps the compact one-line row;
   a keyboard reader sees every section. */
.catnav {
  display: flex;
  align-items: stretch;
  gap: var(--s3);
  border-top: 1px solid var(--rule);
  padding-block: var(--s2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  /* A chip scrolled to by keyboard or by snap parks clear of the sticky label. */
  scroll-padding-inline-start: 5.5rem;
  scroll-padding-inline-end: 1rem;
  scrollbar-width: thin;
  white-space: nowrap;
}
.catnav:focus-within {
  flex-wrap: wrap;
  overflow-x: visible;
  white-space: normal;
}
.catnav-label {
  position: sticky;
  left: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  flex: none;
  color: var(--muted);
  background: var(--paper);
  padding-right: var(--s3);
  /* Same reason as the OPERATIONS eyebrow: the sticky SECTIONS label must be a
     snap position, or the row opens snapped to the first chip with its first
     word hidden under the label ("s & Charges" instead of "Arrests & Charges"). */
  scroll-snap-align: start;
}
.cat-link {
  scroll-snap-align: start;
  flex: none;
  display: inline-flex;
  align-items: center;
  font-size: var(--t-sm);
  font-weight: 600;
  text-decoration: none;
  padding: var(--s2) 0 var(--s2) var(--s2);
  border-left: 2px solid currentColor;   /* the coloured leading tick */
  border-bottom: 2px solid transparent;
}
.cat-link:hover { text-decoration: underline; }
.cat-link[aria-current] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* The condensed bar: the ONE sticky element on the page. The bar is full-bleed
   and the row inside it is the .wrap, so THE FLEX LIVES ON THE ROW - the bar
   itself only has to be sticky and paint its ground. (The bar used to be the
   flex container as well, which left `.topbar .status-chip { margin-left: auto }`
   with nothing to push against, because the chip's flex parent is the row.)

   --topbar-h states the bar's height ONCE. Everything that sticks below it
   offsets by that token - .settings-nav and .month-head both do - so the bar
   can never get taller without the things beneath it following. It is declared
   on :root, not here, because a sticky element elsewhere in the page cannot
   inherit a custom property from a sibling. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--paper);
  border-bottom: 1px solid var(--rule-strong);
}
.topbar-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--topbar-h);
}
.topbar-name { font-family: var(--serif); font-size: 1.125rem; font-weight: 600; text-decoration: none; color: var(--ink); }
.topbar-page { color: var(--muted); }
.topbar .status-chip { margin-left: auto; }

/* --------------------------------------------------------------------------
   07 - Section identity colour
   --------------------------------------------------------------------------
   Colour still means state on every state surface, so a section may not use a
   filled chip. It changes SHAPE instead: a 2px coloured leading tick plus
   coloured chip text. No card, table row or section background ever carries a
   section colour. -------------------------------------------------------- */

.cat-c0, .cat-c0 .chip { color: var(--c0); }
.cat-c1, .cat-c1 .chip { color: var(--c1); }
.cat-c2, .cat-c2 .chip { color: var(--c2); }
.cat-c3, .cat-c3 .chip { color: var(--c3); }
.cat-c4, .cat-c4 .chip { color: var(--c4); }
.cat-c5, .cat-c5 .chip { color: var(--c5); }
.cat-c6, .cat-c6 .chip { color: var(--c6); }
.cat-c7, .cat-c7 .chip { color: var(--c7); }
.cat-cx, .cat-cx .chip { color: var(--cx); }

.chip {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  background: transparent;
  padding: 1px 0 1px var(--s2);
  border-left: 2px solid currentColor;
}
.chip:hover { text-decoration: underline; }

/* There was a block of eight rules here setting --tick-inset: 0px..14px, under
   a comment claiming that adjacent sections therefore "differ typographically as
   well as by hue". Nothing in this file ever read --tick-inset, so the claim was
   false and the eight rules did nothing. Both are gone. Section identity is a
   2px coloured leading tick plus coloured chip text, and that is all it is. */

/* --------------------------------------------------------------------------
   08 - Badges: text chips, never filled pills
   --------------------------------------------------------------------------
   Transparent fill, 1px border, 2px radius, 11px uppercase. Each badge carries
   a SHAPE as well as a colour, so the state survives greyscale. They wrap
   freely and are never collapsed behind a "+2 more".
   -------------------------------------------------------------------------- */

.badges {
  list-style: none;
  margin: var(--s2) 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
}
.badge {
  display: inline-block;
  padding: 2px var(--s2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-2);
  vertical-align: middle;
  /* NOT `white-space: nowrap`. A two-word badge has room and does not wrap
     either way; a badge that quotes a machine's own words does not. The held-back
     row on /runs carries "re-queued after worker restart (attempt 2 of 3)" -
     MEASURED at 369px, which pushed /runs to 385px wide at both a 320px and a
     360px viewport. The words are the disclosure, so the badge wraps and the
     page does not. */
  white-space: normal;
}
.badge-ok,   .health-ok,   .status-ok    { color: var(--ok-fg);   border-color: var(--ok-fg); }
.badge-info, .badge-update, .status-running, .status-partial { color: var(--info-fg); border-color: var(--info-fg); }
.badge-warn, .health-warn, .status-empty, .status-skipped { color: var(--warn-fg); border-color: var(--warn-fg); }
.health-bad, .status-failed, .status-error { color: var(--err-fg); border-color: var(--err-fg); }
.status-partial { border-style: dashed; }          /* shape, not only colour */
.status-empty, .status-skipped { border-style: dotted; }
.status-failed, .status-error { border-width: 2px; }
.badge-archive { border-style: dashed; color: var(--muted); }
.badge-muted, .health-off, .health-unknown, .status-unknown {
  color: var(--muted);
  border-color: var(--rule-strong);
  border-style: dotted;
}
/* The hidden badge carries the hatch too, so the mark and the word agree. */
.badge-warn.section-off-label { border-style: dashed; }

/* --------------------------------------------------------------------------
   09 - Standing notes and severity slabs
   --------------------------------------------------------------------------
   A STANDING NOTE is the journal talking ABOUT a story: a 4px left rule in the
   state stroke, an 11px uppercase state word, then the exact existing sentence
   in SANS, then a real underlined link. Deliberately not serif - that is the
   whole point of the two voices, and it is what stops a caveat reading as body
   copy.

   A SEVERITY SLAB is the paper speaking about itself: no radius, no icon, a 4px
   left rule, a state wash, an 11px uppercase state word, then the sentence in
   SERIF at 18px, then any named list, then the action link as a bordered
   button. The state WORD, not an icon, is the greyscale carrier.
   -------------------------------------------------------------------------- */

.standing-note, .lead-label, .claims-note, .corroboration-label, .revised-note,
.archived-note, .section-note, .worker-state, .failure-diag-none, .held-count,
.brief-cleared-note, .subhead-note, .resurfaced-in {
  font-family: var(--sans);
}

/* The three frozen notes from _macros.html get the standing-note treatment
   directly, because no builder owns _macros.html this round. */
.lead-label, .claims-note, .corroboration-label, .standing-note {
  display: block;
  background: var(--paper-2);
  /* Longhands on three sides, never the `border` shorthand: a shorthand here
     sits after section 04 and silently resets the standing stroke. Only the
     left edge's WIDTH and COLOUR are set; its PATTERN comes from section 04 or
     from a .stand-* class the template states, which is the whole point of
     having one place where patterns are decided. */
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w4);
  border-left-color: var(--stand-headline);
  border-radius: var(--radius-0);
  padding: var(--s2) var(--s3);
  margin: var(--s2) 0;
  font-size: var(--t-base);
  line-height: 1.5;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.lead-label { border-left-color: var(--stand-headline); }
.corroboration-label { border-left-color: var(--stand-news); border-left-style: dashed; }
.claims-note { border-left-color: var(--stand-warn); }
.lead-label a, .claims-note a, .standing-note a { color: var(--link); font-weight: 600; }

/* The state word above a standing note. Every state word in this design is
   SERVER-WORDED - a <span class="note-state"> or <span class="slab-state"> the
   template writes - because a word invented by the stylesheet is a claim the
   journal never made, and it vanishes if the stylesheet does.

   The three frozen notes from _macros.html carried CSS ::before words here
   ("Headline only", "News report", "Wording removed"). They are gone, and
   nothing is lost, because each of those sentences ALREADY opens by naming its
   own state:
     "Headline only: details not confirmed yet. Open the original source."
     "News report: no official record found yet ..."
     "Some wording was removed: the AI's summary stated things its source ..."
   The ::before printed the state a second time, and the second copy was the one
   the server had not written. The rule this leaves holds everywhere: a state
   word is ADDED where the sentence does not already name the state (a notice, a
   banner, a slab, a source's error row), and is not where it does. */
.note-state {
  display: block;
  color: var(--muted);
  margin-bottom: var(--s1);
}

/* Severity slabs: .notice and .banner are the frozen class names; .slab is the
   name a template may use directly. */
.notice, .slab {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  margin: var(--s4) 0;
  padding: var(--s3) var(--s4);
  /* Same reason as the standing note above: three longhand sides, and the left
     edge keeps the pattern section 04 gave it. */
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w4);
  border-left-color: var(--rule-strong);
  border-radius: var(--radius-0);
  background: var(--paper-2);
}
.notice p, .slab p { margin: 0; flex: 1 1 16rem; }
.notice ul, .slab ul { flex-basis: 100%; margin: var(--s2) 0 0; padding-left: var(--s4); }
.notice p strong, .slab p strong { font-weight: 600; }
.slab-text { font-size: var(--t-md); line-height: 1.5; }
.slab-state { display: block; flex-basis: 100%; color: inherit; margin-bottom: var(--s1); }

.notice-ok   { background: var(--ok-bg);   color: var(--ok-fg);   border-left-color: var(--ok-fg); }
.notice-info { background: var(--info-bg); color: var(--info-fg); border-left-color: var(--info-fg); }
.notice-warn { background: var(--warn-bg); color: var(--warn-fg); border-left-color: var(--warn-fg); border-left-style: dashed; }
.notice-err  { background: var(--err-bg);  color: var(--err-fg);  border-left-color: var(--err-fg); }
.notice a, .slab a { color: inherit; font-weight: 600; }
.banner { border-left-width: var(--stand-w4); }
.banner p, .slab p.slab-text {
  flex-basis: 100%;
  font-family: var(--serif);
  font-size: var(--t-md);
  line-height: 1.55;
}
/* A plain .notice (an action was requested, a setting was saved) stays in the
   sans UI voice: that is the app explaining itself, not the paper reporting. */
.notice .run-reasons, .notice .run-notes { flex-basis: 100%; }

/* --------------------------------------------------------------------------
   10 - Masthead, folio, morning brief
   -------------------------------------------------------------------------- */

.masthead {
  text-align: center;
  padding: var(--s5) 0 var(--s4);
  border-bottom: 3px double var(--rule-strong);
  margin-bottom: var(--s4);
}
.masthead .brand-mark {
  display: block;
  margin: 0 auto var(--s2);
  width: 28px; height: 28px;
  color: var(--accent);
}
.masthead-mark {
  display: block;
  margin: 0 auto var(--s2);
  width: 28px; height: 28px;
  color: var(--accent);
}
.kicker { color: var(--muted); margin-bottom: var(--s1); }
.kicker a { color: inherit; }
.masthead-title {
  font-size: var(--t-3xl);
  line-height: 1.02;
  letter-spacing: var(--track-3xl);
  font-weight: 600;
  margin: 0 0 var(--s1);
}
.masthead-date {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--muted);
  margin: 0;
}

/* THE FOLIO - the signature. A full-width band between two hairlines, always
   rendered, stating where this edition came from. Every segment is a link. When
   something is wrong the words change AND the band's own rules take the
   edition's standing pattern, because a green chip is easy to stop seeing but a
   line read every morning registers on the morning it says something else. */
.folio {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s3);
  margin: 0 0 var(--s4);
  padding: var(--s2) 0;
  border-block: 1px solid var(--rule-strong);
  color: var(--muted);
}
.folio a { color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; }
.folio a:hover { color: var(--ink); }
.folio-seg { display: inline-flex; align-items: center; gap: var(--s2); }
/* The only generated content in this file, and it is PUNCTUATION, not a word: a
   middot between folio segments, the same separator the count-bar pills carry in
   their server-written text. It states nothing, so a browser that drops it loses
   nothing. Every WORD on every page is written by the server. */
.folio-seg + .folio-seg::before { content: "\00B7"; color: var(--rule-strong); }
.folio.folio-warn { color: var(--warn-fg); background: var(--warn-bg); border-color: var(--warn-fg); padding-inline: var(--s3); }
.folio.folio-err  { color: var(--err-fg);  background: var(--err-bg);  border-color: var(--err-fg);  padding-inline: var(--s3); }

.brief {
  margin: 0 0 var(--s4);
  padding: var(--s3) 0;
  border-block: 1px solid var(--rule);
  background: transparent;
  max-width: var(--measure);
}
.brief-title { color: var(--muted); margin: 0 0 var(--s2); }
.brief p {
  font-size: var(--t-md);
  line-height: 1.62;
  margin: 0;
  max-width: var(--measure);
}
.brief-cleared p { color: var(--ink-2); }
.brief-cleared-note { color: var(--muted); font-style: italic; }

/* --------------------------------------------------------------------------
   11 - The count bar
   --------------------------------------------------------------------------
   A frozen contract (ul.pills / li.pill, and the exact string shape
   "N/M sources healthy - X degraded - Y failing"), restyled: one
   hairline-bounded row, figures tabular, every segment a link. pill-warn and
   pill-bad tint the figure AND add a state stroke beneath it, so the state is
   not carried by colour alone. -------------------------------------------- */

.pills {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 0 0 var(--s4);
  padding: 0;
  border-block: 1px solid var(--rule);
}
/* ONE label voice for both count bars. The edition bar cannot wrap its labels
   (the suite reads "<strong>N</strong> new stor..." straight out of the raw HTML
   and a wrapper span breaks the match) and the run bar does wrap them in
   .pill-label; styling the pill ITSELF is what makes the two read the same.
   The size, weight, tracking and uppercase all come from the sans furniture
   voice in section 03 - this rule only does layout and colour. */
.pill {
  flex: 1 1 auto;
  min-width: 9rem;
  padding: var(--s3) var(--s4) var(--s3) 0;
  border-bottom: 3px solid transparent;
  color: var(--muted);
  background: transparent;
  border-radius: var(--radius-0);
}
.pill + .pill { padding-left: var(--s4); border-left: 1px solid var(--rule); }
.pill a { color: inherit; text-decoration: none; display: block; }
.pill a:hover { color: var(--ink); text-decoration: underline; }
.pill strong {
  display: block;
  font-family: var(--sans);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: 1.1;
  color: var(--ink);
  letter-spacing: var(--track-lg);
  /* The figure is a figure, not furniture: 22px, sentence case. A pill whose
     figure is a word ("Yes", "No topic keyword") reads as itself. */
  text-transform: none;
}
.pill-warn { color: var(--warn-fg); border-bottom-color: var(--warn-fg); border-bottom-style: dashed; }
.pill-warn strong { color: var(--warn-fg); }
.pill-bad  { color: var(--err-fg);  border-bottom-color: var(--err-fg); }
.pill-bad strong { color: var(--err-fg); }
.pill-lead { border-bottom-color: var(--stand-headline); border-bottom-style: dotted; }
.pill-back { border-bottom-color: var(--info-fg); }
.pill-run-sources a { color: inherit; }
.pill-label { display: block; color: inherit; }

/* The view toggle is a visible bordered link, never a switch inside a menu. */
.view-toggle { margin: 0 0 var(--s4); font-size: var(--t-sm); }
.view-toggle a {
  display: inline-block;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  color: var(--ink-2);
  text-decoration: none;
}
.view-toggle a:hover { border-color: var(--ink-2); text-decoration: underline; }

/* --------------------------------------------------------------------------
   12 - The front well
   -------------------------------------------------------------------------- */

.front {
  display: grid;
  gap: var(--s5);
  margin: 0 0 var(--s5);
  padding-bottom: var(--s5);
  border-bottom: 3px double var(--rule-strong);
}
.front-main { min-width: 0; }

.lead-story {
  padding: 0 0 var(--s4) var(--s4);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w4);
}
.lead-headline {
  font-size: var(--t-2xl);
  line-height: 1.08;
  letter-spacing: var(--track-2xl);
  font-weight: 600;
  margin: var(--s1) 0 var(--s2);
  overflow-wrap: anywhere;
}
.lead-headline a { color: var(--ink); text-decoration: none; }
.lead-headline a:hover { text-decoration: underline; }
.lead-dek {
  font-size: var(--t-md);
  line-height: 1.62;
  color: var(--ink-2);
  max-width: var(--measure);
  margin: 0 0 var(--s3);
  overflow-wrap: anywhere;
}

/* Two up, and never one card alone in the last row. `repeat(auto-fit,
   minmax(14rem, 1fr))` gave three 250px columns in the 800px main column at
   1280px, so a four-card front well printed 3 + 1 and the fourth story sat
   alone in a row of its own - MEASURED rowCounts [3, 1] at 1280. A fixed pair
   of columns is deterministic at every width instead of depending on how wide
   the rail happens to leave the main column, and the odd card out spans the
   row rather than being orphaned in it. It keeps the front-card type scale, so
   a wider box is not a promotion: rank is stated by position and by the lead's
   own type, never by a box's width. */
.front-grid {
  display: grid;
  gap: var(--s4);
  margin-top: var(--s4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.front-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }
.front-card {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  min-width: 0;
  padding: var(--s3) 0 var(--s3) var(--s3);
  border-top: 1px solid var(--rule);
}
.front-headline {
  font-size: var(--t-lg);
  line-height: 1.22;
  letter-spacing: var(--track-lg);
  margin: 0;
  overflow-wrap: anywhere;
}
.front-headline a { color: var(--ink); text-decoration: none; }
.front-headline a:hover { text-decoration: underline; }
.front-dek {
  font-size: var(--t-base);
  line-height: 1.5;
  color: var(--ink-2);
  margin: 0;
  overflow-wrap: anywhere;
}

/* The rail states its own cap in its head, so "the latest 8" can never read as
   "all of them". */
.rail { border-top: 3px double var(--rule-strong); padding-top: var(--s3); min-width: 0; }
.rail-title { color: var(--muted); margin: 0 0 var(--s3); }
.rail-list { list-style: none; margin: 0; padding: 0; }
.rail-item {
  padding: var(--s3) 0 var(--s3) var(--s3);
  border-top: 1px solid var(--rule);
}
.rail-item:first-child { border-top: 0; }
.rail-headline {
  display: block;
  font-family: var(--serif);
  font-size: var(--t-base);
  line-height: 1.3;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.rail-headline:hover { text-decoration: underline; }
.rail-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s2);
  margin: var(--s1) 0 0;
  font-size: var(--t-sm);
  color: var(--muted);
}
.rail-more { margin: var(--s3) 0 0; font-size: var(--t-sm); }

.by-section-title { color: var(--muted); margin: var(--s5) 0 var(--s2); }

/* --------------------------------------------------------------------------
   13 - Sections and cards
   --------------------------------------------------------------------------
   A card is a piece of paper bounded by rules, not a rounded box with a shadow.
   The rounded-box-with-shadow is the strongest generic tell there is, and it
   also fights the standing stroke for the left edge. -------------------- */

.section { margin: var(--s5) 0; }
.section-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s3);
  font-size: var(--t-lg);
  letter-spacing: var(--track-lg);
  border-bottom: 2px solid var(--rule-strong);
  padding-bottom: var(--s2);
  margin-bottom: var(--s4);
}
.section-title a { color: var(--ink); text-decoration: none; }

/* The month head of the grouped editions list stays on screen while its month
   scrolls past. It parks UNDER the condensed bar, not behind it - --topbar-h is
   the one place that height is written down. The ground is --bg, because a
   .section sits on the page ground, not on paper; anything else leaves a pale
   slab dragging down the list. */
.month-head {
  position: sticky;
  top: var(--topbar-h);
  z-index: 2;
  background: var(--bg);
  padding-top: var(--s2);
}
.section-title a:hover { text-decoration: underline; }
.section-count {
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1px var(--s2);
  background: transparent;
}
.section-off .section-title a { color: var(--muted); }
.section-note { margin: calc(var(--s4) * -1 + var(--s2)) 0 var(--s4); font-size: var(--t-sm); }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr));
  gap: var(--s4);
}
.cards-single { grid-template-columns: 1fr; }
.card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--paper);
  /* Three longhand sides. The `border` shorthand would reset the left edge that
     section 04 just set - it happens to be harmless here, because every rule
     that states a card's standing is a two-class selector and outranks this one,
     but the same shape with `background` is what silently cleared a hidden
     story's hatch, and the cure is not to write the shorthand on these elements
     at all. */
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
}
.card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s3);
  margin-bottom: var(--s1);
}
.dateline {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--muted);
}
.card-headline {
  font-size: var(--t-lg);
  line-height: 1.22;
  letter-spacing: var(--track-lg);
  margin: var(--s1) 0 var(--s2);
  overflow-wrap: anywhere;
}
.card-headline a { color: var(--ink); text-decoration: none; }
.card-headline a:hover { text-decoration: underline; }
.card-dek {
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--ink-2);
  margin: 0 0 var(--s2);
  max-width: var(--measure);
  overflow-wrap: anywhere;
}
.card-sources {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--muted);
  margin: auto 0 0;
  padding-top: var(--s2);
  overflow-wrap: anywhere;
}

/* The empty day is typeset at lead weight, not as a grey placeholder: a broken
   pipeline gets the same visual rank as a lead story, because that is what it
   is. The quiet variant sits in the identical slot with different rules and
   different words. */
.empty {
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w4);
  border-left-color: var(--stand-confirmed);
  border-radius: var(--radius-0);
  padding: var(--s4);
  margin: var(--s4) 0;
  text-align: left;
}
.empty h2 { font-size: var(--t-xl); letter-spacing: var(--track-xl); margin-bottom: var(--s2); }
.empty p { font-size: var(--t-md); line-height: 1.55; max-width: var(--measure); margin: 0; }
.actions { margin-top: var(--s4); display: flex; gap: var(--s2); flex-wrap: wrap; }
.archived-note { font-size: var(--t-sm); margin: calc(var(--s4) * -1) 0 var(--s4); }

/* --------------------------------------------------------------------------
   14 - Page heads, lists, pagers
   -------------------------------------------------------------------------- */

.page-head {
  margin: var(--s5) 0 var(--s4);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--rule-strong);
}
.page-head h1 { font-size: var(--t-xl); letter-spacing: var(--track-xl); margin: 0 0 var(--s2); }
.ops-eyebrow { display: block; color: var(--muted); margin-bottom: var(--s1); }
.lede {
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--ink-2);
  max-width: var(--measure);
  margin: 0;
}

.edition-list, .plain-list, .archive-months, .related, .resources, .rail-list,
.failure-list, .revisions, .timeline, .badges, .pills {
  list-style: none;
}
.edition-list, .plain-list, .archive-months, .related { margin: 0; padding: 0; }
.edition-list li {
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s1) var(--s3);
}
.edition-link { font-size: var(--t-md); }
.edition-intro {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--ink-2);
  margin: var(--s1) 0 0;
  flex-basis: 100%;
}
.archive-months { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); }
.plain-list li { padding: var(--s2) 0; border-bottom: 1px solid var(--rule); }
.related li { padding: var(--s2) 0; border-bottom: 1px solid var(--rule); font-size: var(--t-sm); }

.edition-nav, .pager {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: var(--s5) 0 var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: var(--t-sm);
}

/* --------------------------------------------------------------------------
   15 - The story page
   -------------------------------------------------------------------------- */

.story { margin-top: var(--s4); }
.story-headline {
  font-size: var(--t-2xl);
  line-height: 1.08;
  letter-spacing: var(--track-2xl);
  margin: var(--s2) 0 var(--s3);
  overflow-wrap: anywhere;
  text-wrap: balance;
}
.story-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--rail);
  gap: var(--s5);
  margin-top: var(--s4);
}
.story-main { min-width: 0; }
.story-side { min-width: 0; }
@media (min-width: 68.75rem) {
  .story-layout { column-gap: 2.5rem; }
  .story-side { border-left: 1px solid var(--rule); padding-left: 2.5rem; }
}

.story-dates {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--ink-2);
  margin: var(--s2) 0;
}
.event-date, .effective-date {
  display: inline-block;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: 1px var(--s2);
  font-variant-numeric: tabular-nums;
}
.effective-date { border-color: var(--ink-2); }

/* THE PROVENANCE LEDGER. One mono strip, fixed field order, every field always
   present: an unknown value renders an em dash, never disappears. To hide a
   caveat you would have to delete a column that is visibly present on every
   other story. */
.ledger {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  margin: var(--s3) 0 var(--s4);
  padding: var(--s2) var(--s3);
  background: var(--paper-2);
  border-block: 1px solid var(--rule-strong);
  font-size: var(--t-sm);
}
.ledger-field { display: inline-flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.ledger-label { color: var(--muted); }
.ledger-value { color: var(--ink); font-weight: 600; }
.ledger a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--rule-strong); }
.ledger a:hover { border-bottom-color: currentColor; }
.ledger-yes { color: var(--ok-fg); }
.ledger-no  { color: var(--muted); }

/* The AI label is attached to the top edge of the summary block so it cannot be
   styled away independently of the thing it labels. */
.ai-label {
  display: block;
  color: var(--muted);
  margin: var(--s4) 0 0;
  padding-bottom: var(--s1);
  border-bottom: 1px solid var(--rule);
}
.story-summary {
  font-size: var(--t-md);
  line-height: 1.62;
  max-width: var(--measure);
  margin: var(--s3) 0 var(--s4);
  overflow-wrap: anywhere;
}

.subhead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s3);
  font-size: var(--t-lg);
  letter-spacing: var(--track-lg);
  margin: var(--s5) 0 var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--rule);
}
.subhead-note {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
}

/* Key facts: a hanging hairline dash, not a bullet. */
.facts { list-style: none; margin: 0 0 var(--s4); padding: 0; max-width: var(--measure); }
.facts li {
  position: relative;
  padding-left: var(--s4);
  margin-bottom: var(--s3);
  font-size: var(--t-md);
  line-height: 1.55;
  overflow-wrap: anywhere;
}
.facts li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.75em;
  width: var(--s3);
  border-top: 1px solid var(--rule-strong);
}

/* A quote is verbatim and checked against the source it links to, so it takes
   the SOLID ink stroke. It is set roman, not italic: a 300 word italic quote is
   unreadable, and the rule and the indent already do the work. */
.evidence {
  margin: var(--s3) 0 var(--s4);
  padding: var(--s2) 0 var(--s2) var(--s4);
  border-left: var(--stand-w) solid var(--stand-confirmed);
  color: var(--ink-2);
  max-width: var(--measure);
  overflow-wrap: anywhere;
}
.evidence p { font-size: var(--t-md); line-height: 1.62; margin: 0; }
.evidence footer {
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-style: italic;
  margin-top: var(--s2);
  color: var(--muted);
}
.evidence .evidence-claim {
  font-family: var(--sans);
  font-style: normal;
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 var(--s1);
}
.evidence-help { font-family: var(--sans); font-size: var(--t-sm); margin: 0 0 var(--s3); color: var(--muted); }

/* Resources: outlet in the sans voice, title in serif, domain and gate note in
   mono, because the domain and the tier are machine facts. */
.resources { margin: 0; padding: 0; }
.resource {
  padding: var(--s3) 0 var(--s3) var(--s3);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w);
  min-width: 0;
}
.resource-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s2);
  font-size: var(--t-sm);
}
.outlet {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--ink-2);
}
.resource-title { font-size: var(--t-md); margin: var(--s1) 0; overflow-wrap: anywhere; }
.resource-snippet {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--ink-2);
  margin: var(--s1) 0;
  max-width: var(--measure);
  overflow-wrap: anywhere;
}
.resource-url { font-size: var(--t-sm); color: var(--muted); margin: 0; overflow-wrap: anywhere; }
.nolink { color: var(--ink-2); border-bottom: 1px dotted var(--muted); }
.article-fetch { color: var(--muted); }

.gate-note {
  font-family: var(--mono);
  font-size: var(--t-sm);
  margin: var(--s1) 0 0;
  overflow-wrap: anywhere;
}
.gate-pass { color: var(--ok-fg); }
.gate-drop { color: var(--muted); }
/* The gate tier is a machine fact and takes the mono voice above and nothing
   else: it used to be listed in the sans furniture voice as well, and then spent
   two declarations here turning that voice's tracking and uppercase back off. */
.gate-tier {
  display: inline-block;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: 0 var(--s1);
  font-size: var(--t-xs);
  color: var(--muted);
}
.gate-reason { color: var(--muted); }
.dropped-note { color: var(--warn-fg); }

/* The aside: three hairline-ruled boxes, no radius, no fill. */
.side-box {
  background: transparent;
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
}
.side-title { color: var(--muted); margin: 0 0 var(--s3); }
.side-subtitle { font-size: var(--t-base); margin: var(--s4) 0 var(--s2); }
.case-title { font-size: var(--t-md); }
.side-box form + form { margin-top: var(--s4); }
.side-box input[type="text"], .side-box select { width: 100%; margin-bottom: var(--s2); }
/* A settings form nested in a side box is not a second panel: the box is the
   panel. It drops every border, the standing stroke included. */
.side-box .settings-form { border: 0; padding: 0; background: transparent; }
.mute-form { margin-top: var(--s4); }

.kv {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--s1) var(--s3);
  margin: 0 0 var(--s3);
  font-size: var(--t-base);
}
.kv dt {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
  padding-top: 0.2em;
}
.kv dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.kv-inline { max-width: 45rem; }
.alias { color: var(--muted); }
.resurfaced-in { font-size: var(--t-sm); color: var(--muted); }

.revised-note { font-family: var(--sans); font-size: var(--t-sm); margin: var(--s2) 0; }
.revised-link { font-weight: 600; }

.revisions { margin: 0; padding: 0; display: grid; gap: var(--s4); }
.revision {
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
}
.revision-head { font-family: var(--sans); font-size: var(--t-sm); margin: 0 0 var(--s2); color: var(--muted); }
.revision-label { color: var(--muted); margin: var(--s3) 0 var(--s1); display: block; }
/* The superseded wording is RECESSED - set back onto --paper-2 inside the
   panel's paper - so a previous headline can never be read as the current one.
   The label above it already says "Before the change of <date>"; this is the
   same statement as a surface. */
.revision-headline, .revision-summary {
  background: var(--paper-2);
  padding: var(--s2) var(--s3);
  margin: 0;
  overflow-wrap: anywhere;
}
.revision-headline { font-size: var(--t-md); }
.revision-summary { max-width: var(--measure); }

/* --------------------------------------------------------------------------
   16 - Cases and the timeline
   --------------------------------------------------------------------------
   Dot grammar: filled = a grounded event date, hollow = the date is only
   reported (the macro already writes the word "(reported)"; this makes it
   scannable), struck = the event's story is hidden. Drawn in currentColor so it
   is right in both themes and in greyscale.
   -------------------------------------------------------------------------- */

.timeline {
  margin: var(--s3) 0 var(--s4);
  padding: 0 0 0 var(--s4);
  border-left: 1px solid var(--rule-strong);
}
.timeline-item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 0 var(--s4) var(--s3);
}
.timeline-item::before {
  content: "";
  position: absolute;
  left: calc(var(--s4) * -1 - 4px);
  top: 0.45em;
  width: 9px; height: 9px;
  background: currentColor;
  border: 1px solid currentColor;
  box-sizing: border-box;
}
/* hollow: the date is only reported. */
.timeline-item:has(.timeline-date .muted)::before { background: var(--paper); }
/* struck: the event's story is hidden. */
.timeline-item:has(.badge-warn)::before {
  background: var(--paper);
  border-style: dashed;
}
.timeline-item.is-current::before { outline: 2px solid currentColor; outline-offset: 2px; }
/* Timeline nodes past the 12-node cap. A native <details> - never a script -
   so the collapsed half is in the DOM, is findable with the browser's own
   in-page search, and prints. The summary is furniture: it is the journal
   saying how much it is holding back, not part of the record. */
.timeline-more { margin: 0 0 var(--s4); }
.timeline-more summary {
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  padding: var(--s2) 0;
}
.timeline-more summary:hover { text-decoration: underline; }
.timeline-more .timeline { margin-top: var(--s2); }

.timeline-date { font-size: var(--t-sm); color: var(--muted); }
.timeline-type { font-family: var(--sans); font-weight: 600; font-size: var(--t-base); }
.timeline-desc { color: var(--ink-2); font-size: var(--t-base); }
.timeline-link { font-size: var(--t-base); }

/* --------------------------------------------------------------------------
   17 - Operations: tables, runs, logs, sources, failures
   --------------------------------------------------------------------------
   The back of the book, not a different product: same tokens, same standing
   rule, same components. What changes is the MIX - sans-dominant, tabular
   figures, mono for machine output. No second accent colour.
   -------------------------------------------------------------------------- */

.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-0);
}
.table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
/* The caption's voice comes from the sans furniture list in section 03, which
   names it; only its layout and colour belong here. Restating the voice is how
   a second type scale gets in - change it in one place and the other copy
   silently keeps the old value. */
.table caption {
  caption-side: top;
  text-align: left;
  color: var(--muted);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
}
.table th, .table td {
  text-align: left;
  vertical-align: top;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
}
/* No zebra striping: a stripe fights the standing stroke and adds a second,
   meaningless rhythm. Rules do the separating. */
.table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--paper-2);
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--rule-strong);
}
.table tbody th { font-family: var(--sans); font-weight: 600; }
.table tbody tr:last-child th, .table tbody tr:last-child td { border-bottom: 0; }
.table td.mono, .table .mono { font-family: var(--mono); }

/* A row that states its standing in its first cell (/sources by source health,
   /runs history by run status). The .stand-* classes set pattern and colour but
   not width, and a table cell's initial border-width is the browser's `medium`,
   so the width is stated here rather than left implicit. .stand-fail and
   .stand-warn raise it to 4px themselves in section 19b, which is later and
   wins. */
.table th[class*="stand-"], .table td[class*="stand-"] {
  border-left-width: var(--stand-w);
}

/* A source's last error is full width, on its own wash, in mono, and its
   truncation discloses itself. The wash follows the SOURCE's state: a degraded
   source's message on the failing wash reads as a failure the journal did not
   record. */
.error-row td {
  padding-top: 0;
  font-family: var(--mono);
  font-size: var(--t-sm);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  background: var(--err-bg);
  color: var(--err-fg);
  border-left: var(--stand-w4) solid var(--err-fg);
}
.error-row.is-warn td {
  background: var(--warn-bg);
  color: var(--warn-fg);
  border-left-color: var(--warn-fg);
  border-left-style: dashed;
}
tr.is-hidden td, tr.is-hidden th { opacity: 0.75; }

.run-actions {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: var(--s4);
  margin-bottom: var(--s4);
}
.action-card {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  background: transparent;
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
}
/* An action card's title names a thing you can DO, so it is a heading in the UI
   voice, not another line of 11px furniture like the rail titles it shares a
   class with. */
.action-card .side-title {
  font-size: var(--t-base);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  margin-bottom: var(--s1);
}
.action-card input[type="number"] { width: 6rem; }
.action-card .field-help { margin-top: 0; }
.action-card form { margin-top: auto; }

.log {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-left: var(--stand-w) solid var(--rule-strong);
  border-radius: var(--radius-0);
  padding: var(--s3);
  margin: 0;
  overflow: auto;
  overscroll-behavior: contain;
  max-height: 70vh;
  font-size: var(--t-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.log-traceback { border-left-color: var(--err-fg); }

.worker-state { font-size: var(--t-base); margin: var(--s3) 0 0; }
.held-count { font-size: var(--t-sm); margin: 0 0 var(--s3); }
.run-notes, .run-reasons { margin: var(--s2) 0 0; padding-left: var(--s4); }
.section-traceback .section-title { border-bottom-color: var(--err-fg); }

/* The Apify spend line as a thermometer: a 1px bordered bar with the spend
   filled in ink and the budget marked by a tick. No donut, no gradient, and no
   inline style attribute - it is an inline SVG whose rects are filled with the
   presentation attribute fill="currentColor", so the fill follows this rule's
   colour in both themes and becomes the system text colour under forced
   colours with nothing pinned.

   There is no .thermo-fill. A block element with that name and no width would
   be 100% wide - a bar that silently reads as a fully spent budget - and no
   template can give it a width without an inline style attribute, which six
   routes assert never appears. */
.thermo {
  display: block;
  height: 0.75rem;
  border: 1px solid var(--rule-strong);
  background: var(--paper-2);
  color: var(--ink);
  margin: var(--s2) 0;
}

/* What failed: the errata page, and the only page allowed to be dense with
   state colour. The frozen class names are counted by the suite. */
.failure-list { margin: var(--s4) 0; padding: 0; display: grid; gap: var(--s4); }
.failure {
  background: var(--paper);
  /* Longhands, never the `border` shorthand: this rule sits after section 04 and
     a shorthand here would silently reset the standing stroke the failure level
     just set. The measured symptom was every failure on /failures showing the
     neutral grey stroke instead of the red one. */
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-left-width: var(--stand-w4);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
}
.failure-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  margin-bottom: var(--s2);
}
.failure-title { font-size: var(--t-lg); letter-spacing: var(--track-lg); margin: 0; }
.failure-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--ink-2);
}
.failure-when { font-size: var(--t-sm); color: var(--muted); }
.failure-count {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
}
.failure-what {
  font-size: var(--t-md);
  line-height: 1.55;
  max-width: var(--measure);
  margin: var(--s2) 0;
}
.failure-rows {
  margin: var(--s2) 0 var(--s3);
  padding: 0;
  list-style: none;
  font-family: var(--sans);
  font-size: var(--t-sm);
}
.failure-rows li { padding: var(--s1) 0; border-bottom: 1px solid var(--rule); }
.failure-rows li:last-child { border-bottom: 0; }
/* WHAT YOU CAN DO is the single most actionable text in the app, so it gets its
   own bordered block rather than being an inline strong in a paragraph. */
.failure-action {
  font-family: var(--sans);
  font-size: var(--t-base);
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s2) var(--s3);
  margin: var(--s3) 0;
  max-width: var(--measure);
}
.failure-action strong {
  display: block;
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s1);
}
.failure-links { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; margin: var(--s3) 0 0; }
.failure-link { margin: var(--s3) 0; }
.failure-diag { margin-top: var(--s3); }
.failure-diag summary {
  cursor: pointer;
  font-family: var(--sans);
  font-size: var(--t-sm);
  padding: var(--s1) 0;
}
.failure-diag .log { margin-top: var(--s2); max-height: 22rem; }
.failure-diag-none { font-size: var(--t-sm); margin-top: var(--s2); }

/* --------------------------------------------------------------------------
   18 - Forms, buttons, settings
   --------------------------------------------------------------------------
   Forms are the one place a 1px box is right; the rest of the page stays quiet
   around them. Inputs are never below 16px, because iOS zooms below that and
   the zoom breaks the layout. ------------------------------------------- */

input[type="text"], input[type="search"], input[type="number"], input[type="date"],
input[type="time"], input[type="password"], input[type="email"], select, textarea {
  font: inherit;
  font-size: var(--t-base);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--s2) var(--s3);
  max-width: 100%;
}
textarea { width: 100%; line-height: 1.45; resize: vertical; }
/* .mono is 0.9em everywhere else, which is right for a domain inside running
   text and wrong for a control: at 14.4px iOS zooms the page on focus and the
   layout moves under the reader. The JSON editor on /settings/advanced is the
   one input in the app that hits this. It keeps the mono VOICE (it is machine
   text) and the 16px floor. */
textarea.mono, input.mono, select.mono { font-size: var(--t-base); }
input[type="file"] { font: inherit; max-width: 100%; }
input[type="checkbox"] {
  width: 1.05rem; height: 1.05rem;
  accent-color: var(--accent);
  vertical-align: middle;
}

.btn {
  display: inline-block;
  font: inherit;
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1.3;
  cursor: pointer;
  text-decoration: none;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--s2) var(--s3);
  min-height: 2.25rem;
}
.btn:hover { border-color: var(--ink); text-decoration: underline; }
.btn.primary, .btn-primary {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
}
/* In dark the accent is a light violet, so the label has to be the dark ground
   rather than paper: 8.69:1 either way. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn.primary,
  :root:not([data-theme="light"]) .btn-primary { color: var(--bg); }
}
:root[data-theme="dark"] .btn.primary,
:root[data-theme="dark"] .btn-primary { color: var(--bg); }
.btn.primary:hover { text-decoration: underline; }
.btn-danger { color: var(--err-fg); border-color: var(--err-fg); }
.inline-form { display: inline; margin: 0; }

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3) var(--s4);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  font-size: var(--t-sm);
}
/* Same as the caption: section 03 names this selector, so the voice is already
   set and only the layout and the colour belong here. */
.filters label:not(.check) {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  color: var(--muted);
}
.filter-actions { display: flex; align-items: center; gap: var(--s3); }
.check { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; font-size: var(--t-base); }
.check-danger { color: var(--err-fg); }
.checks { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); }

.settings {
  display: grid;
  grid-template-columns: 13rem minmax(0, 1fr);
  gap: var(--s5);
  margin-top: var(--s4);
}
.settings-main { min-width: 0; }
/* Parks UNDER the condensed bar, not behind it: at top: var(--s3) the bar ate
   its first ~36px whenever the page was scrolled. Below 56rem it is static (see
   section 20), so this is a desktop-only rule.

   MEASURED, and the reason for the max-height: this rail is 843px tall - every
   settings page plus every source, none of them collapsed. A sticky element
   TALLER than the space between its `top` and the bottom of the viewport never
   pins at all; it just scrolls away, which is what it was doing on any screen
   shorter than about 950px. Bounding it to the viewport is what makes the rule
   true instead of only true on a tall monitor, and `auto` means the scrollbar
   appears only when it is actually needed. */
.settings-nav {
  position: sticky;
  top: calc(var(--topbar-h) + var(--s3));
  align-self: start;
  max-height: calc(100vh - var(--topbar-h) - var(--s5));
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  font-size: var(--t-sm);
}
.settings-nav ul { list-style: none; margin: 0 0 var(--s4); padding: 0; }
.settings-nav a {
  display: block;
  padding: var(--s2) var(--s3);
  border-left: 2px solid transparent;
  color: var(--ink-2);
  text-decoration: none;
}
.settings-nav a:hover { color: var(--ink); text-decoration: underline; }
.settings-nav a[aria-current] {
  color: var(--ink);
  font-weight: 600;
  border-left-color: var(--accent);
}
.settings-nav-title { color: var(--muted); margin: var(--s3) 0 var(--s1) var(--s3); display: block; }

.settings-form {
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius-0);
  padding: var(--s3) var(--s4);
  max-width: 48rem;
}
.field { padding: var(--s3) 0; border-bottom: 1px solid var(--rule); }
.field:last-of-type { border-bottom: 0; }
.field fieldset, fieldset.cats { border: 0; margin: 0; padding: 0; min-width: 0; }
.field-label {
  display: block;
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  margin-bottom: var(--s1);
}
/* Help is ALWAYS visible - never a tooltip, which is unreachable on touch and
   invisible in a printout. */
.field-help { font-family: var(--sans); font-size: var(--t-sm); color: var(--muted); margin: var(--s1) 0 0; }
.field-error { color: var(--err-fg); font-size: var(--t-sm); font-weight: 600; margin: var(--s1) 0 0; }
.field-warn { color: var(--warn-fg); font-size: var(--t-sm); font-weight: 600; }
/* A state word that also carries a state COLOUR keeps the furniture size. The
   source error rows on /sources write <span class="note-state field-error">, and
   without this the word renders at 13px - the one size the furniture voice never
   uses, which is how a second type scale gets in. */
.note-state.field-error, .note-state.field-warn { font-size: var(--t-xs); }
.has-error input, .has-error select, .has-error textarea { border-color: var(--err-fg); border-width: 2px; }
.form-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s2) var(--s3);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule-strong);
  margin-top: var(--s3);
}
.form-foot .field-label { flex-basis: 100%; margin: 0; }
.form-foot input[type="text"] { flex: 1 1 15rem; }

/* The save bar on a settings form. It follows the reader down a thirty-field
   form, so the button is never a scroll away from the field just changed, and
   it sits on paper with a rule above it so it reads as the foot of the form
   rather than as a floating widget. env(safe-area-inset-bottom) keeps it off
   the home indicator on a phone. The sentence underneath states the version
   this page was opened on - it is read where the decision is made. */
.settings-save {
  position: sticky;
  bottom: 0;
  z-index: 3;
  background: var(--paper);
  padding-bottom: calc(var(--s3) + env(safe-area-inset-bottom, 0px));
}
.settings-save-note { margin: var(--s2) 0 0; font-size: var(--t-sm); }
/* The JSON editor's own form is not paper-backed the same way; a sticky bar
   over a 30-row textarea would cover the text being edited on a short screen. */
@media (max-height: 34rem) {
  .settings-save { position: static; }
}

.cat-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 12rem);
  gap: var(--s2) var(--s3);
  padding: var(--s3);
  border: 1px solid var(--rule);
  border-radius: var(--radius-0);
  margin: var(--s3) 0;
  font-size: var(--t-sm);
  color: var(--muted);
}
.cat-row label:not(.check) { display: flex; flex-direction: column; gap: var(--s1); }
.cat-row input[type="text"] { width: 100%; }
.cat-name { font-weight: 600; }
.cat-key { display: flex; flex-direction: column; gap: var(--s1); }
.cat-key-fixed { color: var(--ink-2); padding: var(--s1) 0; font-family: var(--mono); font-size: var(--t-sm); }
.cat-desc { grid-column: 1 / -1; }
.cat-flags { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--s4); color: var(--ink-2); }
.cat-row-new { border-style: dashed; }
.cat-new-title { grid-column: 1 / -1; margin: 0; font-size: var(--t-base); font-weight: 600; color: var(--ink-2); }
.cat-row.is-deleting { opacity: 0.55; border-color: var(--err-fg); border-style: dashed; }

/* --------------------------------------------------------------------------
   19 - Footer
   -------------------------------------------------------------------------- */

.site-footer {
  border-top: 1px solid var(--rule-strong);
  margin-top: var(--s6);
  background: var(--paper);
}
.footer-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s1) var(--s4);
  padding-block: var(--s4) var(--s5);
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--muted);
}
.footer-row p { margin: 0; }
.schedule-line { flex: 1 1 20rem; }

/* --------------------------------------------------------------------------
   19b - The Standing Rule, stated explicitly
   --------------------------------------------------------------------------
   Last in the file on purpose. Section 04 holds the neutral default and the
   rules derived from the frozen macro markup with :has(); these are what a
   template says on purpose, so they must outrank every component rule above,
   including the ones that set a `border` or `background` shorthand.
   -------------------------------------------------------------------------- */

/* (a) explicit. Doubled class selectors so a template's stated intent always
   beats the derived rule above. */
.stand-confirmed.stand-confirmed { border-left-style: solid;  border-left-color: var(--stand-confirmed); }
.stand-news.stand-news           { border-left-style: dashed; border-left-color: var(--stand-news); }
.stand-headline.stand-headline   { border-left-style: dotted; border-left-color: var(--stand-headline); }
.stand-archive.stand-archive     { border-left-style: double; border-left-color: var(--stand-archive); }
.stand-fail.stand-fail           { border-left-style: solid;  border-left-color: var(--stand-fail); border-left-width: var(--stand-w4); }
.stand-warn.stand-warn           { border-left-style: dashed; border-left-color: var(--stand-warn); border-left-width: var(--stand-w4); }
.stand-lead.stand-lead           { border-left-style: solid;  border-left-color: var(--stand-lead); border-left-width: var(--stand-w4); }
.stand-none.stand-none           { border-left-style: solid;  border-left-color: var(--stand-none); }
.stand-unknown.stand-unknown     { border-left-style: dotted; border-left-color: var(--stand-none); }

/* HIDDEN - five simultaneous signals, four of them colour-free: the 45 degree
   hatched stroke, a dashed outer border, a recessed ground, the italic headline
   (the only other italic in the design is a quote's attribution, so italic
   stays unambiguous) and the Hidden badge the macro already emits.

   THE GROUND REPLACES A BLANKET `opacity: 0.66`, and the reason is measured.
   Opacity composites the whole card, so it dimmed the card's OWN disclosure
   furniture: the Hidden badge came out at 3.57:1, the News-only badge, the
   dateline and the source line at 2.97:1, the corroboration note at 4.12:1 -
   every one of them below AA, on exactly the words that tell the reader this
   story is suppressed and why. A suppressed story must still be legible ABOUT
   being suppressed. With the recessed ground and the two recessed text tokens
   below, the worst measurement inside a hidden card is 5.55:1 in light and
   5.57:1 in dark, and the card still reads as switched off at a glance. */
.is-hidden.is-hidden, .stand-hidden.stand-hidden,
.card.is-hidden, .lead-story.is-hidden, .front-card.is-hidden,
.rail-item.is-hidden, .resource.is-hidden, tr.is-hidden {
  border-left-color: transparent;
  border-left-width: var(--stand-w4);
  border-left-style: solid;
  background-image: var(--hatch);
  background-repeat: no-repeat;
  background-size: var(--stand-w4) 100%;
  background-position: left top;
  background-origin: border-box;
  background-color: var(--paper-2);
}
/* The story recedes; the disclosure does not. The headline and the dek drop one
   token (11.68:1 and 6.01:1 on paper, 11.1:1 and 5.55:1 on the recessed ground),
   while the badges, the dateline, the source line and the standing notes keep
   their full strength - they are what states that this story is hidden. */
.is-hidden .card-headline a, .is-hidden .front-headline a, .is-hidden .lead-headline a,
.is-hidden .story-headline a, .is-hidden .rail-headline, .is-hidden .archive-headline a {
  color: var(--ink-2);
}
.is-hidden .card-dek, .is-hidden .front-dek, .is-hidden .lead-dek { color: var(--muted); }
/* A NOTE IS NOT A HIDDEN CARD. This is where `.standing-note.stand-hidden { opacity: 1 }` used to sit,
   because `opacity: 0.66` dimmed the sentence that DISCLOSES a suppression - the "the defendants and
   status above are computed from visible stories only" note on /story and /case measured 2.81:1, below
   AA. The exemption is gone with the mechanism it defended: no hidden treatment dims anything now, on a
   note or on a card, so a note needs no rule to escape it. A standing note already sits on --paper-2,
   which is the ground a hidden card now takes, and it keeps the hatched stroke. */

.card.is-hidden, .lead-story.is-hidden, .front-card.is-hidden,
tr.is-hidden, .rail-item.is-hidden {
  border-style: dashed;
  border-left-style: solid;
}
.is-hidden .card-headline, .is-hidden .lead-headline, .is-hidden .front-headline,
.is-hidden .story-headline, .is-hidden .rail-headline { font-style: italic; }

/* --------------------------------------------------------------------------
   20 - Widths
   --------------------------------------------------------------------------
   Nothing collapses behind a tap. The header keeps every destination visible at
   every width; only the layout columns fold. -------------------------- */

@media (min-width: 70rem) {
  .front { grid-template-columns: minmax(0, 1fr) var(--rail); }
}

@media (max-width: 68.74rem) {
  .story-layout { grid-template-columns: 1fr; gap: var(--s4); }
}

@media (max-width: 56rem) {
  .settings { grid-template-columns: 1fr; gap: var(--s4); }
  .settings-nav {
    position: static;
    /* The viewport cap belongs to the sticky desktop rail only. Left on, it
       would turn the phone rail - a wrapped list about 600px tall - into an
       inner scroller on any handset shorter than about 680px. */
    max-height: none;
    overflow: visible;
    border: 1px solid var(--rule);
    padding: var(--s2);
  }
  .settings-nav ul { display: flex; flex-wrap: wrap; gap: var(--s1); margin-bottom: var(--s2); }
  .settings-nav a { border-left: 0; border-bottom: 2px solid transparent; }
  .settings-nav a[aria-current] { border-left: 0; border-bottom-color: var(--accent); }
}

@media (max-width: 40rem) {
  :root { --gutter: 1rem; }
  .header-row { padding-block: var(--s3) var(--s2); gap: var(--s2) var(--s3); }
  .brand { flex: 1 1 100%; margin-right: 0; }
  .brand-name { font-size: 1.125rem; }
  .search { flex: 1 1 100%; max-width: none; }
  .mainnav { gap: 0; }
  .nav-read, .nav-ops { width: 100%; }
  /* Every navigation target is a 44px tap target at phone width. The section
     chips are navigation too - they were the one row this rule had missed, and
     they measured 38px. */
  .mainnav a, .cat-link { padding-block: var(--s3); min-height: 2.75rem; display: inline-flex; align-items: center; }
  .front { gap: var(--s4); }
  .lead-story { padding-left: var(--s3); }
  .cards { grid-template-columns: 1fr; }
  .front-grid { grid-template-columns: 1fr; }
  .pill { min-width: 0; flex-basis: 100%; padding-inline: 0; }
  .pill + .pill { padding-left: 0; border-left: 0; border-top: 1px solid var(--rule); }
  .filters { flex-direction: column; align-items: stretch; }
  .filters select, .filters input[type="date"] { width: 100%; }
  .cat-row { grid-template-columns: 1fr; }
  .card { padding: var(--s3); }
  .story-side { border-left: 0; padding-left: 0; }
}

/* --------------------------------------------------------------------------
   21 - Motion, forced colors, print
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }
  .catnav { scroll-snap-type: none; }
}

/* Under forced colours the browser supplies every colour. That is exactly the
   case this design was built for: the PATTERN carries the state, so solid /
   dashed / dotted / double all survive untouched and nothing here needs to
   fight the user's theme. Author colours are deliberately NOT pinned with
   forced-color-adjust: none - a near-black stroke pinned onto a forced black
   ground is an invisible stroke.

   One exception, and it needed more than it had. The `hidden` stroke is a
   TRANSPARENT border filled by a repeating gradient, and the recessed ground is
   a background colour; forced colours drop the gradient and replace every
   background with the user's Canvas, so two of the five signals go at once. The
   stroke becomes dashed - but a news-only card's stroke is dashed too (3px
   against 4px is not a distinction a reader can make), so a hidden story was
   left signalled by its badge alone. It takes a SECOND concentric dashed frame,
   drawn as an outline so it costs no layout and cannot run into the text of a
   card whose right padding is zero: no other object in this design has one, and
   it survives any forced palette because CanvasText is whatever the reader's
   theme says ink is. The remaining signals are untouched: the all-round dashed
   border, the italic headline and the `Hidden` badge. !important is used
   because these have to beat the derived :has() rules and the doubled .stand-*
   classes, which are more specific by construction. */
@media (forced-colors: active) {
  .is-hidden, .stand-hidden.stand-hidden {
    background-image: none !important;
    border-left-style: dashed !important;
    border-left-color: CanvasText !important;
    outline: 2px dashed CanvasText !important;
    outline-offset: 2px !important;
  }
  /* The chip's glyph keeps its three shapes: filled circle, hollow ring, square. */
  .status-dot { background: CanvasText; border: 2px solid CanvasText; }
  .status-warn .status-dot { background: Canvas; }
  .status-err .status-dot { border-radius: 0; }
  .status-unknown .status-dot { background: Canvas; }
}

@media print {
  .site-header, .site-footer, .skip, .view-toggle, .pager, .edition-nav,
  .run-actions, .filters { display: none !important; }
  body { background: #fff; color: #000; }
  .wrap { max-width: none; padding: 0; }
  a { text-decoration: underline; }
  .log { max-height: none; }
}

/* --------------------------------------------------------------------------
   Inventory: what is styled here that nothing renders yet
   --------------------------------------------------------------------------
   Naming these is cheaper than rediscovering them, and deleting a class the
   suite or the next template may use is a silent break. Checked against all 28
   templates in this tree, not remembered.

   STYLED, NOT EMITTED BY ANY TEMPLATE TODAY
     .btn-primary     - the `.btn.primary` form is what every template writes;
                        both are styled so either markup renders.
     .masthead-mark   - superseded by `.masthead .brand-mark` when the mark moved
                        into the _mark.svg include.
     .is-deleting     - added by app.js when a category delete box is ticked.
     .topbar-name     - the condensed bar carries the running head (.topbar-page)
                        and the health chip; the wordmark stays in the plate, so
                        a second one 40px above it would be a duplicate. Styled
                        in case the bar ever carries it.
     .thermo-fill     - GONE. It was a block with no width, which renders as a
                        fully spent budget; the bar is an SVG with currentColor
                        rects instead. Both the rule and its forced-colors line
                        were removed rather than left to be found and used.

   EMITTED ONLY IN SOME STATES (all live, none dead)
     .status-empty .status-skipped .status-running .status-partial .status-failed
     .status-error .status-unknown .status-ok   - whatever the database holds.
     .health-off .health-unknown                - source states the fixture does
                                                  not currently produce.
     .notice-ok .notice-info                    - notice.level can be ok/info.
     .failure-err .failure-warn                 - both live on /failures.
     .cat-c0 ... .cat-c7 .cat-cx                - depends on how many sections
                                                  are enabled.
     .kv-inline .cards-single .search-big .log-traceback .failure-link
                                                - live, one page each.

   ADDED THIS ROUND AND NOW RENDERED (they were styled-but-unrendered in the
   foundation pass; every one of these is emitted by a template in this tree)
     .folio .folio-seg .folio-ok .folio-warn .folio-err   edition, first_run, run
     .ledger .ledger-field .ledger-label .ledger-value
     .ledger-yes .ledger-no                               story
     .standing-note .note-state                           failures, sources,
                                                          story, case, edition
     .slab .slab-state .slab-text                         base, run, settings
     .showing                                             editions, archive,
                                                          category, search, runs,
                                                          sources, settings
     .ops-eyebrow                                         sources, runs, failures
     .pill-label                                          run
     .topbar .topbar-row .topbar-page                     base
     .thermo                                              sources
     .is-current                                          story
     .stand-confirmed .stand-news .stand-headline .stand-archive .stand-lead
     .stand-fail .stand-warn .stand-hidden .stand-none .stand-unknown
                                                          edition, first_run,
                                                          run, runs, sources,
                                                          settings_version
     .month-head                                          editions
     .timeline-more                                       story, case
     .settings-save .settings-save-note                   settings_section,
                                                          settings_advanced
     .topbar-row                                          base

   NOT IMPLEMENTED, AND SAID SO RATHER THAN LEFT TO BE ASSUMED
     A sticky first column with a scroll cue on the 15-column run history
     (design_spec S10). It needs a modifier class on that one <table>, and a
     blanket `.table td:first-child { position: sticky }` would also grab the
     colspan=8 error row on /sources and paint over its state wash. The table's
     own <caption> states that it scrolls sideways, which is the disclosure;
     the sticky column is a convenience that is not worth a rule that lies on
     another page.
   -------------------------------------------------------------------------- */

/* An ARCHIVE card in the quiet-day well and on the first-run page: the same object as a front card, but
   it is not part of this edition and must not read as today's news. Same size and rhythm, one shade
   quieter, and its own headline class so nothing - including the test suite - counts it as an edition
   card. The "Archive" badge the macro already emits carries the meaning in words. */
.front-card.from-archive { border-left: var(--stand-w3, 3px) double var(--rule-strong); padding-left: 0.9rem; }
.archive-headline {
  font-family: var(--serif);
  font-size: 1.12rem;
  line-height: 1.25;
  margin: 0.1rem 0 0.2rem;
  letter-spacing: -0.012em;
}
.archive-headline a { color: var(--ink); text-decoration: none; }
.archive-headline a:hover, .archive-headline a:focus-visible { color: var(--link); text-decoration: underline; }

/* --------------------------------------------------------------------------
   13 - ARCH-AUTH: the account menu, the sign-in page, Settings > Access
   --------------------------------------------------------------------------
   Appended, not woven in: every rule below is new furniture for three new
   surfaces, and nothing above this line is redefined. Tokens only, so both
   themes and the forced-colors pass in section 12 keep applying.

   The account menu is a <details>. Not a stylistic preference: the
   Content-Security-Policy is `script-src 'self'` with no inline script, and six
   routes assert that no page carries one, so a scripted dropdown could not
   exist here. <details> opens with the keyboard, works with JavaScript off,
   announces its own expanded state, and degrades - with no stylesheet at all -
   to a summary line with three links under it. -------------------------- */

.account { position: relative; flex: none; }

.account-summary {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  cursor: pointer;
  list-style: none;                 /* the disclosure triangle is replaced below */
  padding: var(--s2) var(--s3);
  min-height: 2.75rem;              /* a 44px tap target on a phone */
  justify-content: center;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
}
.account-summary::-webkit-details-marker { display: none; }
.account-summary:hover { border-color: var(--ink); }

.account-eyebrow {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: var(--track-furniture);
  text-transform: uppercase;
  color: var(--muted);
}
/* The name is a machine fact read out of the database, so it takes the mono
   voice section 03 reserves for exactly that - it is not the owner's title. */
.account-name { font-family: var(--mono); font-size: var(--t-sm); color: var(--ink); }

/* Two carriers for the state, because neither alone is enough. A screen reader
   is told "expanded"/"collapsed" by <details> itself, which is why the caret in
   the markup is aria-hidden - it would otherwise be read out as the name of a
   triangle. A sighted reader gets the caret (so the name reads as something
   that opens, not as a label) plus the summary's border darkening, which is a
   change in WEIGHT and survives greyscale and forced-colors. */
.account[open] .account-summary { border-color: var(--ink); }
.account-caret {
  display: inline-block;
  margin-left: var(--s2);
  font-family: var(--sans);
  color: var(--muted);
}
.account[open] .account-caret { transform: rotate(180deg); }

.account-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s1));
  z-index: 35;                      /* above .topbar's 30 */
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-width: 14rem;
  padding: var(--s3);
  background: var(--paper);
  border: 1px solid var(--ink);
  border-radius: var(--radius-0);
}
.account-menu a {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--link);
  text-decoration: none;
  padding-block: var(--s1);
}
.account-menu a:hover, .account-menu a:focus-visible { text-decoration: underline; }
/* Sign out is a form with a real button, never a link: a GET that destroys a
   session can be fired by any image tag on any page the owner happens to open,
   and the CSRF token the middleware checks can only ride on a POST. */
.account-signout {
  margin: 0;
  padding-top: var(--s2);
  border-top: 1px solid var(--rule);
}
.account-signout .btn { width: 100%; }

/* At phone width the masthead already gives the brand and the search a full row
   each; the menu takes its own rather than being squeezed beside the search,
   and it opens in flow instead of floating over the page it came from. */
@media (max-width: 40rem) {
  .account { flex: 1 1 100%; }
  .account-summary { width: 100%; }
  .account-menu { position: static; min-width: 0; margin-top: var(--s2); }
}

/* -------------------------------------------------------------- the sign-in page
   One column, at form measure rather than reading measure, centred in the page
   because there is nothing else on it to align to. */
.login-wrap { max-width: 30rem; }

.login { display: flex; flex-direction: column; gap: var(--s4); padding-top: var(--s6); }

.login-nameplate {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding-bottom: var(--s3);
  border-bottom: 2px solid var(--ink);
}
.login-nameplate .brand-mark { flex: none; color: var(--accent); width: 40px; height: 40px; }
.login-title {
  font-family: var(--serif);
  font-size: var(--t-xl);
  font-weight: 600;
  letter-spacing: var(--track-xl);
  margin: 0;
}

.login-form { display: flex; flex-direction: column; gap: var(--s3); }
.login-form .field { margin: 0; }
.login-form input { width: 100%; }
.login-form .btn { align-self: flex-start; }

/* The one sentence the page is allowed to say about itself. Quiet, in the sans
   furniture voice - it is the journal labelling itself, not editorial text. */
.login-note {
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--muted);
  margin: 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
}

/* ------------------------------------------------------- Settings > Access
   A token is shown exactly once, so it has to survive being selected on a
   phone: it wraps on any character rather than scrolling off the panel, and it
   sits on the inset surface so it reads as a value to copy, not as prose. */
.token-plain {
  font-family: var(--mono);
  font-size: var(--t-sm);
  background: var(--paper-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-0);
  padding: var(--s3);
  margin: var(--s3) 0;
  overflow-wrap: anywhere;
  word-break: break-all;
  user-select: all;                 /* one click or tap selects the whole token */
}

/* The access surface is a run of h3s inside one panel; without this they sit at
   the same rhythm as the paragraphs between them and the five doors read as one
   undifferentiated wall. */
#surface .subhead { margin-top: var(--s4); }
#surface .subhead:first-of-type { margin-top: var(--s2); }
#surface .badge { vertical-align: middle; }
