/* Echo Future — Registry (ADR-0027): a public register of predictions.
   A records office, not a feed: a four-column ledger (FILED · CLAIM · RESOLVES ·
   STANDING) on warm paper, claims in Spectral, every measured value in IBM Plex
   Mono, verdicts as rubber stamps, and ONE shared NOW→10Y axis down the register
   so spans are comparable in a single scan (the axis is views.AxisPos, whose
   log10(days)/log10(3650) scale is exactly the imported design's tick spacing).
   Contrast ratios below are measured against EVERY ground a token lands on, not
   just the page background, and views/design_guard_test.go asserts them — the
   source mockup's own #8f8a7c metadata is only 3.05:1, so it is darkened here on
   purpose, and the obvious first correction still failed on --surface. Don't
   "restore" a lighter value. */

/* Self-hosted fonts (OFL, latin subset) — no third-party CDN, so no per-page
   request leaks a user's IP to Google and the binary stays offline-safe.
   Spectral has no variable cut; 500 roman + 400 italic are the two styles the
   design actually uses. Archivo is variable across the weight axis. */
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 500; font-display: swap; src: url(/static/fonts/spectral-500.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: italic; font-weight: 400; font-display: swap; src: url(/static/fonts/spectral-italic.woff2) format('woff2'); }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400 800; font-display: swap; src: url(/static/fonts/archivo.woff2) format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(/static/fonts/plex-mono-400.woff2) format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url(/static/fonts/plex-mono-500.woff2) format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url(/static/fonts/plex-mono-600.woff2) format('woff2'); }

/* ── Night register ── the design's own dark values; it is a second ground, not
   an inversion of paper. ── */
/* color-scheme tells the UA which palette to paint NATIVE controls from — <select>, date
   inputs, scrollbars, the date picker. Without it the UA assumed light regardless of
   data-theme, so every form control on a dark page painted light-on-dark. Author CSS cannot
   fix this from the outside: an `appearance: auto` control lets the UA override background
   and colour, which is why a `background: var(--surface)` on select.chip measured as the
   light value on a dark page. Declared per theme so it tracks the app's own switch, not the
   OS's. */
html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #191a16;            /* register ground */
  --surface: #141510;       /* masthead / chrome — recedes behind the register */
  --raised: #22231d;        /* modals, dropdowns */
  --elevated: #23241d;      /* hover tint / inset fill */

  --fg: #e8e5d9;            /* 13.9:1 */
  --muted: #b0ac9d;         /*  7.7:1 */
  --muted-2: #918c7c;       /*  >=4.66:1 on every night ground (bg/surface/elevated/raised).
                               Lightened from the mockup's #7d7a6c (4.06:1) and again from #8a8576,
                               which failed on --elevated (4.25:1). Same rule as paper: check a
                               token against every surface it lands on. */

  --accent: #93a9d8;        /*  7.4:1 — registrar's blue, lifted for the dark ground */
  --accent-light: #adbde3;
  --accent-contrast: #191a16;
  --accent-soft: rgba(147, 169, 216, 0.14);

  --border: #33342c;
  --border-strong: #4a4940;

  /* One ground behind every overlay. The drawer's scrim and the modal's were two
     copies of the same literal, so darkening one left the other lighter and the two
     read as different surfaces on the same screen (ADR-0044). */
  --scrim: rgba(0, 0, 0, 0.55);

  --success: #7fbf95;
  --error: #e08a76;
  --warning: #e0b85a;
  --good-bg: rgba(127, 191, 149, 0.14);
  --good-fg: #7fbf95;       /* the mockup shows no night Hit sample; 8.2:1 */
  --bad-bg: rgba(224, 138, 118, 0.14);
  --bad-fg: #e08a76;        /* 6.7:1 */
  --warn-bg: rgba(224, 184, 90, 0.14);
  --warn-fg: #e0b85a;

  --shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
}

/* ── Paper register (default) ── */
:root,
html[data-theme="light"] {
  color-scheme: light;
  --bg: #f3f1e9;
  --surface: #efeada;
  --raised: #f6f4ed;
  --elevated: #e9e5d5;

  --fg: #22241d;            /* 13.9:1 */
  --muted: #55524a;         /*  6.9:1 */
  --muted-2: #6b6656;       /*  5.07:1 on --bg, 4.77:1 on --surface, 4.55:1 on --elevated.
                                Darkened from the mockup's #8f8a7c (3.05:1, fails AA) — and then
                                again from #726d5e, which cleared 4.5:1 on --bg but only reached
                                4.29:1 on --surface, the ground the sidebar, topbar and search
                                placeholder actually sit on. Check new consumers against THEIR
                                background, not the page's. */

  --accent: #35538f;        /*  6.7:1 */
  --accent-light: #43629f;
  --accent-contrast: #f3f1e9;
  --accent-soft: rgba(53, 83, 143, 0.10);

  --border: #d8d4c4;        /* the register's rules */
  --border-strong: #bcb6a2; /* the masthead's double rule */

  --scrim: rgba(0, 0, 0, 0.55);

  --success: #2f6b45;
  --error: #a63b2e;
  --warning: #7d5f10;
  --good-bg: #e3ebe2;
  --good-fg: #2f6b45;       /* 5.6:1 */
  --bad-bg: #f0e2dc;
  --bad-fg: #a63b2e;        /* 5.7:1 */
  --warn-bg: #efe7d0;
  --warn-fg: #7d5f10;

  --shadow: 6px 6px 0 rgba(34, 36, 29, 0.06);   /* the register's hard offset, not a blur */

  /* A register is ruled, not rounded. */
  --radius: 2px;
  --radius-sm: 0px;

  --font:  'Archivo', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono:  'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --serif: 'Spectral', ui-serif, Charter, Georgia, serif;

  /* Registry's scale — a real display tier, and 11px as the floor. */
  --t-display: 46px;
  --t-h1: 34px;
  --t-h2: 24px;
  --t-claim: 20px;
  --t-body: 15px;
  /* The floor under every user-text height bound (ADR-0079). ONE source: the literal was repeated
     at seven sites, which is the drift `.board-body`'s own comment argues against. 30rem = 480px
     at the default root, which is what `32em` already resolved to on every body-sized surface —
     the arithmetic ADR-0056 §2 recorded as 512px assumed a 16px document and this one is 15px. */
  --text-floor: 30rem;
  --t-sub: 13.5px;
  --t-meta: 12px;
  --t-micro: 11px;

  --measure: 64ch;
}

@media (prefers-color-scheme: dark) {
  html[data-theme="system"] {
    color-scheme: dark;
    --bg: #191a16;
    --surface: #141510;
    --raised: #22231d;
    --elevated: #23241d;
    --fg: #e8e5d9;
    --muted: #b0ac9d;
    --muted-2: #918c7c;
    --accent: #93a9d8;
    --accent-light: #adbde3;
    --accent-contrast: #191a16;
    --accent-soft: rgba(147, 169, 216, 0.14);
    --border: #33342c;
    --border-strong: #4a4940;
    --scrim: rgba(0, 0, 0, 0.55);
    --success: #7fbf95;
    --error: #e08a76;
    --warning: #e0b85a;
    --good-bg: rgba(127, 191, 149, 0.14);
    --good-fg: #7fbf95;
    --bad-bg: rgba(224, 138, 118, 0.14);
    --bad-fg: #e08a76;
    --warn-bg: rgba(224, 184, 90, 0.14);
    --warn-fg: #e0b85a;
    --shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  }
}


* { box-sizing: border-box; }

body {
  margin: 0;
  font: var(--t-body, 16.5px)/1.6 var(--font, system-ui), -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
::selection { background: var(--accent); color: var(--accent-contrast); }
a { color: var(--accent); }
[x-cloak] { display: none !important; }
.hidden { display: none; }

/* Keyboard focus: a visible indicator on every interactive element (overrides
   component-level outline:none), shown only to keyboard/AT users so mouse
   interaction stays clean. WCAG 2.4.7. */
:focus-visible {
  outline: 2px solid var(--accent) !important;
  outline-offset: 2px;
}

/* Text for assistive tech only. The register labels its columns once, in a head
   the rows are not descendants of, so each row has to name its own two dates —
   otherwise "27 Jul 2026 ... 31 Dec 2026" is read with no clue which is which. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── App shell ── */
.app-shell {
  display: grid;
  grid-template-columns: 256px 1fr;
  grid-template-rows: 64px 1fr;
  grid-template-areas: "sidebar topbar" "sidebar main";
  min-height: 100vh;
}
/* ── The COLLAPSED DESKTOP RAIL, and nothing else (ADR-0043 §1) ──
   Scoped to the desktop because a phone has no rail to collapse: the `sidebar`
   cookie is path=/ and lives a year, so it is read at every width, and these
   rules used to reach the drawer and win on specificity.

   What that cost, measured: `.app-shell.sidebar-collapsed .sidebar` (0,3,0)
   also carried the drawer's off-canvas transform and out-ranked
   `.sidebar.drawer-open` (0,2,0), so for anyone carrying the cookie the drawer
   NEVER OPENED — class applied, transform still translateX(-280px), while the
   scrim painted, the topbar and the record went inert, the body locked and focus
   moved into an off-screen aside. A black screen with no visible way out. And
   `.nav-cta` below (0,4,1) beat the 44px floor (0,2,1), leaving the drawer's
   primary action 39px tall with no horizontal padding.

   ADR-0042 fought these rule by rule from inside the phone block, restoring
   .brand-name, .brand-sub, .brand-link, .sidebar-foot and .brand one at a time —
   a list that can only be as complete as its author's memory, and was not. One
   media query replaces all of it, and the next rule added here cannot reach a
   phone at all.

   768.02, not 769: a media query is evaluated against a FRACTIONAL layout viewport,
   so `max-width: 768px` / `min-width: 769px` is not a partition — it leaves the open
   interval (768, 769) matched by neither block. Reachable through fractional display
   scaling and some zoom levels, and there the shell has no rules at all: a viewer
   carrying the year-long `sidebar=collapsed` cookie gets the server-rendered class
   and `aria-expanded="false"` on a rail that renders at its full 256px, so what the
   rail looks like and what it announces come apart — the drift REG-DESIGN-021 exists
   to prevent. Before ADR-0043 these rules were unscoped, so the gap is new (ADR-0044,
   REG-MOBILE-027).

   0.02 and not 0.01 is Bootstrap's constant, chosen because Safari rounds a hundredth
   away. It NARROWS the gap rather than erasing it — a viewport resolved at 1/64px could
   still land on 768.015625 — and the exact complement would be
   `@media not all and (max-width: 768px)`. The convention is kept because it is the one
   the ecosystem tests, but the residue is real and is not worth claiming away. ── */
@media (min-width: 768.02px) {
  .app-shell.sidebar-collapsed { grid-template-columns: 64px 1fr; }
  .app-shell.sidebar-collapsed .brand-name,
  .app-shell.sidebar-collapsed .brand-sub,
  .app-shell.sidebar-collapsed .sidebar nav span,
  .app-shell.sidebar-collapsed .sidebar-foot { display: none; }
  /* Collapsed: drop the wordmark/logo and center the toggle so it stays inside the
     64px rail. Otherwise the 40px logo pushes the toggle past the edge, where the
     sidebar's overflow:hidden clips it — leaving no visible way to expand again. */
  .app-shell.sidebar-collapsed .brand { justify-content: center; padding: 0; }
  .app-shell.sidebar-collapsed .brand-link { display: none; }
  .app-shell.sidebar-collapsed .sidebar-toggle { margin-left: 0; }
  .app-shell.sidebar-collapsed .sidebar nav .nav-cta { margin-left: 6px; margin-right: 6px; padding: 10px 0; }
}

/* ── Sidebar ── */
.sidebar {
  grid-area: sidebar;
  background: var(--surface);
  border-right: 1px solid var(--border-strong);
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* The brand block is the register's masthead: a set wordmark over a mono
   sub-line, closed by the double rule Registry uses to open a document. Its
   64px height matches the topbar row, and the border is inside it (global
   border-box), so the two stay aligned. */
.brand {
  height: 64px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 14px;
  border-bottom: 3px double var(--border-strong);
}
.brand a.brand-link { display: flex; align-items: center; gap: 10px; text-decoration: none; min-width: 0; position: relative; }
.brand img.logo { width: 26px; height: 26px; flex-shrink: 0; }
.brand-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.brand-name {
  color: var(--fg); font-family: var(--font); font-weight: 800; font-size: 15px;
  letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap;
}
.brand-sub {
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 500; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted-2); white-space: nowrap;
}
/* The brand tooltip (ADR-0052) — revives the retired brand-tip (ADR-0027 Task 8) with the
   same 500ms deliberate-hover delay, but built against current tokens rather than restored
   verbatim: the original's hardcoded 10px radius fails RuledNotRounded outright, and its
   z-index 50 was never reconciled with the panels ADR-0034 added afterward. Matches
   .acct-menu/.search-results's own treatment instead — same ground, rule, radius and shadow,
   same z-index tier — so the masthead gets a third dropdown-style panel that reads as a
   sibling of the other two, not a fourth one-off style. Hidden below 768px like the original:
   a hover-only affordance means nothing without a pointer device.

   `visibility` carries the accessibility contract that `opacity`/`pointer-events` cannot
   (PR #48 review): opacity alone still leaves the span in the accessible tree, so
   .brand-link's computed name included the full tooltip prose on EVERY render, not just while
   revealed — same idiom `.sidebar` already uses for the phone drawer. `visibility: hidden` at
   rest excludes it from both name-from-content and the aria-describedby it also carries.
   `visibility 0s 200ms` (zero duration, 200ms delay) in the transition list means going
   visible→hidden the flip waits until the opacity/transform fade has finished, so it isn't
   pulled from the accessible tree mid-fade; going hidden→visible, the hover rule's own
   `transition-delay: 500ms` below overrides this per-property delay for all three properties,
   so the flip lands the instant the deliberate-hover wait ends, in step with the fade-in. */
.brand-tip {
  position: absolute; left: 0; top: 100%; margin-top: 10px;
  width: 240px; padding: 12px 14px; z-index: 40;
  background: var(--raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  color: var(--muted); font-family: var(--font); font-size: var(--t-sub); line-height: 1.6;
  visibility: hidden;
  opacity: 0; pointer-events: none; transform: translateY(-4px);
  transition: opacity 200ms ease, transform 200ms ease, visibility 0s 200ms;
}
.brand-link:hover .brand-tip,
.brand-link:focus-visible .brand-tip,
.brand-link:focus-within .brand-tip {
  visibility: visible;
  opacity: 1; pointer-events: auto; transform: translateY(0);
  transition-delay: 500ms; /* deliberate hover, unchanged from the original */
}
@media (max-width: 768px) {
  .brand-tip { display: none; }
}
.sidebar-toggle { margin-left: auto; }

/* The nav is an index of the register's sections: mono, tracked, uppercase, the
   same setting as the tabs. State is marked with a RULE rather than a fill —
   the tabs' 2px accent underline, rotated to the vertical — so the sidebar never
   carries a saturated block of colour that outweighs the claims it sits beside. */
.sidebar nav { flex: 1; padding: 14px 0; display: flex; flex-direction: column; gap: 1px; }
.sidebar nav a,
.sidebar nav .nav-cta {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  color: var(--muted-2);
  text-decoration: none;
  padding: 9px 14px;
  border-left: 2px solid transparent;
  border-radius: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 150ms ease, border-color 150ms ease;
}
.sidebar nav svg.nav-icon { width: 15px; height: 15px; flex-shrink: 0; }
.sidebar nav a:hover { color: var(--fg); border-left-color: var(--border-strong); }
.sidebar nav a.active { color: var(--fg); font-weight: 600; border-left-color: var(--accent); }
.sidebar nav a.active svg.nav-icon { color: var(--accent); }

/* The one filled control in the chrome — square, the way the design files it. */
.sidebar nav .nav-cta {
  margin: 12px 14px 0;
  padding: 10px 12px;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-contrast);
  border-left: 0;
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-size: var(--t-meta);
  font-weight: 600;
  letter-spacing: 0.06em;
}
.sidebar nav .nav-cta:hover { background: var(--accent-light); color: var(--accent-contrast); }
.sidebar-foot {
  padding: 12px 14px; border-top: 1px solid var(--border-strong);
  color: var(--muted-2); font-family: var(--mono); font-size: var(--t-micro); line-height: 1.7;
}

/* ── Topbar ── */
.topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border-strong);
}
/* Three marks: theme · bell │ seal (ADR-0034). The nickname, the Settings ghost and the
   exposed Log out form that used to sit here are behind the seal now — see .acct below.
   The gap is what centres the .actions-rule separator, so it has no offsets of its own. */
.actions { display: flex; gap: 10px; align-items: center; }

.icon-btn {
  background: transparent; color: var(--muted);
  border: 0; border-radius: var(--radius-sm); padding: 7px 9px;
  cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center;
  transition: all 150ms ease;
}
.icon-btn:hover { background: var(--elevated); color: var(--fg); }
.icon-btn .ico { width: 18px; height: 18px; }
.ico-moon { display: none; }
html[data-theme="light"] .ico-sun { display: none; }
html[data-theme="light"] .ico-moon { display: inline-block; }
@media (prefers-color-scheme: light) {
  html[data-theme="system"] .ico-sun { display: none; }
  html[data-theme="system"] .ico-moon { display: inline-block; }
}

/* min-width:0 keeps the main grid item from growing to its content's min-content
   (the default min-width:auto), which otherwise lets wide, non-wrapping content
   like the topic-chips carousel push the column — and the full-width cards — past
   the viewport on narrow screens. */
main { grid-area: main; padding: 28px 32px; max-width: 1100px; min-width: 0; }

/* A global type scale. Until now every page heading rendered at the UA default (2em of
   the 15px body), so the scale ADR-0027 §2 declares was only honoured where a class
   happened to set it. Class-level rules (.pd-statement, .land-name) still win. */
h1 { font-family: var(--font); font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: -0.01em; margin: 0 0 16px; }
h2 { font-family: var(--font); font-size: var(--t-h2); font-weight: 700; line-height: 1.25; margin: 26px 0 10px; }
h3 { font-family: var(--font); font-size: var(--t-body); font-weight: 700; margin: 0 0 8px; }
.placeholder h1 { margin: 0 0 8px; }
.hint { color: var(--muted); font-size: var(--t-sub); }

/* ── Buttons ── */
button {
  background: var(--accent);
  color: var(--accent-contrast);
  border: 0;
  border-radius: var(--radius-sm);
  padding: 9px 16px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: all 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
button:hover { background: var(--accent-light); }
/* .ghost is a MODIFIER, not a button-only rule. Element-qualified as button.ghost, the
   two ANCHORS that ask for it — the topbar's Settings link and the audit pager —
   rendered as bare accent text with no border, no padding and no hit area.
   De-qualifying alone is not enough: an <a> inherits nothing from the `button` element
   rule, so everything it needs is restated here. */
.ghost {
  display: inline-block;
  background: transparent; color: var(--fg);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 9px 16px; font: inherit; font-weight: 600; text-decoration: none;
  cursor: pointer; transition: all 150ms ease;
}
.ghost:hover { background: var(--elevated); color: var(--fg); transform: none; }
a.button, .button {
  display: inline-block;
  background: var(--accent); color: var(--accent-contrast);
  border-radius: var(--radius-sm); padding: 9px 18px;
  font-weight: 600; text-decoration: none;
  transition: all 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
a.button:hover, .button:hover { background: var(--accent-light); }
button.link { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px 4px; font: inherit; transition: color 150ms; }
button.link:hover { color: var(--accent); background: none; transform: none; }


pre {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px;
  min-height: 1em;
  margin-top: 12px;
}

/* ── Search (pill, reference) ── */
/* The register's search is a ruled line, not a pill: no fill, no box, no radius —
   just a mono field on a hairline, the way a form in a records office is ruled.
   Keyboard focus still gets the global :focus-visible outline; the border colour
   is the pointer-focus affordance. */
/* The two phone-only shell controls (ADR-0042 §§2,4), hidden on the desktop masthead by
   SPECIFICITY, not by source order (ADR-0045). This rule and `.icon-btn`'s
   `display: inline-flex` were both one class, ~80 lines apart, so nothing but the gap
   between them decided the outcome — and the natural place to add the next `.icon-btn`
   rule is after this one, which would put a second burger in the desktop masthead with
   TWO elements carrying `aria-controls="sidebar"` and contradictory `aria-expanded`
   values in the a11y tree. The `.topbar` ancestor is (0,2,0) and both live in the topbar,
   so a later single-class rule cannot reach them. Their un-hide rules — in the ≤768px and
   ≤600px blocks, on their OWN axes — carry the same qualifier for the same reason. */
.topbar .drawer-toggle,
.topbar .search-toggle { display: none; }
/* ── THE GROUND BEHIND AN OVERLAY (ADR-0046 §3). One owner, and it is a selector rather than a
   flag. `document.body.style.overflow` used to be written from layout.templ's x-effect keyed on
   `nav` alone, so the auth modal — promoted by ADR-0045 §4 to "a LAYER, not an overlay drawn on
   a live shell" — left its record scrolling, and the drawer→modal handoff actively UNLOCKED
   because `@show-auth-modal` sets `nav = false` and the else branch ran in the same flush.

   Each overlay's own truth is what is read here: the drawer's `.drawer-open` class, and the
   dialogs' `[open]`. The three `is-open` classes this used to read are DELETED (ADR-0048 §4) —
   one was bound to `authModal`, one to the onboarding modal's local `open`, and one was static
   because the Tune manager removed itself from the DOM and presence had to stand in for state.
   `[open]` is the dialog's own attribute, set and cleared by `showModal()`/`close()`, so there
   is nothing left for a template to keep in step: the class could be forgotten on a new overlay
   or on one of three removal paths, and a page left permanently unscrollable is worse than a
   scrolling ground. That reasoning is why this is a selector rather than a flag, and the same
   reasoning is why it now reads the platform's attribute rather than one of ours.

   `dialog:modal`, NOT `dialog[open]` (ADR-0050 §2). `[open]` was chosen because it is "the literal
   a design guard can assert" — optimizing a selector so a string-matching test can read it, which
   is the wrong direction when the /chrome pass already measures
   `getComputedStyle(document.body).overflow` directly. The two are not interchangeable: `[open]`
   is set by `show()` too, and a NON-modal dialog has no close watcher and inerts nothing, so it
   would pin the viewport with no way to unfreeze it and no visible cause — the same end state the
   restored-overlay path produced before ADR-0049 (`open` but not `:modal`, ground locked, Escape
   dead). Measured: appending a bare `<dialog>` and calling `show()` gave
   `{ bodyOverflowWithNonModalOpen: "hidden", bodySelectorMatched: true }`, and `afterRemove:
   "visible"`. `:modal` matches every overlay this app ships and cannot be reached that way.

   TWO RULES, NOT ONE WITH TWO PRELUDES, and that is a correctness requirement rather than
   formatting (ADR-0051). `:has()` is Chrome 105+/Safari 15.4+/Firefox 121+; `:modal` is Chrome
   105+/Safari 15.6+/Firefox 111+. They do NOT share a baseline, and the claim that they did was
   written into this comment. A style rule's selector list is non-forgiving — an unparseable member
   invalidates the WHOLE rule — so on Safari/iOS 15.4-15.5 the shared prelude took the DRAWER's lock
   down with the modal's, on a surface neither ADR-0048 nor ADR-0050 touched, and strictly worse
   than the `body:has(.modal-backdrop.is-open)` form it replaced (whose twin was a plain class
   selector, parsing wherever `:has()` did). Split, an engine that cannot read one arm still gets
   the other. Where `:has()` itself is unsupported the ground scrolls — a degradation, not a
   break. ── */
body:has(.sidebar.drawer-open) { overflow: hidden; }
body:has(dialog:modal) { overflow: hidden; }
/* ...and the ROOT SCROLLER keeps the containment half a browser can actually read (ADR-0051).
   ADR-0050 §1 retracted `touch-action` and `overscroll-behavior` from `.modal::backdrop` together,
   on a single argument — "there is no third surface", because the backdrop region and the modal's
   own box hit-test to the same `<dialog>`. That is exactly true of `touch-action`, which resolves
   from the HIT-TESTED element and its ancestors. It is not true of `overscroll-behavior`, which
   resolves on the SCROLL CONTAINER, and the root scroller is a real element under a condition this
   stylesheet already computes one line above.

   It cannot reach the dialog's own scrolling, because the dialog is not the root scroller — the
   defect that moved the pair off `.modal` in the first place is out of reach here. And it covers
   what `.modal { overscroll-behavior: contain }` does not: `contain` needs scrollable overflow to
   act on, and the auth and onboarding modals sit well under their `max-height`, so on those two it
   is inert. What remains is the iOS rubber-band over a ground whose `overflow: hidden` is known not
   to hold (the drawer scrim's own comment says so), and this is its canonical mitigation.

   Unverified on a device, like every touch rule here (ADR-0047 §3) — but CONSULTABLE, which is the
   one property the retracted declarations lacked and the reason they were retracted. */
html:has(dialog:modal) { overscroll-behavior: none; }
/* The wrapper is a REAL BOX at every width, and that is a correctness requirement,
   not a layout preference (ADR-0043 §2). It was `display: contents` here — elegant,
   because `.search` then stayed a direct flex child of `.topbar` and the desktop
   masthead was byte-identical. But Alpine's `outside` modifier begins:
   `offsetWidth < 1 && offsetHeight < 1 || …` — it SKIPS elements with no box. A
   `display: contents` wrapper measures 0×0, so `@click.outside` on it never fired and
   the masthead's search dropdown stopped dismissing on every desktop page. Measured
   against the vendored alpine.min.js 3.14.8: 0 handler calls at `contents`, 1 at `flex`.
   The geometry `.search` used to own moves here so the masthead still lays out the same. */
.search-wrap { position: relative; display: flex; align-items: center; flex: 1; max-width: 380px; min-width: 0; }
.search { position: relative; flex: 1; min-width: 0; }
.search input {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border-strong);
  border-radius: 0;
  color: var(--fg);
  padding: 7px 2px;
  font-family: var(--mono);
  font-size: 12.5px;
  transition: border-color 160ms ease;
}
.search input::placeholder { color: var(--muted-2); text-transform: uppercase; letter-spacing: 0.08em; }
.search input:focus { outline: none; border-bottom-color: var(--accent); box-shadow: none; }
.search-dd { position: absolute; top: 115%; left: 0; right: 0; z-index: 40; }
/* The dropdown renders the same Statement string `.pd-statement` and `.reg-claim` now break, and
   at <=600px it sits inside a box with `overflow-y: auto` (`.search-dd`, in the phone block —
   NOT the <=768px band, which this comment used to name) — whose computed `overflow-x` is `auto`
   too, so an unbreakable token turns the results panel into a sideways scroller instead of
   spilling. Its first rule of its own: until this round `.search-hit` appeared in exactly one
   place in this stylesheet, the bidi-isolation list. */
.search-hit { overflow-wrap: anywhere; }
.search-results {
  background: var(--raised); border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 12px; display: flex; flex-direction: column; gap: 10px; box-shadow: var(--shadow);
}
.search-group { display: flex; flex-direction: column; gap: 2px; }
.search-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
.search-results a { color: var(--fg); text-decoration: none; padding: 4px 0; font-size: 14px; }
.search-results a:hover { color: var(--accent); }
.search-all { color: var(--accent) !important; font-weight: 600; }
.explore-search {
  width: 100%; background: none; border: 0; border-bottom: 1px solid var(--border-strong);
  color: var(--fg); border-radius: 0; padding: 8px 2px; margin-bottom: 16px;
  font-family: var(--mono); font-size: 12.5px;
}
.explore-search::placeholder { color: var(--muted-2); text-transform: uppercase; letter-spacing: 0.08em; }
.explore-search:focus { outline: none; border-bottom-color: var(--accent); }

/* ── Modal ── */
/* The overlay host is where an overlay LIVES — outside the shell's grid, so a modal cannot be
   clipped or stacked by it. It was also a binding target, carrying the one `:inert="nav"` that
   reached all three overlays at once (ADR-0043 §5); that binding is gone with ADR-0048, because
   ancestor `inert` does not reach a top-layer dialog and every child here is one now.
   `display: contents` is what keeps "the wrapper draws nothing" structural instead of
   incidental. Without a rule it was a static block, and the un-chromed interests fragment
   `Feed.InterestsPanel` already serves would have rendered in flow at the foot of every page.
   Safe here and fatal on `.search-wrap` (ADR-0043 §2) for the reason in reverse: Alpine's
   `outside` modifier measures offsetWidth/Height and skips anything that computes to 0×0, and
   nothing measures this element (MD8). */
.overlay-host { display: contents; }
/* ── THE OVERLAY DIALOGS (ADR-0048). `.modal` is now the `<dialog>` ITSELF, not a box inside a
   `.modal-backdrop` wrapper: `showModal()` puts this element in the top layer and `::backdrop`
   paints the ground behind it, so the wrapper had no remaining job. Three consequences are
   load-bearing and none of them is obvious.

   FIRST, THE UA BOX NEEDS A REAL RESET, not just backdrop styling. Measured against this
   stylesheet, a bare `<dialog>` arrives with `border: 3px solid`, `padding: 15px`,
   `background: white` and a `max-width` that clamps the 92vw asked for below to ~354px at
   390px. Every one of those five is overridden here, `max-width` included, so `width` is what
   decides. `max-height` is NOT left to the UA either — it is stated, with `overflow: auto`, so
   a modal taller than the viewport scrolls instead of overflowing a flex-centred box off both
   ends the way the old `.modal-backdrop` did.

   SECOND, `display` MUST BE STATED FOR BOTH STATES. The UA hides a closed dialog with
   `dialog:not([open]) { display: none }`, and an author `.modal { display: flex }` beats it on
   origin — a closed modal would be painted over the page on every load. So the box's `display`
   lives on `[open]` and the base rule keeps it hidden. That is also what retires `x-cloak` on
   these three: a closed dialog is hidden before any script runs, which the `x-show` it replaces
   could never be.

   THIRD, THE BLUR MOVES EXPLICITLY. The UA `::backdrop` is `rgba(0, 0, 0, 0.1)` with no filter,
   so `--scrim` and the 2px blur both have to be restated here or the ground behind an overlay
   silently lightens. ── */
.modal {
  background: var(--raised); border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 26px; width: min(440px, 92vw); max-width: none; box-shadow: var(--shadow);
  color: var(--fg); max-height: calc(100vh - 32px); overflow: auto;
  display: none; flex-direction: column; gap: 12px;
  /* `overscroll-behavior: contain` and NO `touch-action`, which is the drawer's pairing exactly:
     `.sidebar` scrolls and declares only `contain`, while `.drawer-scrim` — which scrolls
     nothing — is the one that carries `touch-action: pinch-zoom`. The first version of this rule
     put `pinch-zoom` here too, and that is a defect this conversion INTRODUCED rather than
     inherited: `pinch-zoom` permits multi-finger pan and zoom of the page and forbids the
     one-finger drag that scrolls, so declaring it on the element that IS the scroll container
     kills the modal's own scrolling. On HEAD^ the same declaration sat on the `.modal-backdrop`
     WRAPPER and was harmless, because an ancestor's touch-action does not propagate into a
     descendant scroll container; moving it onto the box is what made it bite, and this modal is
     a scroll container now (`max-height` + `overflow: auto` above) where the old one was not.
     Worst case was onboarding, whose Done and Skip sit below a chip list it had just become
     possible — and then impossible — to scroll to.

     PROVENANCE, because it decides how much to trust this: the defect was measured during the
     ADR-0048 review pass, which reported the manager panning its full 220px range at 9 of 11
     probe points on HEAD^ against 0px at 8 of 11 with the declaration moved onto the scroller,
     a wheel scrolling 300px throughout, and an isolator pair changing nothing but which element
     carries the rule. It is NOT reproducible from this repo's own rig: both CDP touch paths were
     tried here and neither is gated by touch-action — `Input.synthesizeScrollGesture` with
     `gestureSourceType: 'touch'` and a hand-built `dispatchTouchEvent` drag BOTH scrolled a
     `touch-action: none` element (250px and 293px), so a control that must be able to report
     "blocked" cannot. No gate can cover this rule; it rests on the spec, on the drawer's
     precedent, and on that review measurement. Unverified on a device, like every touch rule
     here (ADR-0047 §3).
     REG-MOBILE-049's ground containment is NOT preserved by moving the rule to `::backdrop`, and
     that claim is RETRACTED (ADR-0050 §1, REG-OVERLAY-001): the pseudo-element generates no
     hit-test target, so the declaration was never consulted — see the `::backdrop` block below.
     What survives is `overscroll-behavior` on THIS element, effective because this is the scroll
     container: chaining out of this box is what `contain` stops. The drawer's division of labour
     does not transfer, because `.drawer-scrim` is a real, hit-testable element and `::backdrop`
     is not. */
  overscroll-behavior: contain;
}
.modal[open] { display: flex; }
.modal::backdrop {
  /* THE `var()` CARRIES A FALLBACK, and that is load-bearing rather than defensive (ADR-0050 §6).
     `--scrim` is declared on the root and nowhere in this rule, so it reaches the pseudo-element
     ONLY by inheritance from the originating element — a comparatively recent behaviour the
     `.modal-backdrop` ELEMENT this replaced never needed. Where it does not hold the declaration
     is invalid-at-computed-value-time, and the author declaration has already won the cascade, so
     the UA's own `rgba(0, 0, 0, 0.1)` is gone too. Measured with a deliberately unresolvable var
     in this exact rule shape: `{ withUnresolvableVar: "rgba(0, 0, 0, 0)", filterStillApplied:
     "blur(2px)", uaDefaultNoAuthorRule: "rgba(0, 0, 0, 0.1)" }` — a blurred but completely
     UNDIMMED ground, which is strictly worse than declaring nothing, and worse still beside the
     drawer's scrim (a real element, so its `var()` always resolves) still dimming correctly.

     NO `touch-action`, AND NO `overscroll-behavior` — retracted, not relocated (ADR-0050 §1,
     REG-OVERLAY-001). ADR-0049 moved the pan-not-zoom pair here on the reasoning that `::backdrop`
     is the ground. It is not a surface a gesture can land on: `::backdrop` generates NO hit-test
     target. Measured at 390px — `document.elementFromPoint()` in the backdrop region returns the
     `<dialog>`, and a real dispatched click there gives `event.target = DIALOG`;
     `getComputedStyle(dialog).touchAction` is `auto` while `getComputedStyle(dialog,
     '::backdrop').touchAction` reads `pinch-zoom`. `touch-action` resolves from the hit-tested
     element and its ancestors, and this pseudo-element is an ancestor of nothing, so the value was
     never consulted. (The same fact is why `@click.self` needs a rect comparison instead of
     `target === $el`.) `overscroll-behavior` fails for a second reason: `::backdrop` scrolls
     nothing, so it is not a scroll container.

     There is no third surface FOR A HIT TEST. The backdrop region and the modal's own box hit-test
     to the SAME element, so no `touch-action` can block a ground pan without also blocking the
     modal's scroll — which is the defect that moved the rule off `.modal` in the first place.
     `overscroll-behavior` is NOT bound by that argument, and ADR-0050 §1 retracted both halves on
     it (ADR-0051): that property resolves on the scroll container, not on the hit-tested element,
     and the ROOT scroller is reachable under the same condition the lock already computes. It is
     declared on `html` above. What holds here: the ground is `overflow: hidden`, the dialog is
     `position: fixed` in the top layer so a drag on it scrolls no document, `.modal` contains its
     own chain where it has one, and the root refuses to rubber-band. The residual iOS risk is
     stated in REG-OVERLAY-001 rather than papered over with a declaration a test can assert and a
     browser cannot read. */
  background: var(--scrim, rgba(0, 0, 0, 0.55)); backdrop-filter: blur(2px);
}
.modal h2 { margin: 0 0 2px; }
.modal .tabs { display: inline-flex; align-self: center; gap: 22px; margin-bottom: 12px; background: none; padding: 0; border-radius: 0; border-bottom: 1px solid var(--border); }
.modal .tabs button {
  background: transparent; color: var(--muted); border: 0; padding: 7px 16px;
  border-radius: 0; font-weight: 600; font-size: var(--t-meta);
}
.modal .tabs button:hover { color: var(--fg); transform: none; }
.modal .tabs button.active { color: var(--fg); background: none; border-bottom-color: var(--accent); }
.modal form { display: flex; flex-direction: column; gap: 9px; }
.modal input {
  background: var(--elevated); border: 1px solid var(--border-strong); color: var(--fg);
  border-radius: var(--radius-sm); padding: 10px 12px; font: inherit;
}
.modal input:focus { outline: none; border-color: var(--accent); }
.modal .oauth { margin-top: 6px; }
.modal .google {
  display: block; text-align: center; padding: 10px; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); color: var(--fg); text-decoration: none;
}
.modal .google:hover { background: var(--elevated); }
.modal .small { color: var(--muted); font-size: var(--t-sub); text-decoration: none; }

/* --bad-fg/--good-fg, not --error/--success: the two are identical in value, but only
   the -fg aliases are checked by TestDesignGuard_ContrastFloorOnEveryGround. Error text
   is the last place to run an unverified colour. */
.err { color: var(--bad-fg); font-size: var(--t-sub); }
.ok { color: var(--good-fg); font-size: var(--t-sub); }

/* ── Chips / tags ── */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.chip {
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 5px 10px;
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.04em;
  cursor: pointer; user-select: none; transition: all 150ms;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip input { margin-right: 6px; }
.row { display: flex; gap: 10px; }
.narrow { max-width: 560px; }
/* Statute prose: one measure, ruled sections, no boxes. `class="legal"` was a hook with
   nothing behind it, so /privacy and /terms rendered as unstyled body text. */
.legal { max-width: var(--measure); }
/* Ruled sections, shared by the two long-form pages that have them. /settings carries
   `class="narrow settings"` and its own `.settings h2` rule was dropped in the chrome
   sweep, which left its four sections running together with nothing between them — the
   global h2 has no rule. One selector list, because it is one idiom. (No `.settings h3`:
   the global h3 is byte-equivalent, and a rule that restates the cascade says nothing.) */
.legal h2,
.settings h2 { margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--border); }
.legal p, .legal li { font-size: var(--t-body); line-height: 1.7; color: var(--muted); }
.legal ul { padding-left: 20px; }
.legal li { margin: 4px 0; }

/* ── Forms (prediction pages) ── */
.narrow form { display: flex; flex-direction: column; gap: 12px; }
.narrow label { display: flex; flex-direction: column; gap: 5px; color: var(--muted); font-size: var(--t-meta); font-family: var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }
/* Ruled boxes, not rounded fills — the same reasoning as .search input: a form in a
   records office is ruled. --elevated is the inset fill ADR-0027 §2 assigns. */
.narrow input, .narrow select, .narrow textarea {
  background: var(--elevated); border: 1px solid var(--border-strong); color: var(--fg);
  border-radius: var(--radius-sm); padding: 9px 12px; font: inherit;
}
.narrow input:focus, .narrow select:focus, .narrow textarea:focus { outline: none; border-color: var(--accent); }

/* ── Badges ── */
.badges { display: flex; gap: 7px; margin-bottom: 10px; flex-wrap: wrap; }
/* One ruled-pill recipe, borrowed from .reg-pill so the chrome and the register cannot
   drift: mono, uppercase, tracked, ruled, unfilled. A badge is a LABEL (Role, Premium,
   Verified, a visibility), and outcome hue is carried by the border, not a wash.
   .status (the moderation report queue, below) deliberately keeps its fills — that is
   an operator surface where amber means "act on this", not a register standing. */
.badge {
  display: inline-block; font-family: var(--mono); font-size: var(--t-micro); font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; padding: 4px 8px;
  border-radius: var(--radius-sm); background: none; color: var(--muted);
  border: 1px solid var(--border-strong);
}
.badge.status-CORRECT { color: var(--good-fg); border-color: currentColor; }
.badge.status-INCORRECT { color: var(--bad-fg); border-color: currentColor; }
.badge.status-PENDING { color: var(--muted); }
.badge.status-EXPIRED { color: var(--warn-fg); border-color: currentColor; }
/* "Escalated" is a CASE state, not a prediction outcome. It borrowed status-INCORRECT,
   which made one vocabulary mean two things and would repaint if Miss ever changed. */
.badge.is-escalated { color: var(--warn-fg); border-color: currentColor; }
/* A generic utility that ALSO carries user text, which is why it breaks (REG-DESIGN-017,
   round four). It was exempt, on the reason "the sites themselves are listed separately" —
   and two of them are not: `<p class="meta">Email: <strong>{ d.Email }</strong></p>`
   (account.templ:11) and `<span class="meta"> — { flat(rep.Details) }</span>` (:161) reach
   this class as bare interpolations with no selector of their own, so nothing listed them and
   nothing could guard them. REG-DESIGN-020 already recorded an email local part as "a token
   nothing bounds at all" when it fixed `.acct-handle`. Declaring it here covers both sites and
   every future one; on ordinary chrome it never engages, because a token that fits is not
   broken.

   `break-word`, NOT `anywhere` — and the difference is not stylistic. Per CSS Text 3 the break
   opportunities `anywhere` introduces ARE counted in min-content intrinsic sizing and
   `break-word`'s are not, so `anywhere` on a class this broad dropped the automatic minimum of
   every `.meta` FLEX ITEM from "widest word" to about one character: `.comment-meta`,
   `.case-row` and `.audit-row` are flex rows holding dates, scores and counts, on 19 sites no
   browser step visits. Both sites that motivated this are BLOCK containers, where the two values
   render identically — so `break-word` buys the containment and pays nothing. */
.meta { color: var(--muted); font-size: 14px; overflow-wrap: break-word; }
.dash-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }

/* ── Tabs (dashboard/feed/explore) ── */
/* Register tabs (ADR-0027): mono, uppercase, tracked, with a ruled active state
   sitting on the strip's own baseline rule — the design's treatment, not a pill.
   The caps are a text-transform, so the labels stay "For you" / "Latest" /
   "Resolving" in the markup, the ADRs and the tests; only their setting changes. */
/* All four tab strips (feed, explore, dashboard, moderation) share this; the
   modal's strip is buttons inside `.modal .tabs` and overrides it. */
.tabs {
  display: flex;
  gap: 26px;
  align-items: flex-end;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  margin-bottom: 16px;
}
.modal .tabs { border-bottom: 0; margin-bottom: 12px; }
.tabs a,
.tabs button {
  background: none; border: 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-2);
  text-decoration: none;
  padding: 0 0 10px;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;          /* the active rule sits ON the strip rule, not under it */
  white-space: nowrap;
}
.tabs a:hover,
.tabs button:hover { color: var(--fg); }
.tabs a.active,
.tabs button.active { color: var(--fg); font-weight: 600; border-bottom-color: var(--accent); }
.pred-list { list-style: none; padding: 0; }
.pred-list li { display: flex; gap: 10px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--border); }
.pred-list li a { color: var(--fg); text-decoration: none; flex: 1; }
.pred-list li a:hover { color: var(--accent); }

/* ── Feed ── */
.feed-tabs { /* spacing + rule now come from .tabs */ }
.feed-tabs a { cursor: pointer; }
/* ── Tune-feed control in the tabs row (ADR-0037): the record's ONE tuning entry
   point — the topics bar is retired. The button sits inside .tabs, so this selector
   must out-rank the `.tabs button` tab-item reset (border:0, padding 0 0 10px, -1px
   margin) with the button's own geometry: gear + label + count, REG-DESIGN-015 gear
   geometry preserved. ── */
/* #tune-toolbar is the wrapper that makes the control an out-of-band swap target, so it
   carries the flex placement inside the tabs row. */
.feed-tabs #tune-toolbar { flex-shrink: 0; margin-left: auto; margin-bottom: 6px; align-self: center; display: inline-flex; }
.feed-tabs .tune-btn { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600; border: 1px solid var(--border-strong); background: none; color: var(--fg); border-radius: var(--radius-sm); padding: 5px 11px; white-space: nowrap; }
/* background:none on hover for the same reason as .mark:hover — the global
   `button:hover` fill outranks a single-class rule. */
.feed-tabs .tune-btn:hover { background: none; border-color: var(--accent); color: var(--accent); transform: none; }
.tune-count-badge { color: var(--accent); font-variant-numeric: tabular-nums; }
/* "· paused" is a state, not a count (ADR-0081 §4): muted ink, so it does not read as a tally.
   Its phone form (.tune-paused-short) is hidden here and shown by the ≤600px rule that also
   hides the label — the word alone wrapped the toolbar to two rows at 390px. */
.tune-count-badge.is-paused { color: var(--muted-2); }
.tune-paused-short { display: none; }
/* Transient zero-state line (ADR-0037 §1): renders only while nothing is tuned — the
   recover-from-empty invitation, not a management surface. */
.tune-zero { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-family: var(--mono); font-size: 11.5px; color: var(--muted); border-bottom: 1px solid var(--border); padding: 0 0 10px; margin: -6px 0 14px; }
.tune-zero .tz-glyph { color: var(--accent); }
.tune-zero.is-paused .tz-glyph { color: var(--muted-2); }
.tune-zero button { background: none; border: 0; padding: 0; cursor: pointer; font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.tune-zero button:hover { background: none; color: var(--accent-light); transform: none; }

/* ── Tune-feed manager (ADR-0037): ONE editor, two mounts — the feed modal and the
   /settings#interests section both render the same TuneFragment. ── */
/* The modal BODY scrolls (found in the ADR-0037 U-pass): "Your topics" is now the
   complete tuned set, so a heavily-tuned account (e.g. at the 50 personal-tag cap)
   makes the modal taller than the viewport — without this, All-topics is pushed off
   screen with no way to reach it. The head stays sticky so Done is always reachable. */
/* No `overflow` here: `.modal` above already declares `overflow: auto` for the same element at the
   same specificity, and restating it is a second copy of one decision (ADR-0050). Only the size
   overrides belong here. */
.tune-modal { width: min(480px, 94vw); max-height: 80vh; }
/* The sticky head bleeds into the modal's 26px padding and repaints that inset itself, so
   it covers the full width and the full top band rather than floating inside them.
   `top: -26px` is not a typo and not cosmetic: a sticky element is constrained by its
   MARGIN box, so the `-26px` top margin and a bare `top: 0` cancel out and pin the head
   26px BELOW the modal's inner edge — leaving a 26px band above it where scrolled rows
   show through (measured at 27px in the /chrome V11 step). Offsetting `top` by the same
   -26px lands the border box exactly on the padding-box edge, and the head's own 26px
   padding-top restores the inset the title needs. The negative bottom margin cancels
   `.modal`'s 12px flex gap so the opaque background, not a transparent band, is what rows
   scroll under. */
.tune-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; position: sticky; top: -26px; z-index: 1; background: var(--raised); margin: -26px -26px -12px; padding: 26px 26px 12px; }
.tune-modal-head h2 { margin: 0; }
.tune-close { cursor: pointer; border: 1px solid var(--border-strong); background: var(--accent); color: var(--accent-contrast); border-radius: var(--radius-sm); padding: 7px 16px; font-weight: 600; }
.tune-modal-hint { font-size: 13px; color: var(--muted); margin: 0; }
/* The live counter (N boosted · M muted) — aria-live, OOB-updated on every mutation. */
.tune-count { font-family: var(--mono); font-size: var(--t-micro); font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2); margin: 2px 0 10px; }
/* The spoken write-error line (cap / lock-timeout; the unknown-name line renders
   in-list instead). It is role="status" — a live region — and MUST stay in the
   accessibility tree even while empty: a region created (or un-`display:none`d) in the
   same frame its text arrives is treated by assistive tech as initial content rather
   than a change, and goes unannounced. That is the whole failure the previous
   `.tune-msg:empty { display: none }` caused — the sighted viewer saw "Personal-tag
   limit reached (50)" while a screen-reader viewer got a click that did nothing.
   An empty block with no margin is already zero-height, so nothing is gained by
   removing it from the box tree; the margin rides on :not(:empty) instead. */
.tune-msg { font-family: var(--mono); font-size: var(--t-micro); color: var(--warn-fg); margin: 0; }
.tune-msg:not(:empty) { margin: 8px 0 0; }
/* Both long lists carry their OWN scroll bound, not just the modal. `.tune-modal` caps
   the modal at 80vh, but /settings mounts TuneFragment bare — no `.tune-modal` wrapper
   at all — so there the height was unbounded, and `overflow-y: auto` without a height
   constraint does nothing. With the catalog browse deliberately unlimited and "Your
   topics" now the complete tuned set, a mature catalog plus a 50-tag account pushed the
   Sessions and Danger-zone sections (which sit BELOW Interests) arbitrarily far down the
   settings page. Bounding the lists fixes both mounts at once. */
#your-topics { max-height: 30vh; overflow-y: auto; }
.tune-list { max-height: 40vh; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.tune-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 4px; border-bottom: 1px solid var(--border); }
.tune-name { font-family: var(--mono); font-size: 12.5px; color: var(--fg); display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; }
/* The name yields to the control, never the other way round (ADR-0082 §6): three segments
   are the row's fixed cost, so a long name truncates and carries itself in title. */
.tune-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The three-segment control (ADR-0082 §1): one bordered pill with 1px dividers, so three
   labels cost ~150px rather than three bordered buttons with gaps. overflow:hidden clips
   the segment corners to the pill's radius, which is why the focus ring is drawn INSIDE
   the segment (outline-offset below) — outside it would be clipped away. */
.tune-seg { display: inline-flex; flex-shrink: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.tune-seg .mark { border: 0; border-radius: 0; border-left: 1px solid var(--border-strong); }
.tune-seg .mark:first-child { border-left: 0; }
.tune-seg .mark:focus-visible { outline-offset: -2px; }
/* The mark-pair (ADR-0037): CONSTANT labels, state as ink — filled = applied, click
   again to clear; aria-pressed carries the state the ink draws. Boost inks accent;
   mute inks border-strong (muting is a preference, not an error). transform:none
   cancels the global button:hover lift — nothing in the manager moves. */
.mark { cursor: pointer; font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; background: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--muted); padding: 3px 9px; }
/* `background: none` is load-bearing on the HOVER rule, not just the base one: the
   global `button:hover { background: var(--accent-light) }` outranks the single-class
   `.mark`, so without it an UNPRESSED mark fills solid on hover — reading as applied,
   and printing accent-on-accent-light (~1.3:1) where the word should be. The pre-ADR-0082
   `.tune-actions .chip` pair out-ranked it by accident of nesting; the segments now sit in
   `.tune-seg`, and `.mark` must say so itself. */
.mark:hover { background: none; border-color: var(--accent); color: var(--accent); transform: none; }
.mark.boost[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); }
.mark.boost[aria-pressed="true"]:hover { color: var(--accent-contrast); opacity: 0.9; }
.mark.mute[aria-pressed="true"] { background: var(--border-strong); border-color: var(--border-strong); color: var(--fg); }
.mark.mute[aria-pressed="true"]:hover { color: var(--fg); opacity: 0.9; }
/* Normal pressed is the quietest of the three inks — the standard state should not
   compete with a boost or a mute for attention, but it must still read as pressed. */
.mark.normal[aria-pressed="true"] { background: var(--border); border-color: var(--border); color: var(--fg); }
.mark.normal[aria-pressed="true"]:hover { color: var(--fg); opacity: 0.9; }
/* A pressed segment is inert (no hx-post — ADR-0082 §1): the cursor says so. */
.mark[aria-pressed="true"] { cursor: default; }
/* The strike goes on the NAME TEXT, not on the row's flex container. A container-level
   line-through propagates into every in-flow descendant and cannot be cancelled from
   inside — `text-decoration: none` on .pchip removes nothing, because the decoration
   belongs to the ancestor that declared it and is merely painted across the child. So a
   muted row struck its "Personal" marker too, which reads as "this marker no longer
   applies" about the one word saying the tag is private. */
.tune-row.is-muted .tune-name { color: var(--muted); }
.tune-row.is-muted .tune-name-text { text-decoration: line-through; text-decoration-thickness: 1px; }
/* PERSONAL marker (ADR-0025 privacy, ADR-0037 surface): only the viewer sees this tag. */
.pchip { font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2); border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); padding: 1px 5px; flex-shrink: 0; }
.tune-empty { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); line-height: 1.7; margin: 6px 4px 2px; }
/* Clear all's head line (ADR-0082 §3): right-aligned, link-shaped, the confirm inline. */
.tune-yt-head { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 6px; font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); padding: 2px 4px 6px; }
.tune-yt-head button.link { text-decoration: underline; text-underline-offset: 2px; }
/* The confirm's two halves toggle through the `hidden` attribute (review round two on #68):
   a synchronous binding, so the focus hand-off cannot outrun the reveal the way it can with
   x-show's next-frame show. The UA sheet's [hidden] rule is what hides them, and any later
   `display` on button.link would out-rank it silently — pinned here so it cannot. */
.tune-yt-head [hidden] { display: none !important; }
/* The topics pause (ADR-0081 §4): a native checkbox with role=switch and a status note. */
.tune-pause { display: flex; flex-direction: column; gap: 4px; margin: 0 0 10px; }
.tune-switch { display: inline-flex; align-items: center; gap: 10px; font-size: 13px; cursor: pointer; }
.tune-switch input { width: 20px; height: 20px; margin: 0; flex-shrink: 0; accent-color: var(--accent); }
.tune-pause-note { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); margin: 0; }
/* The retry control inside a .tune-empty line needs a STATIC affordance. button.link is
   `color: var(--muted); font: inherit` with no underline, and .tune-empty is also muted mono
   micro-type — so without this the only recovery control on the deferred Interests panel's
   failure path is typographically identical to the sentence around it, offering nothing but a
   hover colour to a viewer who is already looking at something broken. (It replaced an
   undefined class that fell through to the filled primary button: too loud, then invisible.) */
.tune-empty button.link { text-decoration: underline; text-underline-offset: 2px; }
/* Static suggestion chips (ADR-0037: the bar's rotator is gone — nothing moves). */
.tune-suggs { display: flex; flex-wrap: wrap; gap: 7px; padding: 4px 4px 2px; }
.sugg-chip { cursor: pointer; display: inline-flex; align-items: center; padding: 4px 9px; border-radius: var(--radius-sm); font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.04em; font-weight: 500; background: none; border: 1px dashed color-mix(in srgb, var(--accent) 50%, transparent); color: var(--accent); }
.sugg-chip:hover { background: var(--accent-soft); border-color: var(--accent); transform: none; }
/* Separation between the three manager sections. NOT `.tune-section + .tune-section`:
   "Suggested" renders inside the `#tune-suggs` OOB wrapper, so the three sections are not
   DOM siblings and that adjacency selector matched nothing — the rule was dead from the
   day the wrapper was added, and the sections have been touching since. A plain top margin
   fires wherever the section sits; the first one follows the message line, where 10px is
   wanted anyway. */
.tune-section { margin-top: 10px; }
.tune-section-label { display: block; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); padding: 6px 4px 2px; }
/* ADR-0025: the per-card "boost this tag into my feed" affordance next to a tag chip. */
.meta-tag-wrap { display: inline-flex; align-items: center; gap: 2px; }
.meta-tag-boost { cursor: pointer; border: 1px solid var(--border-strong); background: none; color: var(--muted-2); border-radius: var(--radius-sm); width: 18px; height: 18px; line-height: 1; font-size: 13px; font-weight: 700; padding: 0; display: inline-flex; align-items: center; justify-content: center; }
/* transform:none cancels the global button:hover lift — the register does not move
   under the pointer, and this is the only button living inside a row. */
.meta-tag-boost:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); transform: none; }
/* The mark's boosted state (ADR-0037 §4): ✓ inked accent; click again to clear. The
   settle-window ring is the swap feedback — the chip answers in place, the feed
   doesn't move. */
.meta-tag-boost.on { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); }
.meta-tag-boost.on:hover { background: var(--accent); color: var(--accent-contrast); opacity: 0.85; }
/* The two INERT card marks (ADR-0038). Neither is a button, because neither has a safe
   one-click action: boosting a muted tag would clear the mute, and nothing at all is safe
   on a state the server could not read. Both are still focusable (tabindex="0" in the
   template) — they are produced by swaps that destroy the focused element, so dropping
   them out of the tab order drops the keyboard viewer to <body> mid-register.
   The dashed border is the shared "reports, does not act" signal; the struck name matches
   how the manager draws a muted row, so the two surfaces say the same thing about the same
   tag. */
.meta-tag-wrap.is-muted .meta-tag { color: var(--muted-2); text-decoration: line-through; text-decoration-thickness: 1px; }
.meta-tag-muted,
.meta-tag-unknown { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); color: var(--muted-2); font-family: var(--mono); font-size: 13px; line-height: 1; }
/* The unknown mark is the only one that reports a FAULT rather than a preference, so it
   borrows the warning ink the manager's message line uses. */
.meta-tag-unknown { color: var(--warn-fg); border-color: var(--warn-fg); cursor: help; }
.meta-tag-wrap.htmx-settling .meta-tag-boost { box-shadow: 0 0 0 3px var(--accent-soft); }

/* A tag chip inside the register's meta line: ruled and squared like the rest of it.
   The old --bg fill was invisible anyway — the rows sit directly on --bg. */
/* REG-DESIGN-017 on the two tag surfaces, whose exemption reason was false (round five). It read
   "a tag is a bounded [a-z0-9_] token of at most 30 characters" — and neither half holds:
   `maxTagLen` is **50** (prediction/service.go), and `tagname.Normalize` only trims, strips a
   leading `#` and lowercases, so the charset is whatever migration 00019's canonical CHECK
   accepts. Fifty mono characters is ~330px at --t-micro and ~360px at --t-meta, against ~358px of
   content width on a 390px phone, and neither chip carried any containment at all. A 50-character
   token is not "bounded" for layout, so these break like every other user-text surface — and here
   `anywhere` is the right value rather than `break-word`, because a chip IS a flex item and
   lowering its automatic minimum is what lets it shrink instead of overflowing the row. */
.meta-tag { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); background: none; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 3px 7px; text-decoration: none; overflow-wrap: anywhere; }
.meta-tag:hover { color: var(--accent); }

.author { color: var(--accent); text-decoration: none; font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.author:hover { text-decoration: underline; }

/* avatar (generated initials, accent fill) */
.avatar {
  display: inline-flex; align-items: center; justify-content: center; border-radius: 50%;
  background: var(--accent); color: var(--accent-contrast); font-weight: 700; flex-shrink: 0; line-height: 1;
}
.avatar.sm { width: 26px; height: 26px; font-size: 12px; }
.avatar.md { width: 34px; height: 34px; font-size: 14px; }
.avatar.lg { width: 46px; height: 46px; font-size: 18px; }

/* ── The account seal and its menu (ADR-0034) ──
   The masthead's third mark. A register is ruled, so the seal is RULED AT REST and fills
   only when engaged: the sidebar CTA is the chrome's ONE accent fill and must stay the
   loudest thing in it. The circle is the seal — the second admitted circle in a ruled
   design. It reuses .avatar's radius for the disc, and declares a matching 50% of its
   OWN on the button box for one reason only: `outline` follows the focused element's
   border-radius, and the global :focus-visible ring is drawn on the BUTTON. Left at 0
   the keyboard ring was a square around this design's signature circle. That is why
   .acct-seal is on RuledNotRounded's allowlist (review round 2 of PR #35).

   The ring is --muted-2, NOT --border-strong. It is the only visual boundary of an
   interactive control, so WCAG 1.4.11 asks 3:1 of it, and --border-strong measures
   2.03:1 on night / 1.69:1 on paper against this bar's --surface — the same correction
   REG-DESIGN-013 already made for the register's progress track, where it read 1.79:1 on
   the lighter --bg. --muted-2 measures 5.46:1 / 4.77:1. NOTHING IN CI CHECKS NON-TEXT
   CONTRAST (ContrastFloorOnEveryGround walks text tokens only), so do not "restore" a
   fainter ring; it is held by measurement in /chrome (REG-DESIGN-018) and by this comment. */
.actions-rule { width: 1px; height: 20px; background: var(--border); }
.acct { position: relative; display: inline-flex; }
.acct-seal {
  background: none; border: 0; border-radius: 50%; padding: 0; cursor: pointer;
  display: inline-flex; line-height: 1;
}
/* The base `button:hover` fill has to be beaten on specificity, not just declared away:
   button:hover is (0,0,1,1) and .acct-seal alone is (0,0,1,0), so the plain rule above
   LOSES and the button box painted an --accent-light SQUARE behind the round seal. Caught
   in /chrome, invisible to httptest — httptest renders markup, not the cascade.

   Only `background` is reset. There is NO global button hover lift to cancel: `button:hover`
   sets background and nothing else, and no translate/scale appears anywhere in this file.
   (SEVENTEEN rules still carry a vestigial `transform: none` from a lift that predates
   the Registry redesign — .ghost, button.link, .modal .tabs button, .meta-tag-boost,
   .watch-btn, button.danger, the six .np-* controls, the four .pd-* controls and
   .phase-tab. This comment said "four" and named the first four until review round 2 of
   PR #35 counted them; an enumerated claim that is 4-of-17 discredits the paragraph it
   sits in. They are harmless no-ops and out of scope here; do not copy them into new
   rules.) */
.acct-seal:hover { background: none; }
.acct-seal .avatar {
  background: none; border: 1px solid var(--muted-2); color: var(--muted); font-weight: 600;
  transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}
.acct-seal:hover .avatar,
.acct-seal[aria-expanded="true"] .avatar {
  background: var(--accent); border-color: var(--accent); color: var(--accent-contrast);
}

/* The panel is the search dropdown's panel — same ground, rule, radius, shadow and (no)
   transition as .search-results, so the two things that drop from the masthead read as
   siblings. */
/* max-width is what makes .acct-email's ellipsis work AT ALL. The panel is absolutely
   positioned with no width, so it shrink-to-fits: without a cap it just grows to the
   address and the three ellipsis declarations below are inert. Measured — a 40-char
   address took the panel from 210px to 294px with the ellipsis never engaging. */
.acct-menu {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 40;
  min-width: 210px; max-width: 260px;
  background: var(--raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.acct-id {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 14px; border-bottom: 1px solid var(--border);
}
/* overflow-wrap, and it is REG-DESIGN-017 again: the handle is `@` + a 3-30 char
   [a-z0-9_] username — a token with ZERO soft-wrap opportunities in it — or, when no
   username is set, DisplayName()'s email local part, which nothing in the app bounds at
   all. Either one paints straight out of the 232px the capped panel leaves, exactly as
   an unbroken handle did out of the Explore people card. `min-width: 0` is NOT copied
   from the .person strong precedent: that rule needs it for a ROW-flex min-content
   floor, and .acct-id is column-flex, where the cross axis is already stretch-bounded.
   Round 1 of the PR-35 review capped the panel and ellipsised .acct-email but left the
   line above it unprotected; both no-username tests happened to use short values. */
.acct-handle { font-size: var(--t-sub); font-weight: 600; color: var(--fg); overflow-wrap: anywhere; }
.acct-email {
  font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct-links { display: flex; flex-direction: column; padding: 6px 0; }
.acct-links a,
.acct-out button {
  display: block; width: 100%; text-align: left; padding: 8px 14px;
  font: inherit; font-size: var(--t-sub); color: var(--fg); text-decoration: none;
}
.acct-out { border-top: 1px solid var(--border); padding: 6px 0; }
.acct-out button { background: none; border: 0; border-radius: 0; cursor: pointer; }
.acct-links a:hover,
.acct-out button:hover { background: var(--elevated); color: var(--accent); }

/* watch (bookmark icon + count) + comment count */
.watch { display: inline-flex; }
.watch-btn, .watch-stat, .comment-count {
  display: inline-flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 500; color: var(--muted);
}
.watch-btn { background: none; border: 0; padding: 0; cursor: pointer; border-radius: 0; transition: color 150ms; }
.watch-btn:hover { background: none; color: var(--accent); transform: none; }
.watch-btn.watching { color: var(--accent); }
.watch-stat { color: var(--muted-2); }
/* The count is an <a> now — the register's door to the discussion (issue #54, REG-FEED-050).
   It keeps the mark's muted colour rather than the link accent (the class rule above out-ranks
   the bare `a` rule) and takes the accent on hover, like the watch mark beside it. */
.comment-count { text-decoration: none; }
.comment-count:hover { color: var(--accent); }
.ico { width: 17px; height: 17px; flex-shrink: 0; }

/* time-elapsed progress bar */
.sentinel { height: 1px; }

/* ── Explore + discovery ── */
.sort-row { display: flex; gap: 10px; margin-bottom: 14px; }
.sort-row a { cursor: pointer; color: var(--muted); text-decoration: none; padding: 4px 0; }
.sort-row a.active { color: var(--accent); font-weight: 600; }
.tag-cloud { display: flex; flex-wrap: wrap; gap: 9px; }
.tag-chip {
  background: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 6px 11px; color: var(--fg); text-decoration: none; transition: all 150ms;
  font-family: var(--mono); font-size: var(--t-meta);
}
.tag-chip:hover { border-color: var(--accent); color: var(--accent); }
.tag-chip .count { color: var(--muted); font-size: 12px; }
.people-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; }
.person {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 16px; text-decoration: none; color: var(--fg); display: flex; flex-direction: column; gap: 4px;
  transition: border-color 180ms;
}
.person:hover { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }

/* ── Comments ── */
.comments { margin-top: 28px; border-top: 1px solid var(--border); padding-top: 18px; }
.comments-head { display: flex; justify-content: space-between; align-items: center; }
/* The hint is a FLEX CHILD of this row (ADR-0054 §6): without flex-wrap it
   rendered as a squeezed vertical column between the field and the button,
   shrinking the textarea. It wraps to its own line instead — the row stays
   [textarea][button], which is the layout that existed before the hint. */
.comment-form, .reply-form { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
.comment-form textarea, .reply-form textarea {
  flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--fg);
  border-radius: var(--radius-sm); padding: 10px 12px; font: inherit;
}
.comment-form textarea:focus, .reply-form textarea:focus { outline: none; border-color: var(--accent); }
.phase-head { display: flex; align-items: baseline; gap: 10px; margin: 12px 0 2px; }
/* Demoted from 15px bold: it is no longer competing with the claim. The <h3> and
   its aria-labelledby contract are unchanged. */
.phase-head h3 { font-size: var(--t-sub); font-weight: 600; margin: 0; }
/* .phase-state is gone: a phase window's state is a standing, so it renders as the
   register's .reg-pill (with .is-open for the live one) like every other standing
   in the app. */
.reg-pill [x-text] { font-weight: 500; }
.comment-list { margin-top: 2px; }
/* Drop the empty-state hint once a thread is inserted (htmx beforebegin keeps it). The
   child is a .thread now, not a .comment (ADR-0080 §1): written against .comment this rule
   stopped matching the moment the wrapper arrived, and "No comments yet." stayed visible over
   the first comment — the PF47 shape one level up. */
.comment-list:has(> .thread) > .hint { display: none; }
.comment { padding: 12px 0; border-bottom: 1px solid var(--border); }
.comment-meta { display: flex; gap: 8px; align-items: center; font-size: 13px; margin-bottom: 5px; }
.prose-plain { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); letter-spacing: 0.04em; }
/* Breaks mode for a moderator's written reason (ADR-0054 §5). The vote
   comment beside it is FLAT instead — it renders inside .vote-list, and no
   list surface keeps line structure. */
.mod-justification { white-space: pre-line; }
.comment-hint { flex: 0 0 100%; margin: 4px 0 0; font-size: var(--t-micro); color: var(--muted-2); }
/* The refusal slot (REG-COMMENTS-014). x-show keeps it out of the row until a 4xx paints it;
   shown, it takes the row's full basis like the hint above, so [textarea][button] stays a row. */
.comment-form > .err, .reply-form > .err { flex: 0 0 100%; margin: 0; }
/* Rich mode's host (ADR-0053 §1). The margin stays on the container; .prose's
   first/last-child resets keep the markdown's own block margins from adding a
   second gap at either end. */
.comment-body { margin: 5px 0; line-height: 1.55; }
.comment-actions { display: flex; gap: 14px; align-items: center; }
.votes { display: inline-flex; gap: 6px; align-items: center; }
.score { min-width: 1.5em; text-align: center; color: var(--muted); font-weight: 600; }
.removed { color: var(--muted); font-style: italic; }

/* ── Bell + profile + notifications ── */
.bell { display: inline-flex; align-items: center; text-decoration: none; color: var(--muted); }
/* The badge's anchor is the INK, not the link: the phone floor grows `.bell` to 44×44
   and an unread count pinned to that box floats off the glyph it is counting for. */
.bell-ink { position: relative; display: inline-flex; align-items: center; }
.bell:hover { color: var(--fg); }
.bell .ico { width: 18px; height: 18px; }
.bell-badge {
  position: absolute; top: -6px; right: -8px; background: var(--accent); color: var(--accent-contrast);
  border-radius: var(--radius-sm); font-family: var(--mono); font-size: var(--t-micro); font-weight: 700; padding: 0 5px; min-width: 16px; text-align: center;
}
.profile-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
.stats { display: flex; gap: 18px; color: var(--muted); margin: 14px 0 22px; }
.follow button { padding: 9px 20px; border-radius: var(--radius-sm); }
/* A ruled register of events rather than a stack of cards. Unread is marked by a rule
   in the margin, not a fill. */
.notif {
  display: flex; flex-direction: column; gap: 3px; padding: 13px 12px 13px 0;
  border: 0; border-bottom: 1px solid var(--border); border-radius: 0; margin: 0;
  text-decoration: none; color: var(--fg); transition: background-color 150ms ease;
}
.notif:hover { background: var(--elevated); }
.notif:last-of-type { border-bottom: 0; }
.notif.unread { border-left: 2px solid var(--accent); padding-left: 11px; background: none; }
.notif span { color: var(--muted); font-size: var(--t-sub); }

/* ── Moderation ── */
/* A moderation notice is an annotation on the record, so it is ruled in the margin
   rather than filled: the register's one fill is the accent CTA. */
.mod-notice {
  background: none; border: 0; border-left: 3px solid var(--bad-fg);
  color: var(--bad-fg); border-radius: 0; padding: 2px 0 2px 13px; margin: 14px 0;
  font-size: var(--t-sub);
}
.content-mod-actions { display: flex; gap: 16px; align-items: center; margin: 14px 0; padding-top: 14px; border-top: 1px solid var(--border); }
.report-form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
.report-form select, .report-form input {
  background: var(--elevated); border: 1px solid var(--border-strong); color: var(--fg);
  border-radius: var(--radius-sm); padding: 7px 9px; font: inherit;
}
.report.ok, .nominate.ok { color: var(--good-fg); font-size: var(--t-sub); }
.moderation .tabs { margin-bottom: 18px; }
.case-list { list-style: none; padding: 0; }
.case-row { display: flex; gap: 10px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.case-link { flex: 1; color: var(--fg); text-decoration: none; min-width: 200px; }
.case-link:hover .case-preview { color: var(--accent); }
/* REG-DESIGN-017 again, and this time the review found it by reading the CLAIM rather than the
   CSS: the round that fixed five surfaces asserted it had fixed "every user-text surface" and had
   not. `.case-preview` is the moderation queue's preview of reported content — the one surface in
   the app whose text is, by definition, something a stranger wrote and somebody objected to — and
   it carried no `overflow` and no break opportunity at all. It sits in `.case-link { flex: 1;
   min-width: 200px }`, and `min-width` REPLACES the flex automatic minimum, so the item cannot
   shrink: one 67-character URL inks straight out of the 200px item and paints over the badges
   beside it. The universal claim is now a derived guard rather than a list (see
   TestProseGuard_UserTextBreaksUnbreakableTokens). */
.case-preview { display: block; overflow-wrap: anywhere; }
/* THE BLOCK PADDING IS LOAD-BEARING, and it did not look it (issue #51, review round four).
   This is the ONLY way `.content-preview` renders — every site (moderation.templ:175/182/184/186)
   is inside `class="moderation case-detail"` — so the Archivo-at-1.6 preview the round-3 decline
   was measured against does not exist in the product. What renders is Spectral at `line-height:
   1.5` against a 1.5208em content area: the same negative half-leading as `.pd-statement`, in the
   same `overflow-y: auto` scroll container, ~0.156px per edge at 15px. The 12px here absorbs it
   77x over — but nothing SAID so, and `padding: 0 14px` is a pure spacing edit that would reopen
   the issue on the one surface in the app that displays reported content. The value is pinned by
   TestProseGuard_TheFixIsTheMeasuredConstant; keep at least ~1px of block padding, or give this
   box the claim's `padding-block`/`margin` pair instead. */
.case-detail .content-preview {
  background: var(--elevated); border: 0; border-left: 3px solid var(--border-strong); border-radius: 0;
  padding: 12px 14px; margin: 14px 0; color: var(--fg);
  font-family: var(--serif); font-size: var(--t-body); line-height: 1.5;
}
.vote-summary { margin: 14px 0; font-size: var(--t-sub); }
.vote-box { margin: 18px 0; padding: 16px 0; border-top: 1px solid var(--border); }
.vote-list, .audit-list { list-style: none; padding: 0; }
.vote-list li { padding: 7px 0; }
.audit-row { display: flex; gap: 10px; align-items: baseline; padding: 9px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
/* The audit JSON carries the moderator's own justification (up to 1 000 runes),
   so it is user-authored text and gets the isolation every other such surface
   has (ADR-0061 §9). It reached this element as a BARE interpolation, which is
   structurally invisible to the guard that derives its list from flat()/breaks()/
   @Rich( call sites — a blind spot worth naming, since ADR-0058 §1 deliberately
   preserves bidi characters at the write boundary and encoding/json does not
   escape U+202E. */
.audit-data { color: var(--muted); font-family: var(--mono); font-size: var(--t-meta); overflow-wrap: anywhere; min-width: 0; unicode-bidi: isolate; }

/* ── Reduced motion: honor the OS setting (WCAG 2.3.3 / vestibular safety). ── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Mobile (≤768px): the rail is a DRAWER, not a strip (ADR-0042 §1).
   It used to lie down as a horizontal, scrollable nav strip under the top bar.
   That fixed the sliver it was written for and hid the app: measured at 390px
   with a five-destination account, the strip held 657px of links in a 390px box
   — Dashboard, Watchlist, People and the New-prediction CTA all off-screen,
   343px of them, behind a swipe iOS paints no scrollbar to invite. The brand
   block alone took 190px of the 390. So the rail leaves the grid entirely and
   is drawn OVER the record on demand, keeping its own markup: same nav, same
   links, same aria-current. ── */
@media (max-width: 768px) {
  /* One arm, not two. The `.app-shell.sidebar-collapsed` twin that used to sit here was
     overriding a rule that no longer reaches this width at all — the collapsed grid is
     inside `@media (min-width: 768.02px)` now (ADR-0043 §1, ADR-0044). Every such twin is a phone rule
     whose only job is to undo a desktop one, which is the pattern that hid the Critical. */
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: 64px 1fr;
    grid-template-areas: "topbar" "main";
  }
  /* Off-canvas at rest. ADR-0042 shipped this WITHOUT `visibility: hidden` for a measured
     reason (MB3, in a real rendering browser rather than the MCP tab, which never paints):
     with visibility on the transition the drawer's computed value was still `hidden` a
     macrotask after the class landed and only flipped ~220ms later, so the shell's focus()
     call hit an element that could not take focus and focus stayed on <body>. The drawer
     opened with the topbar and the record inert behind it and a keyboard had nowhere to go.
     Splitting the transition into an asymmetric pair did not fix it — until ADR-0043 §3
     found the asymmetry that does, which is why `visibility` is declared below after all.
     (This paragraph claimed the omission was permanent for two rounds after the rule beneath
     it contradicted that; corrected in ADR-0046.)

     What keeps a closed drawer out of the tab order and the a11y tree is `inert`,
     bound on the aside itself — an ATTRIBUTE, applied in the same synchronous flush as
     the class and subject to no transition semantics whatsoever. It is the same
     primitive the topbar and the record use while the drawer is open, which leaves the
     shell with one rule: exactly one region is interactive at a time.

     `visibility` is back BESIDE it (ADR-0043 §3), asymmetrically transitioned, and the
     asymmetry is the whole of it: `0s` on open flips the drawer visible in the frame the
     class lands, so the focus call still works; `0s 220ms` on close holds it visible for
     the length of the slide-out. inert is the semantic mechanism; this is what holds in
     the window before deferred Alpine boots, and if it never does. */
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(280px, 84vw);
    z-index: 60;
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 220ms ease, visibility 0s 220ms;
    overflow-y: auto;
    /* The drawer scrolls its own seven links, so a swipe that reaches its scroll end
       chains to the record behind the scrim and the reader loses their place in the feed
       while a modal drawer is open. `contain` stops the chain at this box. Same caveat as
       the scrim's `touch-action`: standard containment, not measured here. */
    overscroll-behavior: contain;
  }
  .sidebar.drawer-open {
    transform: none;
    visibility: visible;
    transition: transform 220ms ease, visibility 0s;
  }
  /* The drawer is a column flex box the height of the viewport, so its masthead is a
     shrinkable item — and the phone rule that used to say otherwise went out with the
     nav strip. Measured at 667×333 (a landscape phone, still inside the drawer's band):
     `.brand` used 48px against its declared 64px and the double rule crowded the
     wordmark. `overflow-y: auto` above scrolls the nav; the masthead should not pay for
     it (REG-MOBILE-028). */
  .brand { flex-shrink: 0; }
  /* `pinch-zoom`, NOT `none` (ADR-0046 §6). The scrim's job is to stop the document PANNING
     behind the drawer on iOS Safari, where `overflow: hidden` on the ground does not reliably
     hold. `touch-action: none` also disables user zoom, and this scrim is `fixed; inset: 0` —
     so with the drawer open the whole viewport outside the 280px panel refused to zoom, which
     is `user-scalable=no` reintroduced per-state. The drawer's own destinations are 11px
     tracked uppercase mono: exactly the text a low-vision reader would pinch, on the only
     route a phone has to every destination (WCAG 2.1 SC 1.4.4). `pinch-zoom` blocks the pan
     and keeps the zoom. Still UNVERIFIED on a device — a desktop rig cannot observe either. */
  .drawer-scrim { position: fixed; inset: 0; background: var(--scrim); z-index: 55; touch-action: pinch-zoom; }
  .topbar .drawer-toggle { display: inline-flex; }
  /* The rail's own control is a desktop affordance — a drawer has no collapsed
     state to announce, and the control would be sealed inside it when shut. Qualified like
     the other two phone-only controls (ADR-0046 §7): at one class this sits 25 lines BEFORE
     `.icon-btn, .bell { … }` in this same block, so a `display` added to that rule — which is
     the natural place to add one — would re-expose the rail's collapse toggle inside the
     drawer, a control whose onclick writes the year-long `sidebar` cookie. That is exactly
     the two-controls-one-region state TestDesignGuard_PhoneOnlyControlsOutrankIconBtn exists
     to prevent, and it protected the other two members of the set and not this one. */
  .sidebar .sidebar-toggle { display: none; }
  /* Nothing restores the masthead, the nav labels or the footer here any more, and
     that is the point of ADR-0043 §1: the rules that used to hide them are scoped to
     the desktop, so a phone never sees them and has nothing to undo. */
  .sidebar nav { padding: 8px 0; }
  /* The band carries controls only, left to right, with the account marks pushed
     to the far edge; `position: relative` anchors the expanded search row below it. */
  .topbar { position: relative; justify-content: flex-start; gap: 4px; padding: 0 12px; }
  /* 16px is the iOS auto-zoom floor, not a type-scale choice — and it lives on the TOUCH
     band's axis, ≤768px, not the narrow-screen one (ADR-0047 §4; the same axis rule as the
     touch floors, ADR-0043 §2). Scoped to ≤600px it left the 601–768 band — a landscape
     iPhone, exactly the device the floor exists for — tapping a 12.5px inline field that
     Safari zooms into and never zooms back out of. `.modal input` and `.stack input` use
     `font: inherit` and were never exposed. Unverified on a device: desktop rigs do not
     auto-zoom, which is why three rounds of measurement missed it. */
  .search input { font-size: 16px; }
  .actions { margin-left: auto; }

  /* ══ TOUCH FLOORS — at the SAME breakpoint as the shell they belong to (ADR-0043 §2).
     These lived at ≤600px while the drawer turned on here, which left a 601–768px band
     — a small tablet, a split-screen phone, a narrowed desktop window — carrying the new
     touch shell and none of its ergonomics. Measured at 700px before this move: the
     burger this change introduced was 36×32, the bell 18×18, the watch mark 38×21 and
     the gutters still 32px — the three numbers ADR-0042 opens by calling WCAG failures.
     A control the drawer introduces may not be under the floor at any width the drawer
     exists at. What stays at ≤600px is only what is about a NARROW screen rather than a
     touch one: the search collapse, the tune label, the gutters. ══ */
  /* The bell grows its own box like the other band controls. What makes that safe is
     `.bell-ink` (layout.templ), which the unread badge is anchored to instead of this
     box — without it the count floated off into the gap beside the glyph. An inset
     ::after was tried first and is worse than either: at 44px it reached over the theme
     toggle and the seal, and two of four corner probes on EACH of them hit the bell. A
     hit area that overlaps its neighbours is not a bigger target, it is a wrong one. */
  .icon-btn,
  .bell { min-width: 44px; min-height: 44px; justify-content: center; }
  /* The seal is DRAWN — a 26px disc with a 50% radius the design guard asserts — so
     its area is added AROUND the ink with an inset pseudo-element. Padding would have
     grown the circle itself, and the focus ring follows that radius (ADR-0034 §3). */
  .acct-seal { position: relative; }
  .acct-seal::after { content: ''; position: absolute; inset: -9px; }
  /* ── Area without ink. The watch mark is a 38×21 hit rect with a two-digit count — the
     most tapped control in the record and under the 24×24 floor. The glyph and the count
     must not move, so the area is an inset pseudo-element. The horizontal inset was 6px
     "so it cannot reach the count beside it", a reason that stopped being true when the
     count moved INSIDE the button (watchCount is the button's own child): a ONE-digit
     mark is 29×21 (17px glyph + 5px gap + one digit), 41px under a 6px inset, and scored
     0/4 at the 44px floor on the dossier at 390px (issue #52, PF125) — on every register
     row with a one-digit count too, which MB9 never probed because its row had two. 10px
     makes the narrowest mark 49×45 and the two-digit one 58×45. Nothing interactive sits
     within 10px of it on any surface: the register stacks its standing column, and the
     dossier's pill 8px to its left is not a control. ── */
  .watch-btn { position: relative; }
  .watch-btn::after { content: ''; position: absolute; inset: -12px -10px; }
  /* ── The register's comment count is a link now (issue #54). Its ink is ~29×21 — a 17px
     glyph, a 5px gap and one digit — under the 24px floor the register's in-row marks clear
     (ADR-0042 §6: NOT raised to 44; the row is a dense document and a 44px mark beside every
     claim would read as a button bar). Area without ink, like the watch mark: an inset
     ::after, so the glyph and the count do not move; -4/-6 makes the narrowest ~41×29, and
     the meta row's 14px gap leaves 8px to the next chip. ── */
  .comment-count { position: relative; }
  .comment-count::after { content: ''; position: absolute; inset: -4px -6px; }
  /* Scoped to #main: `.tabs` is also the auth modal's Sign in / Sign up strip, and
     stretching a modal's tabs to 44px flex items repositioned its active underline on
     a surface nothing in this change was reasoning about. Every in-record strip —
     feed, explore, dashboard, moderation — is inside #main and does want the floor.
     `stretch` makes the items share the tallest height and each centre its own label,
     so the active rule stays welded to the strip's bottom border (via the -1px margin
     it already carries) instead of drifting below a top-aligned word.

     The CHILD combinator is load-bearing (ADR-0044 §3). Written as a descendant pair,
     `#main .tabs button` (1,1,1) out-ranked `.feed-tabs .tune-btn` (0,2,0) and painted
     `padding: 0 0 2px` onto the Tune control, which is not a tab item at all — it is a
     button nested in `#tune-toolbar` INSIDE the strip. Measured: 0px horizontal padding
     at 390px and 700px, a 47px pill with the gear against its left border and the count
     against its right, while the two rules meant to size it (line 613 and the ≤600px
     override below) were both dead. A floor for a strip's ITEMS addresses its children;
     anything else it reaches is a control it was never reasoning about. */
  #main .tabs { align-items: stretch; }
  #main .tabs > a,
  #main .tabs > button {
    min-height: 44px; display: flex; align-items: center; padding: 0 0 2px;
  }
  /* The live source of the Tune control's floor now that the strip rule cannot reach it. */
  .feed-tabs .tune-btn { min-height: 44px; }
  /* ── The register's own marks, at the SAME breakpoint as everything else a finger has
     to hit (REG-MOBILE-023). These sat at ≤600px while the shell around them turned
     touch at ≤768px, so the 601–768px band — a small tablet, a split-screen phone, a
     narrowed window — carried 44px band controls beside 18×18 card marks. Measured at
     700px before this move: `.meta-tag-boost` 18×18 (0/4 on a strict 24px probe) against
     28×28 at 500px, and `.meta-tag` 62×26 against 70×32.

     The DENSITY decision is untouched: in-row marks clear 24×24 and are deliberately NOT
     raised to 44 (ADR-0042 §6) — the register is a dense document and a 44px dot beside
     every tag would read as a button bar. Only the band they apply to is corrected. ── */
  /* The per-card boost mark is an 18px dot on desktop — too small to tap; enlarge it. */
  .meta-tag { padding: 6px 11px; }
  .meta-tag-boost { width: 28px; height: 28px; font-size: 15px; }
  /* Roomier Boost / Mute mark targets in the Tune-feed manager. */
  .mark { padding: 8px 14px; }
  /* The three-segment control is the manager's PRIMARY control, not the register's dense
     in-row dot (ADR-0042 §6), so it takes the full 44px floor here (ADR-0082 §6,
     REG-FEED-061) — as do Clear all's link-shaped controls and the pause switch's label. */
  .tune-seg .mark { min-height: 44px; padding: 8px 12px; }
  .tune-yt-head button.link { min-height: 44px; }
  .tune-switch { min-height: 44px; }
  /* The suggestion chip is a real hx-post button. It used to be `.chip chip-suggest`
     inside `.feed-tunebar`, so the bar's deleted mobile rule enlarged it; the rename to
     `.sugg-chip` (ADR-0037) left it behind at ~23px, under the floor every other mark
     here was fixed to. Font-size stays ≥11px for TestDesignGuard_TypeFloor. */
  .sugg-chip { padding: 8px 13px; font-size: 13px; }

  /* The muted marker is the card mark's third state and the same 18px dot on desktop, so
     it needs the same enlargement — otherwise it also misaligns beside the 28px boost
     mark on a row carrying both a muted and an untuned tag. */
  .meta-tag-muted,
  .meta-tag-unknown { width: 28px; height: 28px; font-size: 15px; }
  /* The Explore / watchlist filters are tap targets too, and the base .chip padding
     leaves them ~26px — under the size every other chip in here was fixed to. */
  .filter-group .chip { padding: 9px 12px; }
  /* The drawer's rows are 11px mono, so the desktop rail's 8px padding leaves a ~34px
     row. Pad it back over 44px. */
  .sidebar nav a, .sidebar nav .nav-cta { padding: 14px 12px; }
  /* Privacy · Terms are 46×13 and 33×13 as inline text — under the same 24×24 floor
     this change exists to establish, on links the pre-drawer phone shell hid entirely
     and the drawer now exposes. The drawer is their only route on a phone. */
  .sidebar-foot a { display: inline-block; padding: 9px 4px; }
}

/* ── Topics / Tune UI on phones (ADR-0025 mobile polish; ADR-0037 slimmed it — the
   bar and its stacking rules are gone, so only the touch targets remain).
   ADR-0042 extended this block into the whole phone pass: the band, the toolbar,
   the gutters, and a floor under every target a finger has to hit. ── */
@media (max-width: 600px) {
  /* ── The band (ADR-0042 §4). The search input took 223px of a 390px masthead and
     still clipped its own placeholder mid-word; collapsed to a magnifier it leaves
     room for the burger. `.search` drops out of flow into a full-width row under the
     band — it resolves against `.topbar`, which the ≤768px block made the containing
     block, NOT against the wrapper, which is `position: static` here. Its own
     `position: absolute` keeps it a containing block for `.search-dd`, so the results
     panel still hangs off the input it belongs to. ── */
  /* `position: static` HERE, though the base rule makes the wrapper `relative` so Alpine's
     outside handler has a box to measure: the expanded row is `position: absolute` and must
     resolve against `.topbar` (the ≤768px block makes it the containing block) to span the
     full band. Anchored to the wrapper it measured 169px wide at x=60 — a search row the
     width of the magnifier it dropped out of. The wrapper also sheds the desktop's
     `flex: 1; max-width: 380px`: with the input out of flow there is nothing left to size. */
  .search-wrap { position: static; flex: 0 0 auto; max-width: none; }
  /* `.topbar`-qualified for the same reason as the base hide it overrides: at one class
     each, only source order kept the magnifier off the desktop masthead (ADR-0045). This
     is also the ONE breakpoint the row's state may read — `narrow` in layout.templ is
     this query, not the drawer's 768px `phone` (REG-MOBILE-030). */
  .topbar .search-toggle { display: inline-flex; }
  /* The results scroll INSIDE the dropdown. ADR-0045 gated the scroll dismisser to this band
     and left the defect it names on the band it kept: `.search-dd` had no max-height and no
     overflow, so a result list longer than the fold could only be reached by a page scroll —
     which crosses the 40px threshold and destroys the row. `overscroll-behavior: contain`
     is what keeps the fix from re-creating the bug: without it, scrolling past the end of the
     list chains to the document and dismisses the row anyway. */
  .search-dd { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
  .search {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 45;
    max-width: none; display: none;
    padding: 10px 16px; background: var(--surface);
    border-bottom: 1px solid var(--border-strong);
  }
  .search-wrap.is-open .search { display: block; }
  /* The row is dismissed by tapping away from it, and without a surface to absorb that
     tap the same click also activates whatever it lands on — so closing the search
     opened a prediction. The drawer has a scrim for exactly this reason; this is the
     search's (ADR-0043 §6). It sits under the row (z 45) and over the record.

     ABSOLUTE, in the row's own containing block (ADR-0044 / REG-MOBILE-022). Pinned to
     the viewport with `position: fixed` it outlived the thing it belongs to: `.topbar`
     is `position: relative` here and never sticky, so scrolling took the band, the row
     and the magnifier off-screen and left a transparent full-viewport scrim over the
     record. Measured at scrollY 500: `elementFromPoint` mid-screen returned the scrim,
     a tap on a card did not navigate, and the focus hand-back snapped scroll to 0. It
     now travels with the row exactly as `.search-dd` does. */
  /* FIXED, third shape in three rounds — and this time the geometry is argued, not patched
     (ADR-0047 §2). The history: `fixed; 100vh` (ADR-0043) outlived the row on scroll — the
     original tap-eater. `absolute; top: 100%; 100vh` (ADR-0044) overhung the viewport by the
     band height and added 63px of scrollable overflow to every page. `calc(100dvh - 100%)`
     (ADR-0046) removed the overhang and UNCOVERED a scrollY-sized strip at the viewport
     bottom — measured at scrollY 30 with the row open (a state the 40px threshold exists to
     allow): scrim bottom 814 against an 844 viewport, elementFromPoint in the strip a record
     element, so the dismissing tap also navigated (the ADR-0043 §6 tap-through, back through
     a third gap). The trade is geometric: an absolute box's document extent is fixed at paint,
     but the strip it must cover is viewport-anchored — any height either overhangs at scroll 0
     or uncovers when scrolled.

     What makes `fixed` CORRECT now, when ADR-0043 proved it wrong, is the dismisser ADR-0044
     added: the row — and this scrim with it — closes at 40px of scroll, so a viewport-anchored
     scrim can go stale by at most that. In the surviving window the seam above (the band
     scrolled partly off) is covered by the row itself, which is taller than the maximum
     surviving scroll. And fixed boxes are excluded from scrollable-overflow computation, so
     round 4's finding cannot recur; no dvh, so no fallback problem either. `top: 64px` is the
     band height the shell's grid fixes and MB1 asserts. */
  .search-scrim { position: fixed; top: 64px; left: 0; right: 0; bottom: 0; z-index: 44; }
  /* ── The feed toolbar on ONE row (ADR-0042 §5). 190px of tabs plus a 151px labelled
     button does not fit a 326px measure, so the button wrapped and spent a 90px band
     on itself. Gear + count is ~62px. The name is not lost: the button's aria-label
     carries it in full, which is why the label had to become a span first. The button's
     44px floor lives in the ≤768px block with the other touch floors. ── */
  .feed-tabs .tune-label { display: none; }
  /* The paused badge follows the label (ADR-0081 §4): the word wrapped the strip to two rows
     at 390px (band 119px, control 112px wide), so here the glyph stands in for it. */
  .feed-tabs .tune-paused-long { display: none; }
  .feed-tabs .tune-paused-short { display: inline; }
  .feed-tabs .tune-btn { padding: 8px 12px; font-size: 12px; }
  /* 32px gutters were a desktop measure applied to a 390px screen — a fifth of the
     width spent on margin. 16px returns 32px to the claim (+10%). This one is about a
     NARROW screen, not a touch one, so it stays at 600px while the touch floors sit
     with the drawer at 768px (ADR-0043 §2). */
  #main { padding: 18px 16px; }
}

/* ── The narrowest band (REG-MOBILE-024). ADR-0042 added a 44px burger and a 44px
   magnifier to the masthead and, in the same pass, made `.search-wrap` `flex: 0 0 auto`
   — which removed the band's only compressible item. Signed IN that is fine (theme,
   bell and seal fit at 320px). Signed OUT the squeeze lands on the two auth buttons,
   the only conversion controls on the landing page: measured at 320px they wrapped
   mid-phrase to 67×68 and 69×66 at y = −2 and −1, standing taller than the 64px band
   and spilling above and below its rule. Correct from 350px up, so this block is the
   floor and nothing above it changes.

   The child combinator picks out exactly the two auth buttons: the theme toggle is an
   `.icon-btn` and the account seal is nested inside `.acct`, so neither is touched — and
   the band's gutters and gaps are deliberately NOT touched either. Tightening those was
   tried first and cost a 1px horizontal scroll on the signed-in band at 320px; the two
   buttons give back 32px on their own (81→65 and 84→68), which is more than the fit
   needed. The smallest edit that closes it is the one that stays. ── */
@media (max-width: 360px) {
  .actions > button:not(.icon-btn) { padding: 9px 8px; white-space: nowrap; }
}

.stack { display: flex; flex-direction: column; gap: 10px; max-width: 420px; margin: 10px 0; }
.stack label { display: flex; flex-direction: column; gap: 4px; font-size: 14px; color: var(--muted); }
.stack input, .stack textarea {
  background: var(--surface); border: 1px solid var(--border); color: var(--fg);
  border-radius: var(--radius-sm); padding: 9px 12px; font: inherit;
}
.stack input:focus, .stack textarea:focus { outline: none; border-color: var(--accent); }
.row { display: flex; align-items: center; gap: 12px; }
.form-msg { font-size: 13px; color: var(--muted); }
.form-msg .err { color: var(--bad-fg); }
.form-msg .ok { color: var(--good-fg); }
.danger-title { color: var(--bad-fg); }
button.danger { background: none; color: var(--bad-fg); border: 1px solid var(--bad-fg); border-radius: var(--radius-sm); font-weight: 600; }
button.danger:hover { background: var(--bad-bg); transform: none; }
.report-list { list-style: none; padding: 0; margin: 14px 0; }
.report-row { padding: 12px 0; border-bottom: 1px solid var(--border); }
.report-head { display: flex; gap: 8px; align-items: center; font-size: 13px; }
.report-reason { margin: 6px 0 0; }
/* Qualified to `.status` on purpose. These are the My-Reports list's own colours,
   but `status-<STATUS>` is a modifier several components compose onto themselves —
   the register's standing pill emits `reg-pill status-PENDING`, and while unqualified
   these rules painted a warn-amber fill behind every pending row on every surface.
   A modifier class is never a selector on its own. */
.status { font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; border-radius: var(--radius-sm); padding: 3px 8px; background: var(--surface); color: var(--muted); }
.status.status-PENDING { background: var(--warn-bg); color: var(--warn-fg); }
.status.status-UNDER_REVIEW { background: var(--accent-soft); color: var(--accent); }
.status.status-DISMISSED { background: var(--surface); color: var(--muted); }

/* ── Explore filters + load-more ── */
/* ── Explore / watchlist filters ── ruled controls, not lozenges. border-radius:999px
   was the last pill on this surface, sitting directly under .explore-search, which is
   already a ruled mono line. The base .chip rule supplies mono / square / padding, so
   these override only what a FILTER differs in: a tracked uppercase label, the
   lighter rule, and an INKED active state — an accent rule under the cell rather than
   a tinted wash, so the state does not depend on hue alone. flex-wrap on the group is
   needed because uppercasing and tracking lengthens the labels ("RECENTLY WATCHED" is
   the worst case) and the group must break rather than overflow. */
.explore-filters { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 12px 0 18px; }
.filter-group { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.filter-group .chip {
  font-size: var(--t-meta); letter-spacing: 0.1em; text-transform: uppercase;
  border-color: var(--border); color: var(--muted); text-decoration: none; white-space: nowrap;
}
.filter-group .chip:hover { color: var(--fg); border-color: var(--border-strong); background: var(--elevated); }
.filter-group .chip.active {
  color: var(--fg); font-weight: 600; background: var(--elevated);
  border-color: var(--border-strong); box-shadow: inset 0 -2px 0 var(--accent);
}
.load-more { display: block; margin: 18px auto; }

/* ── Richer following cards ── */
.person-rich { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
/* wrap, or the badges overflow: flex children floor at min-content, so a nowrap row
   (avatar + name + ✓ + PREMIUM) pushed past the card edge at the grid's 190px
   minimum — same lesson .pd-head/.pd-standing already carry. */
.person-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* .person strong, not .person-head strong: the name is an unbroken 3-30 char token
   and BOTH People cards set it in a min-content-floored flex box — the following
   card's head row here, and the Explore people card's bare <strong> (a column-flex
   item in the same 190px-min grid). Break it rather than overflow (the .person-bio
   idiom); scoping to the head alone left the Explore card overflowing. */
.person strong { min-width: 0; overflow-wrap: anywhere; }
/* The moderator judges SOURCE, not a rendering of it (ADR-0053 §5), so this
   is Breaks — the literal text with its line structure, never markdown. */
.content-preview { white-space: pre-line; }
.profile-bio { white-space: pre-line; max-width: var(--measure); }
/* `.person-bio` renders the same stored string as `.profile-bio`, which is exactly the argument
   ADR-0058 §2 already made about these two disagreeing — made again, one property along. */
.person-bio { color: var(--muted); font-size: 13px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }

.pager { display: flex; gap: 12px; justify-content: center; margin: 18px 0; }
/* A <select> is the one .chip that needs a background: the sweep dropped
   `background: var(--surface)` from `.filter-group .chip` — right for an <a> or <button>,
   which should read as ruled rather than filled — and a select with none falls back to the
   UA control paint. --muted on --surface is proven >=4.5:1 by the contrast guard.

   The background alone is NOT enough, and measuring it in the browser is what showed why: a
   <select> renders with `appearance: auto`, and Chrome paints a native control from
   `color-scheme`, overriding the author background and colour outright. With no
   `color-scheme` declared the UA assumed light, so in dark theme this control kept painting
   light-on-dark while computed style still reported the light values. See the color-scheme
   declarations on the theme blocks above — those are the actual fix, and they cover every
   native control (selects, date inputs, scrollbars, the date picker), not just this one. */
select.chip { font: inherit; background: var(--surface); }

/* ── People interests + grid load-more ── */
.person-interests { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.tag-chip-sm { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); background: none; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 1px 6px; }
.people-grid .load-more { grid-column: 1 / -1; }

/* ─────────────────────────────────────────────────────────────────────────
   New prediction workspace — two-pane create form + live preview.
   Built on the theme tokens (no hardcoded palette) so it tracks dark + paper.
   ───────────────────────────────────────────────────────────────────────── */
.np { max-width: 1080px; }
.np-head { margin-bottom: 22px; }
.np-eyebrow { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent); margin-bottom: 8px; }
.np-title { font-size: var(--t-h1); font-weight: 800; letter-spacing: -0.02em; margin: 0; }
.np-sub { margin: 8px 0 0; font-size: var(--t-body); color: var(--muted); max-width: 540px; line-height: 1.5; }

.np-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
@media (min-width: 1180px) {
  .np-grid { grid-template-columns: minmax(0, 1fr) 360px; gap: 28px; }
  .np-preview { position: sticky; top: 24px; }
}

.np-form { display: flex; flex-direction: column; gap: 20px; }
/* A ruled section of the filing form, not a card floating on the page — the same
   treatment .pd-rail-card and the register head use. */
.np-card { background: none; border: 0; border-top: 2px solid var(--border-strong); border-radius: 0; padding: 16px 0 4px; }
.np-label { font-size: var(--t-meta); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--fg); }
.np-label-row { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.np-opt { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--muted-2); font-size: var(--t-meta); }
.np-help { margin: 4px 0 11px; font-size: var(--t-meta); color: var(--muted); line-height: 1.45; }
.np-help-inline { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
.np-count { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
.np-count-warn { color: var(--warn-fg); }

.np-field { width: 100%; background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 12px 14px; color: var(--fg); font: inherit; font-size: var(--t-body); }
.np-field::placeholder { color: var(--muted-2); }
.np-field:focus { outline: none; border-color: var(--accent); }
textarea.np-field { resize: vertical; line-height: 1.5; }
.np-stmt { font-size: var(--t-body); font-weight: 500; line-height: 1.45; }
.np-time { width: auto; }
.np-time-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }

/* .np-starters is gone (ADR-0031): the frame chips were the imported mock's
   invention with no prototype basis. .np-chip lives on with a better job — the
   deadline quick-picks and time presets below. */
.np-chip { font-size: var(--t-micro); color: var(--fg); background: var(--elevated); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 5px 11px; cursor: pointer; font-weight: 500; }
.np-chip:hover { border-color: var(--accent); background: var(--accent-soft); transform: none; }
.np-chip.is-active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
/* ── deadline card (ADR-0031): duration-first dual mode ── */
.np-rel-row { display: flex; align-items: center; gap: 8px; }
.np-rel-in { color: var(--muted); font-size: var(--t-sub); }
.np-rel-n { width: 76px; text-align: center; }
.np-rel-unit { width: auto; }
.np-quick { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
.np-quick-label { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-2); margin-right: 2px; }
.np-time-toggle { display: inline-flex; align-items: center; gap: 7px; font-size: var(--t-sub); color: var(--muted); cursor: pointer; }
.np-time-detail { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 8px; }
.np-time-detail .np-quick { margin-top: 0; }
.np-tznote { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
/* The policy consequence, stated before filing — same mono-micro voice as the
   record's own labels. Colors come from the state (:style), tokens only. */
.np-editwin { font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; margin-top: 6px; }

.np-conf { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; }
.np-conf-opt { display: flex; flex-direction: column; align-items: center; padding: 13px 6px; border-radius: var(--radius-sm); cursor: pointer; background: var(--bg); border: 1.5px solid var(--border-strong); color: var(--muted); }
.np-conf-opt:hover { border-color: var(--accent); background: var(--bg); transform: none; }
.np-conf-opt.is-active { background: var(--accent-soft); border-color: var(--accent); color: var(--fg); }
.np-conf-marks { font-size: var(--t-sub); letter-spacing: 2px; line-height: 1; color: var(--muted-2); }
.np-conf-opt.is-active .np-conf-marks { color: var(--accent); }
.np-conf-label { font-size: var(--t-sub); font-weight: 700; margin-top: 9px; color: inherit; }
.np-conf-pct { font-size: var(--t-micro); margin-top: 3px; color: var(--muted-2); }

.np-sources { display: flex; flex-direction: column; gap: 8px; }
.np-source { display: flex; align-items: center; gap: 12px; padding: 12px 13px; border-radius: var(--radius-sm); cursor: pointer; text-align: left; background: var(--bg); border: 1.5px solid var(--border-strong); color: var(--fg); }
.np-source:hover { border-color: var(--accent); background: var(--bg); transform: none; }
.np-source.is-active { background: var(--accent-soft); border-color: var(--accent); }
.np-radio { width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid var(--border-strong); flex: none; display: flex; align-items: center; justify-content: center; }
.np-source.is-active .np-radio { border-color: var(--accent); }
.np-source.is-active .np-radio::after { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.np-source-text { display: flex; flex-direction: column; }
.np-source-title { font-size: var(--t-sub); font-weight: 600; color: var(--fg); }
.np-source-desc { font-size: var(--t-micro); color: var(--muted); }

.np-attr { border-left: 3px solid var(--accent); }
.np-attr-head { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; color: var(--accent); }
.np-reshare-icon { color: var(--accent); flex: none; }
.np-countdown { margin-top: 12px; font-size: var(--t-meta); font-weight: 600; }

.np-seg { display: inline-flex; background: none; border: 0; border-bottom: 1px solid var(--border-strong); border-radius: 0; padding: 0; gap: 22px; margin-bottom: 20px; }
.np-seg-opt { border: 0; border-bottom: 2px solid transparent; border-radius: 0; margin-bottom: -1px; padding: 8px 0; font-family: var(--mono); font-size: var(--t-meta); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; background: none; color: var(--muted); }
.np-seg-opt:hover { background: transparent; color: var(--fg); transform: none; }
.np-seg-opt.is-active, .np-seg-opt.is-active:hover { background: none; color: var(--fg); border-bottom-color: var(--accent); }

.np-tags-head { margin-bottom: 10px; }
.np-taginput { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 9px 11px; cursor: text; }
.np-tagchip { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: var(--t-meta); background: var(--elevated); border: 1px solid var(--border); color: var(--accent-light); border-radius: var(--radius-sm); padding: 4px 9px; overflow-wrap: anywhere; }
/* `flex: none` is the touch floor's other half, and it was missing (round six, measured).
   REG-MOBILE-060 gives this control `width: 44px; height: 44px` at <=768px because it is
   DESTRUCTIVE — ADR-0070 §9 made it an in-flow box for that reason — but a specified width on a
   flex item with the default `flex-shrink: 1` is a starting point, not a floor. Measured at 390px
   with a 50-character tag: the button computed **32px** wide against its declared 44. Nothing
   could see it: mobile_guard_test.go asserts the 44px as a DECLARATION, and no step had ever
   rendered this chip (PF118x now does). */
.np-tagx { background: none; border: 0; color: var(--muted); font-size: var(--t-sub); line-height: 1; cursor: pointer; padding: 0; flex: none; }
.np-tagx:hover { color: var(--fg); background: none; transform: none; }
.np-tagfield { flex: 1; min-width: 140px; background: transparent; border: none; color: var(--fg); font: inherit; font-size: var(--t-sub); padding: 4px; }
.np-tagfield:focus { outline: none; }
/* "tags unavailable" is a STATEMENT OF IGNORANCE, not a tag: muted, not a link, and never styled
   as something to click (ADR-0071 §8). */
.pd-tags-unknown { color: var(--muted-2); font-style: italic; }

.np-actions { display: flex; align-items: center; gap: 14px; }
.np-submit { font-weight: 700; font-size: var(--t-meta); font-family: var(--mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 13px 26px; border-radius: var(--radius-sm); }
/* The ready gate is enforced, not advisory (ADR-0031 / PR #30 review): dimmed and
   inert until a statement plus a deadline-or-open-ended choice exist. */
.np-submit:disabled { opacity: 0.55; cursor: not-allowed; }
.np-submit:disabled:hover { background: var(--accent); transform: none; }
.np-ready { margin-left: auto; font-size: var(--t-meta); color: var(--muted-2); }

/* live preview pane */
.np-preview-label { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2); margin-bottom: 11px; display: flex; align-items: center; gap: 8px; }
.np-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.np-prevcard { background: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 19px; }
.np-prev-head { display: flex; align-items: center; gap: 11px; margin-bottom: 14px; }
.np-prev-id { flex: 1; min-width: 0; }
.np-prev-name { font-size: var(--t-sub); font-weight: 700; color: var(--fg); }
.np-prev-handle { font-size: var(--t-micro); color: var(--muted); }
.np-prev-reshare { display: flex; align-items: center; gap: 8px; margin-bottom: 11px; padding: 8px 11px; background: var(--accent-soft); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--t-micro); color: var(--muted); }
.np-prev-resharewho { color: var(--accent-light); font-weight: 600; }
.np-prev-stmt { font-family: var(--serif); font-size: var(--t-claim); font-weight: 500; line-height: 1.4; margin: 0; color: var(--fg); min-height: 25px; }
.np-prev-stmt.is-empty { color: var(--muted-2); font-weight: 500; }
.np-prev-desc { margin: 11px 0 0; font-size: var(--t-meta); line-height: 1.5; color: var(--muted); border-left: 2px solid var(--border); padding-left: 11px; }
.np-prev-timing { margin-top: 16px; }
.np-prev-timing-row { display: flex; justify-content: space-between; font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.05em; color: var(--muted-2); margin-bottom: 6px; }
/* A dotted track with a now-dot, the register's own idiom — not the per-row progress
   bar ADR-0027 replaced with the shared axis. */
.np-prev-bar { position: relative; height: 9px; }
.np-prev-bar::before { content: ""; position: absolute; left: 0; right: 0; top: 4px; border-top: 1px dotted var(--muted-2); }
.np-prev-bar-fill { position: absolute; top: 0; left: 0; width: 9px; height: 9px; margin-left: -4.5px; border-radius: 50%; background: var(--accent); }
.np-prev-meta { display: flex; align-items: center; gap: 8px; margin-top: 15px; flex-wrap: wrap; }
.np-prev-vis { display: inline-flex; align-items: center; gap: 5px; font-size: var(--t-micro); color: var(--muted); background: var(--elevated); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 11px; }
/* REG-DESIGN-017 on the composer's PREVIEW tag, the third surface rendering a tag name and the
   one no guard could see: the derived break list comes from the bidi-isolation inventory, and this
   class was never in it. Measured at 390px with a 50-character tag — the real `maxTagLen` — the
   preview chip pushed the document to 393px against a 375px client width, so the composer scrolled
   sideways while the two chips beside it wrapped. Isolated as well as broken, since it renders
   user-authored text on a line it shares with others (ADR-0055 §7). */
.np-prev-tag { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); background: var(--elevated); border-radius: var(--radius-sm); padding: 4px 10px; overflow-wrap: anywhere; unicode-bidi: isolate; }
.np-prev-foot { display: flex; align-items: center; gap: 16px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); font-size: var(--t-meta); color: var(--muted-2); }
.np-prev-foot-tag { margin-left: auto; font-family: var(--mono); font-size: var(--t-micro); }
.np-tip { display: flex; gap: 10px; margin-top: 13px; padding: 12px 0 0; background: none; border: 0; border-top: 1px solid var(--border); border-radius: 0; }
.np-tip svg { flex: none; margin-top: 1px; color: var(--accent); }
.np-tip p { margin: 0; font-size: var(--t-meta); line-height: 1.5; color: var(--muted); }

/* ─────────────────────────────────────────────────────────────────────────
   Prediction detail — two-pane workspace (prediction + phased discussion |
   owner Manage rail + lifecycle). Built on the theme tokens so it tracks the
   dark and paper themes. Inset boxes/inputs use --bg; raised panels use --surface.
   ───────────────────────────────────────────────────────────────────────── */
.pd { max-width: 1080px; }
.pd-ico { flex: none; vertical-align: -2px; }
.pd-ico-mute { color: var(--muted-2); }
.pd-back {
  display: inline-flex; align-items: center; gap: 7px; margin-bottom: 18px; text-decoration: none;
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted-2);
}
.pd-back:hover { color: var(--accent); }

.pd-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 26px; align-items: start; }
.pd-main { display: flex; flex-direction: column; gap: 26px; min-width: 0; }
.pd-rail { display: flex; flex-direction: column; gap: 20px; }

/* ── the dossier ── one entry in the register, opened. No shadow: --shadow on paper
   is a hard 6px offset, which on a borderless transparent card drew a phantom warm
   rectangle behind the content. ── */
/* No bottom rule: .pd-record already closes the dossier's data with its own, and
   .comments draws the separator before the discussion — a third hairline inside 110px
   read as a stray double rule with nothing between the two. */
.pd-card { background: none; border: 0; border-radius: 0; padding: 0; }
/* FILED (who) on the left, STANDING on the right — the register's column order. */
.pd-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.pd-standing { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pd-author { display: flex; align-items: center; gap: 11px; min-width: 0; }
/* Found by the derived guard rather than by a third reviewer: the attribution is a username, an
   unbroken token with no soft-wrap opportunity — the same argument `.acct-handle` was given in
   REG-DESIGN-020, on a line nobody had connected to it. */
.pd-reshare {
  display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
  background: none; border: 0; border-radius: 0; padding: 0;
  font-family: var(--serif); font-style: italic; font-size: var(--t-meta); color: var(--muted-2);
  overflow-wrap: anywhere;
}
.pd-reshare strong { color: var(--muted); font-weight: 500; }
/* The claim is the one thing set in the serif, and 500 is not a preference: Spectral
   ships 500 roman + 400 italic only, so the old 800 was a synthetic bold. */
/* pre-line, not pre-wrap (ADR-0053): the author's line breaks are the thing
   #46 asks for, but pasted indentation and space runs are not, and a claim is
   not preformatted text. Composes with text-wrap: pretty, which governs where
   the REMAINING soft wraps fall. */
/* THE BOX CONTAINS THE LEADING IT DRAWS (issue #51). Spectral's content area
   measures 1.5208em ((254 + 111) / 240) and the claim's leading is 1.28em, so the
   half-leading is NEGATIVE by 0.1204em at each end. A scroll container measures its
   scrollable overflow on the CONTENT AREA, not on the line box — and ADR-0058 §2 made
   this element one — so every claim overflowed its own box by ~3px and drew a
   scrollbar for every reader whose OS paints classic ones, stealing 15px of the
   measure on the way (clientWidth 419 -> 404, which is where the reporter's first line
   broke). The padding is that room and the negative margin hands it straight back to
   the layout, so no ink moves WHILE THE CAP IS OFF: the claim's rect and the block
   after it are unchanged to 0.01px at 24px and at 34px (PF111). The bound matters —
   `* { box-sizing: border-box }` puts this padding INSIDE max-height below, so for a
   claim tall enough to hit the cap the border box stays put while the MARGIN box
   shrinks by 0.32em and the record moves up ~7.7px (24px) / ~10.9px (34px). That is
   the 150-newline claim ADR-0058 §2 is about, and 8px on it is not worth a second
   number; it is stated because "no ink moves" without the qualifier is false.

   0.16em, and the number is MEASURED rather than derived, because the overhang is not
   a constant fraction of the font size — the browser rounds a font's ascent and
   descent per size, so it is 0.1254em at 24px and 0.1474em at 34px, the two sizes the
   claim renders (PF110i). The scrollbar edge alone would take 0.11em; the top edge
   takes more, and one number serves both with room to spare. 0.13em was the first
   answer and it was 0.59px short at the top at 34px — invisibly, which is the point:
   nothing reports a clipped top edge, so only a measurement finds it.

   Symmetric because the box is short at both ends: the bottom is what the scrollbar
   reports, the top is what the container silently clips. The top half changes nothing
   visible today — no glyph reaches that edge — and it does not promise an unbounded
   one; a deliberately stacked diacritic can still exceed it.

   The number is NOT fallback-dependent, which is the question a serif with partial
   coverage invites: ten samples (Latin, Cyrillic, Greek, CJK, Thai, Devanagari,
   Arabic, mixed emoji, emoji-only, stacked combining marks) each overflow by the same
   ~3px and each are closed by the same number, because the box follows THIS element's
   font metrics rather than the font that draws the glyph. */
.pd-statement {
  font-family: var(--serif); font-size: var(--t-h2); font-weight: 500;
  line-height: 1.28; letter-spacing: 0; margin: -0.16em 0; color: var(--fg);
  max-width: 34ch; text-wrap: pretty; white-space: pre-line;
  padding-block: 0.16em;
}
/* 1180px is the register's own breakpoint — one number in the language, not two. At
   1180 the shell leaves main 860px, so the claim column is 860-300-32 = 528px, about
   31 characters of 34px Spectral. Below it the claim steps down to --t-h2.

   THE BLOCK SITS AFTER THE BASE RULES IT OVERRIDES, and that placement is load-bearing
   rather than tidy (issue #51): a media query adds no specificity, so a base rule
   later in the file wins at every width the query matches. Declared above
   `.pd-statement` — where it lived from 4db86cc until this commit — the font-size here
   was dead on arrival: at a 1920px viewport `matchMedia('(min-width: 1180px)')`
   matched and the claim still computed 24px, so the 34px display size this comment
   describes had never once rendered. `.pd-rail { position: sticky }` was one
   declaration away from the same fate — it survived only because the base `.pd-rail`
   rule below happens not to set `position`.
   TestDesignGuard_NoBreakpointOverrideIsDeadOnArrival fails the build for THIS SHAPE — a later
   base rule at the SAME selector, which is decidable from source order and weight alone. It does
   NOT resolve specificity, and cannot soundly: a later `.pd-main .pd-statement` at (0,2,0) would
   kill this font-size again, but it matches a SUBSET of `.pd-statement` elements, so calling the
   media rule dead would fail correct CSS. What covers that here is the inventory in
   usertext_guard_test.go, which lists every rule whose SUBJECT is one of these boxes and fails on
   one it has not seen — including a descendant. */
@media (min-width: 1180px) {
  .pd-grid { grid-template-columns: minmax(0, 1fr) 300px; gap: 32px; }
  .pd-grid-solo { grid-template-columns: minmax(0, 1fr); }
  .pd-rail { position: sticky; top: 24px; }
  .pd-statement { font-size: var(--t-h1); }
}
/* Resolution criteria annotates the claim, and a ruled margin is the one place a
   left rule belongs in a register. */
.pd-criteria {
  margin: 15px 0 0; font-size: var(--t-body); line-height: 1.6; color: var(--muted);
  border-left: 2px solid var(--border-strong); padding-left: 14px; max-width: var(--measure);
  overflow-x: auto;
}

/* ── .prose ── Rich mode's container (ADR-0053 §1): the markdown subset, drawn
   in the register's idiom. Four design guards constrain this block, and the
   obvious first draft of a code style trips two of them — hence a token
   background rather than a literal, var(--radius-sm) rather than a hand-picked
   3px, var(--t-sub) rather than --t-micro (the 11px floor), and no new custom
   properties at all. */
/* Rich content CONTAINS its own horizontal overflow (ADR-0054 §4). Nesting
   inset is a layout problem and this is the layer that owns layout: markdown
   nests one blockquote per '>' character, so a 532-rune comment can inset its
   text by 500 x 14px and scroll the whole PAGE sideways for every reader. The
   same rule is what a wide table or an unbreakable token would need, and it
   needs no constant in Go that has to be re-derived when the parser list
   changes. Contained, not clipped: the box scrolls, exactly as .prose pre does
   one rule below. */
.prose { overflow-x: auto; max-height: max(70vh, var(--text-floor)); overflow-y: auto; }
/* And its own HEIGHT (ADR-0055 §7). ADR-0053 removed the only vertical bound
   this content had — `white-space: normal` collapsing newlines — and put
   nothing back, so a comment of "x\n" repeated 5 000 times is exactly the
   10 000-rune storage cap and renders 4 999 <br>, roughly 40x the height the
   same runes produced before, burying every later comment in the thread. 70vh
   is chosen so nothing is ever clipped by more than one screenful: an item
   taller than the viewport was going to be scrolled past either way, and now it
   scrolls inside its own box instead of pushing the discussion off the page.

   The 32rem FLOOR is what stops that rule biting a short viewport (ADR-0056 §2).
   A phone in landscape is ~390px tall, where 70vh is ~273px — about eleven
   lines, so a perfectly ordinary medium comment would have scrolled inside
   itself. The floor costs the bound nothing: 512px against the 116 250px one
   maximal comment renders is the same order of magnitude as 655px, and on any
   normal viewport max() picks the vh term, so the value PF26 measured does not
   move.

   ROOT-relative, not em (ADR-0079, arithmetic corrected in round six). It was
   `32em`, which resolved to a DIFFERENT number on each surface: 480px here and on
   `.profile-bio`, `.mod-justification` and `.case-detail .content-preview` (the
   document is --t-body, 15px, and nothing sets a font-size on html/:root, so 1em
   is 15px while 1rem is 16px); 432px on `.board-body`, which is --t-sub; and
   768px / 1088px on `.pd-statement`, which is --t-h2 / --t-h1. ADR-0058 §2 gave
   one literal to five surfaces of four sizes, and on the only one with a DISPLAY
   size the em term won at every normal viewport — inverting the "max() picks the
   vh term" property this very paragraph asserts, and making the claim's bound
   121% of a 1440x900 window.

   A floor is a VIEWPORT rescue, so how large the text on one surface is has
   nothing to do with it — but how large the READER set their root font does: a
   viewer who enlarges text needs more room for the same number of lines. That is
   what `rem` follows and `em` did not. --text-floor is 30rem = 480px, which is
   what the four body-sized surfaces already had, so their bound does not move and
   the crossover where 70vh overtakes the floor stays at 686px. */
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose p { margin: 0 0 0.7em; }
.prose ul, .prose ol { margin: 0 0 0.7em; padding-left: 1.4em; }
.prose li { margin: 0.2em 0; }
.prose li > ul, .prose li > ol { margin: 0.2em 0 0; }
.prose blockquote {
  margin: 0.7em 0; padding-left: 12px;
  border-left: 2px solid var(--border-strong); color: var(--muted-2); font-style: italic;
}
.prose code {
  font-family: var(--mono); font-size: var(--t-sub);
  background: var(--raised); padding: 1px 5px; border-radius: var(--radius-sm);
}
/* A long line of code scrolls INSIDE its own box; the page never scrolls
   sideways because of one pasted command. */
.prose pre {
  margin: 0.7em 0; padding: 10px 12px; overflow-x: auto;
  background: var(--raised); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.prose pre code { background: none; padding: 0; border-radius: 0; }
.prose hr { margin: 1em 0; border: 0; border-top: 1px solid var(--border); }
.prose strong { font-weight: 600; color: var(--fg); }
.prose del { color: var(--muted-2); }
/* A URL is never truncated to fit — that would break text-equals-href, the one
   property the link rule exists to hold (ADR-0053 §3). It wraps instead. The
   same property .reg-after already uses in this stylesheet. */
.prose a { color: var(--accent); overflow-wrap: anywhere; }

/* ── bidi containment ── every container that renders user text (ADR-0055 §7).
   Neither Normalize, Flatten nor bluemonday removes U+202E RIGHT-TO-LEFT
   OVERRIDE, and an unterminated override runs to the end of the bidi PARAGRAPH
   — the block container — so a statement ending "…gpj.exe<RLO>nasa" reversed
   the display of the sibling badges and timestamps rendered in the same
   register row. `unicode-bidi: isolate` confines the algorithm to the element,
   which closes that without deleting a character and without breaking
   legitimate right-to-left text, which needs U+200F and U+200E to render at
   all. Stripping the controls would do the second thing to fix the first. */
.prose,
.pd-statement,
.pd-criteria,
.profile-bio,
.content-preview,
.mod-justification,
.reg-claim,
.reg-desc,
.reg-after,
/* The six the first version missed (ADR-0058 §2). It said "every container that
   renders user text" and named nine. Derived from the call sites, there are
   fourteen. `.person-bio` shows why a hand-kept list fails: `.profile-bio` WAS
   covered, so the two bio surfaces disagreed about the same stored string.
   `.meta` is taken whole rather than qualified — it is a generic utility class
   carrying three separate user-text sites, and isolation is a display-only
   property that costs nothing on the rest. */
.case-preview,
.person-bio,
.search-hit,
.meta,
.pd-reshare,
/* The notice board's two user-text surfaces (ADR-0063 §8). Admin-authored rather than
   member-authored, which changes nothing here: an RTL run reorders the line it sits on
   whoever typed it, and the board's title shares a line with the date and the controls
   in strip mode. */
.board-title,
.board-body,
/* The /admin panel's copy of the title. Added because the guard REFUSED the render site
   for having no element of its own — `<strong>{ flat(it.Title) }</strong>` was unisolated
   and unverifiable, on a page that lists both kinds side by side where an RTL title would
   reorder the row it shares with a date and two controls. Exactly the "new render site
   fails the build until its container is covered" property a hand-kept list cannot have. */
.board-panel-title,
/* The two tag surfaces, added BY HAND with this comment as the reason: a tag name is
   user-authored, and TestProseGuard_EveryUserTextSiteIsBidiIsolated derives its list from
   `flat(` / `breaks(` / `@Rich(` call sites, which neither of these is. `.np-tagchip`
   renders through Alpine `x-text`, and `.meta-tag` through a plain `{ "#" + t.Name }`
   concat. `.meta-tag` predates the edit panel and is included anyway: it sits on the same
   page, and an unterminated RTL override in a tag name reorders the visibility, confidence
   and counts sharing its row — the exact failure this rule exists for (ADR-0065 §9). */
.np-tagchip,
.meta-tag { unicode-bidi: isolate; }

/* Breaks mode carries the same vertical hazard Rich does, and ADR-0055 §7
   bounded only .prose (ADR-0058 §2). `a` + Enter x150 is exactly 300 UTF-16
   units, so maxlength="300" accepts it, it normalizes to 299 runes so the
   server accepts it, and .pd-statement renders 150 line boxes at 24px x 1.28 —
   about 4 600px of heading, with the dossier, the record and the whole
   discussion pushed below it. Typed, not crafted. A heading with a scrollbar is
   the lesser evil, and at 300 runes the cap only engages past ~28 lines. */
/* And the same box in the OTHER axis, which the first version of this fix gave to
   `.pd-statement` alone (issue #51, review round one). `overflow-y: auto` computes
   `overflow-x` to `auto` as well, so a surface with no break opportunity does not spill
   — it SCROLLS SIDEWAYS. Measured with one 67-character URL: the claim 700 against 404
   at 24px; `.profile-bio` and `.mod-justification` 53px of sideways scroll at a 390px
   body, `.content-preview` 112px. Three of the four render exactly the text most likely
   to carry a URL — a bio, a reported comment, a moderator's justification of why a link
   was removed — so the declaration belongs on the list, not on one member of it.

   ADR-0054 §4 chose the opposite for `.prose` and was right to: a nested blockquote, a
   wide table and a <pre> cannot wrap, and scrolling contains them. A URL can wrap. */
.pd-statement,
.profile-bio,
.content-preview,
.mod-justification { max-height: max(70vh, var(--text-floor)); overflow-y: auto; overflow-wrap: anywhere; }
/* `.board-body` is NOT in that list any more, and its absence is the point (ADR-0064 §4).
   It carries `.prose` since the body became Rich, and `.prose` declares the same bound —
   two sources for one number is how the two drift. The bound still matters more here than
   anywhere it applies: 400 characters of "a\n" is 200 line boxes, and at >=1500px the board
   is `position: sticky`, where a 4 000px element sticks to nothing. BS-B14 measures it. */

/* ── THE record ── the page's one timeline. It replaced an inline TARGET/REMAINING
   inset, a right-rail lifecycle step list, and a third partial restatement in the
   verdict note; each fact now appears exactly once. ── */
.pd-record { margin-top: 22px; border-top: 1px solid var(--border-strong); border-bottom: 1px solid var(--border); padding: 14px 0 12px; }
.pd-record-ends { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; }
.pd-rec-right { text-align: right; }
.pd-rec-k { font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2); margin-bottom: 4px; }
.pd-rec-v { font-family: var(--mono); font-size: var(--t-sub); font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; }
.pd-rec-open { color: var(--muted-2); font-weight: 500; }
/* Same drawing idiom as the register's axis: a dotted track, an inked elapsed
   portion, and a now-marker positioned from one percentage. */
.pd-span { position: relative; height: 9px; margin: 12px 0 2px; }
.pd-span::before { content: ""; position: absolute; left: 0; right: 0; top: 4px; border-top: 1px dotted var(--muted-2); }
.pd-span-ink { position: absolute; left: 0; top: 4px; height: 0; border-top: 1px solid var(--accent); }
.pd-span-now { position: absolute; top: 0; width: 9px; height: 9px; margin-left: -4.5px; border-radius: 50%; background: var(--accent); }
.pd-record-foot { display: flex; justify-content: space-between; gap: 12px; margin-top: 10px; font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-2); }
.pd-remaining { color: var(--accent); }

/* the register's own meta line, reused verbatim; only these two are page-specific */
/* Qualified, not bare: the element is `class="reg-meta pd-meta"`, and .reg-meta declares
   its own margin-top further down the file. At equal specificity source order decided it,
   so a bare `.pd-meta` lost silently. */
.reg-meta.pd-meta { margin-top: 18px; }
.pd-vis { letter-spacing: 0.06em; white-space: nowrap; }
.pd-stat { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }

/* The verdict note. No fill and no colour strip: the stamp above and the word
   Hit/Miss already carry the outcome twice, without spending a third signal. */
.pd-resolved { margin-top: 18px; border-top: 1px solid var(--border); padding-top: 13px; font-size: var(--t-sub); }
.pd-resolved p { margin: 6px 0 0; color: var(--muted); max-width: var(--measure); }

/* owner inline edit form (left column) */
.pd-editform { display: flex; flex-direction: column; gap: 12px; background: none; border: 0; border-top: 2px solid var(--border-strong); border-bottom: 1px solid var(--border); border-radius: 0; padding: 16px 0; }
.pd-editform-head { font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2); }
.pd-editform label { display: flex; flex-direction: column; gap: 5px; font-size: var(--t-sub); color: var(--muted); }
.pd-editform textarea, .pd-editform select, .pd-editform input[type="date"], .pd-editform input[type="time"] { background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 10px 12px; color: var(--fg); font: inherit; }
/* Pointer focus is a border colour; keyboard focus is the global :focus-visible ring. */
.pd-editform textarea:focus, .pd-editform select:focus, .pd-editform input[type="date"]:focus, .pd-editform input[type="time"]:focus { outline: none; border-color: var(--accent); }
/* The panel's three composite fields (tags, visibility, deadline) are plain blocks in the
   form's flex column; each owns its own label row. */
.pd-editform-field { display: block; }
.pd-edit-deadline { display: flex; flex-wrap: wrap; gap: 10px; }
/* `.pd-editform label` sets --t-sub/--muted and out-specifies `.np-label` (0,1,1 vs 0,1,0),
   so the tag, visibility and deadline heads borrowed from the composer would have rendered
   in a different size and colour from the composer's own. Restores `.np-label`'s values,
   no new tokens (ADR-0065). */
.pd-editform .np-label { font-size: var(--t-meta); font-weight: 700; color: var(--fg); }
.pd-editform-actions { display: flex; gap: 10px; }

/* ── right rail ── a titled column of the ledger, not a card floating beside it ── */
.pd-rail-card { background: none; border: 0; border-top: 2px solid var(--border-strong); border-radius: 0; overflow: visible; }
.pd-rail-head { display: flex; align-items: center; gap: 8px; padding: 10px 0 8px; font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2); border-bottom: 1px solid var(--border); }
.pd-rail-head svg { color: var(--accent); }
.pd-rail-tag { margin-left: auto; font-family: var(--mono); font-size: var(--t-micro); font-weight: 500; letter-spacing: 0.04em; text-transform: none; color: var(--muted-2); }
.pd-rail-body { padding: 14px 0; display: flex; flex-direction: column; gap: 14px; }

/* manage rows + controls */
.pd-manage-row { display: flex; align-items: center; gap: 11px; width: 100%; background: none; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; padding: 10px 0; cursor: pointer; text-align: left; color: var(--fg); }
.pd-manage-row:hover { border-bottom-color: var(--accent); transform: none; }
.pd-manage-row-txt { flex: 1; display: flex; flex-direction: column; }
.pd-manage-row-title { font-size: var(--t-sub); font-weight: 600; color: var(--fg); }
.pd-manage-row-sub { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
/* Resolve and Delete were the loudest things on the page — two saturated full-width
   blocks in the rail, beside a claim set in a reading serif. They become ruled
   controls, and the confirm buttons are drawn as the stamp they are about to apply
   (compare .reg-stamp) rather than as a new device. transform:none cancels the
   global button hover lift; the register does not move under the pointer. */
.pd-btn { background: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--fg); font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; padding: 10px 12px; cursor: pointer; }
.pd-btn:hover { border-color: var(--accent); color: var(--accent); background: none; transform: none; }
.pd-btn-block { width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.pd-btn-hit { color: var(--good-fg); border-color: currentColor; border-width: 2px; }
.pd-btn-miss { color: var(--bad-fg); border-color: currentColor; border-width: 2px; }
.pd-btn-danger { width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: none; border: 1px solid var(--bad-fg); color: var(--bad-fg); border-radius: var(--radius-sm); font-family: var(--mono); font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; padding: 10px 12px; cursor: pointer; }
.pd-btn-danger:hover { background: var(--bad-bg); transform: none; }

/* resolve box — a DISCLOSURE since ADR-0075, open only once the claim is due.

   The summary carries `.pd-manage-row` verbatim, the Edit row's own class, so the rail reads
   as a list of owner rows rather than a row plus a panel. The `border-top` and `14px` pad this
   rule used to hold went with it: that hairline sat directly under the Edit row's own
   `border-bottom` with the body's 14px `gap` between them, and the pair drew a double rule. */
.pd-resolve { background: none; border: 0; border-radius: 0; padding: 0; }
/* No UA triangle beside the chevron. `.pd-manage-row`'s `display: flex` already suppresses the
   marker in Chrome and Firefox; these two say so outright rather than depending on a side
   effect of a display value some later edit is free to change. */
.pd-resolve-row { list-style: none; }
.pd-resolve-row::-webkit-details-marker { display: none; }
/* Scoped to `.pd-ico-mute` — on this row that is the chevron and nothing else, where a bare
   `.pd-ico` would spin the ✓ as well. Transition is covered by the reduced-motion block above. */
.pd-resolve-row .pd-ico-mute { transition: transform 120ms ease; }
.pd-resolve[open] > .pd-resolve-row .pd-ico-mute { transform: rotate(90deg); }
/* Hovering an OPEN summary must not accent the rule beneath it: once expanded that hairline
   is an interior divider between the disclosure's own header and its own body, and
   `.pd-manage-row:hover`'s accent reads as an end-of-section marker in mid-section.
   Collapsed, the same rule IS the row's edge and keeps the hover. Outranks the base rule
   on specificity (0,4,0 vs 0,2,0), so it does not depend on source order. ADR-0076. */
.pd-resolve[open] > .pd-resolve-row:hover { border-bottom-color: var(--border); }
.pd-resolve-note { margin: 8px 0 11px; font-size: var(--t-micro); color: var(--muted-2); line-height: 1.5; }
.pd-outcomes { display: flex; gap: 8px; margin-bottom: 10px; }
.pd-outcome { flex: 1; border: 1.5px solid var(--border-strong); background: none; color: var(--muted); font-family: var(--mono); font-weight: 600; font-size: var(--t-meta); letter-spacing: 0.08em; text-transform: uppercase; padding: 9px; border-radius: var(--radius-sm); cursor: pointer; }
.pd-outcome:hover { transform: none; }
.pd-outcome-hit.is-active { border-color: var(--good-fg); background: var(--good-bg); color: var(--good-fg); }
.pd-outcome-miss.is-active { border-color: var(--bad-fg); background: var(--bad-bg); color: var(--bad-fg); }
.pd-evidence { width: 100%; background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 10px 12px; color: var(--fg); font-family: var(--mono); font-size: var(--t-meta); margin-bottom: 10px; }
.pd-evidence:focus { outline: none; border-color: var(--accent); }

/* ── discussion: the phase strip + panels ──
   Three rounded boxes become one ruled band divided into segments, using the tab
   device already shipped in .tabs: a 2px accent rule sitting ON the strip's own
   baseline rule. CSS only — the APG roving-tabindex markup is untouched. ── */
.comments-head { align-items: flex-start; gap: 12px 18px; }
.comments-title h2 { margin: 0; }
.disc-sub { margin: 4px 0 0; font-size: var(--t-sub); color: var(--muted); line-height: 1.5; max-width: var(--measure); }
/* Owner controls: the dossier's control voice (.pd-btn), right-aligned against the
   Discussion masthead. This container shipped with NO rule — its buttons wrapped as
   raw inline content, colliding borders with zero row gap. Wrap stays allowed; the
   flex gap makes it deliberate on both axes. */
.owner-comment-controls { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
/* Hide all / Unhide all are inverse sweeps over the same set — one segmented pair
   sharing its middle rule. The hovered/focused half lifts (z-index) so its full
   ruled edge and the focus ring paint over the overlapped 1px. */
.oc-pair { display: inline-flex; }
.oc-pair .pd-btn + .pd-btn { margin-left: -1px; }
.oc-pair .pd-btn:hover, .oc-pair .pd-btn:focus-visible { position: relative; z-index: 1; }
/* Placed after the base .comments-head override above — an earlier media block
   would lose to it at equal specificity. */
@media (max-width: 768px) {
  .comments-head { flex-direction: column; }
  .owner-comment-controls { justify-content: flex-start; }

  /* ══ THE PREDICTION DETAIL PAGE'S TOUCH FLOORS (ADR-0042 §6 applied to the surface its
     enumeration missed; REG-MOBILE-051). §6 listed the shell's controls, the toolbar's, the
     drawer's rows and the in-record tab items — this page appeared in none of them, so
     nothing measured it until MB13 swept it.

     THEY LIVE IN THIS BLOCK, not the ≤768px block ~500 lines up, and that is a cascade
     requirement rather than a filing preference: `.pd-btn` (base), `.pd-outcome` (base) and
     the rest are declared just above here, and a media-query rule does not outrank a base
     rule — at equal specificity SOURCE ORDER decides. Placed earlier, any later base-rule
     `min-height` would silently win, and a guard scoped to the media block would not see
     it (the exact shape ADR-0046 §7 was written about).

     The 44px half — the page's own action controls. Measured 40px each: `.pd-btn` at
     `padding: 10px 12px` around an 11px mono glyph, and `.pd-outcome` at `padding: 9px` +
     `1.5px` border around 12px type. `min-height`, not more padding, which would distort
     pills whose proportions are set by that type. These are the consequential controls —
     disabling comments, hiding a thread, resolving the record, deleting it — the ones a
     mis-tap costs most. `.pd-outcome` was missed by the first pass and by its gate's
     allowlist at once, so the claim "no `.pd-*` control is under 44px" was untrue AND
     untested until round 6 found it. ══ */
  .pd-btn,
  .pd-btn-danger,
  .pd-outcome { min-height: 44px; }

  /* The edit panel's own fields. It carried ONE control (a `select`, and only for SELF
     claims) when §6 enumerated this page, so it was never measured; it carries five now.
     `min-height` for the boxes, for the reason stated above — padding would distort a
     date control whose proportions the UA sets. `.np-tagfield` is the composer's class
     reused verbatim, so this reaches the composer's phone layout too, which is correct:
     it was under the floor there as well. */
  .pd-editform select,
  .pd-editform input[type="date"],
  .pd-editform input[type="time"],
  .np-tagfield { min-height: 44px; }

  /* And the OTHER phone floor, the one §6 measured for `.search input` and this block did not
     inherit: 16px is where iOS stops auto-zooming on focus, and every control here takes
     `font: inherit` off a --t-body of 15px. Tapping the date field on an iPhone zoomed the
     dossier in and left it there. Same axis (≤768px, the touch band), same reason, and the
     textareas ride along because a zoomed-in statement field is the worst of the five.
     ADR-0069 §11 / REG-MOBILE-051. */
  .pd-editform textarea,
  .pd-editform select,
  .pd-editform input[type="date"],
  .pd-editform input[type="time"],
  .pd-evidence,
  .np-tagfield { font-size: 16px; }

  /* The chip's × is an IN-FLOW box, not an inset `::after` (ADR-0070 §9). The overlay idiom §6
     chose for `.acct-seal` and `.watch-btn` works where the mark has empty space around it; here
     it does not. At `inset: -16px -14px` the pseudo-element is 38×44 over a ~10×12 glyph inside a
     `gap: 8px` WRAPPING flex row, so it paints over the tag field's leading edge (tap the field,
     lose a tag) and ~9px into the adjacent wrapped row, where later-in-DOM chips win (tap one
     row's ×, delete the row below's tag). A destructive action bound to a mis-hit.
     A box that participates in layout cannot paint over its neighbours, which makes the property
     structural instead of arithmetic — and it costs the chip's height, not the glyph's position
     inside it. The chip carries the text's padding; the button carries the target. */
  .np-tagchip { padding: 0 0 0 12px; gap: 2px; }
  .np-tagx { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; }

  /* The 24px half. Three mechanisms, because which one works is a MEASURED question:

     `.pd-back` (117×18) takes an inset `::after` — the idiom §6 chose for `.acct-seal` and
     `.watch-btn`, for the same reason: the ink may not move. Its `position: relative` is
     load-bearing, not decoration — without it the absolutely-positioned pseudo-element
     resolves against the initial containing block and becomes a transparent viewport-wide
     overlay that swallows every tap on the page. `TestDesignGuard_PhoneTapTargets` asserts
     the pairing so the line cannot be deleted as redundant.

     `a.author` (38–45×22) takes PADDING, because its base rule carries `overflow: hidden`
     for the username ellipsis guard and an overflow-clipped box clips its own `::after` —
     measured, the inset applied, computed `-2px 0px`, and hit-tested 2/4 anyway. Padding is
     safe here precisely because §6's "never buy a mark's area with padding" governs DRAWN
     marks, where padding grows the drawn circle; `.author` draws nothing.

     `display: inline-block` with it, and that is not cosmetic: vertical padding does not
     grow the line box of an inline non-replaced element. On the comment and detail rows
     `.author` is a flex item and is blockified anyway, but the moderation queue renders it
     inside a plain `<li>` (`.vote-list li`), where it stays inline and the padding bought
     nothing. Three surfaces were claimed and two delivered until round 6 measured the third.

     `a.author`, never bare `.author`: moderation.templ also renders a non-interactive
     `<span class="author">anonymous</span>`, and a hit area on a span is a phantom target. */
  .pd-back { position: relative; }
  .pd-back::after { content: ''; position: absolute; inset: -4px 0; }
  a.author { display: inline-block; padding: 2px 0; }

  /* The comment vote arrows. `▲`/`▼` measure 19×28 — clearing 24 by HEIGHT and failing it
     by WIDTH, which is how the first survey recorded them as "clears": it read one axis.
     WCAG 2.5.8 is 24×24, not 24-in-one-dimension.

     `::after` rather than `min-width` because the box must not move: `.votes` is
     `inline-flex; gap: 6px` with the score between the arrows, so growing the boxes would
     reflow the row on every comment in the thread. `-4px`, not `-3px`: the 19px width is
     8px of padding plus a ~11px glyph advance drawn by whatever face in the `--font` chain
     carries U+25B2, which differs between a macOS system symbol font and a Linux runner's
     DejaVu/Noto. At `-3px` the probe cleared each edge by 0.51px — one pixel of glyph
     shrinkage away from a red gate with no code change. `-4px` gives 27px and 1.51px of
     margin; the arrows sit 35px apart and the only thing between them is a `<span>`, so
     nothing overlaps a control. */
  .votes button.link { position: relative; }
  .votes button.link::after { content: ''; position: absolute; inset: 0 -4px; }
}
.phase-rail { display: flex; gap: 0; margin: 18px 0 0; flex-wrap: wrap; border-bottom: 1px solid var(--border-strong); }
.phase-tab { flex: 1 1 0; min-width: 150px; display: flex; flex-direction: column; align-items: flex-start; gap: 4px; padding: 9px 12px 10px; background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0; margin-bottom: -1px; color: var(--muted); cursor: pointer; text-align: left; }
.phase-tab + .phase-tab { border-left: 1px solid var(--border); }
.phase-tab:hover { border-bottom-color: var(--border-strong); transform: none; }
.phase-tab.is-active { background: none; border-bottom-color: var(--accent); }
.phase-tab-top { display: flex; align-items: center; gap: 8px; }
.phase-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted-2); flex: none; }
.phase-dot.is-open { background: var(--good-fg); }
.phase-tab-name { font-family: var(--font); font-size: var(--t-sub); font-weight: 600; color: var(--muted); }
.phase-tab.is-active .phase-tab-name { color: var(--fg); }
.phase-tab-meta { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-2); }
/* No `+ .phase-section` rule: all three panels are always in the DOM (x-show only
   hides them), so it matched Running and Retrospective unconditionally and gave
   them a stray top border and 20px the Pre-Discussion panel never had — the panel's
   geometry changed with which tab you picked. The strip above bounds it now. */
.phase-section { margin-top: 0; background: none; border: 0; border-radius: 0; padding: 18px 0 0; }

/* ══════════════════════════════════════════════════════════════════════════
   THE REGISTER (ADR-0027)
   A four-column ledger — FILED · CLAIM · RESOLVES · STANDING — with one shared
   NOW→10Y axis running down the RESOLVES column. The axis is the point: a
   per-row progress bar tells you how far along one claim is, but only a shared
   scale lets you see that one claim lands in December and another in 2035.
   ══════════════════════════════════════════════════════════════════════════ */
.reg-head,
.reg-row {
  display: grid;
  /* CLAIM is the only elastic track, so the breakpoint below is set by what is left
     for it: viewport − 256px sidebar − 2×32px main padding − (112 + 210 + 124 fixed
     tracks + 3×20px gaps = 506). At 1000px that leaves CLAIM 174px — the squeeze
     this grid exists to avoid — so the columns stack at 1180px, where it gets 354px. */
  grid-template-columns: minmax(92px, 112px) minmax(0, 1fr) minmax(168px, 210px) minmax(104px, 124px);
  gap: 0 20px;
}
/* Not sticky: ADR-0029 retired the shared axis, so there is no scale for a row to
   be projected onto and nothing the head needs to stay on screen for. Each row's bar
   is self-contained. */
.reg-head {
  align-items: end;
  padding: 0 0 9px;
  border-bottom: 1px solid var(--border-strong);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted-2);
}
.reg-h-right { text-align: right; }

/* The shared axis, drawn once in the header. EVERY gradation is centred on its
   own percentage — including the NOW and 10Y ends — because the row dots are
   centred too, and a header whose end labels were edge-anchored disagreed with
   its own dots by half a label width at exactly the two positions readers check
   first. A centred end label overhangs ~11px into the grid's 20px column gap,
   which is empty on both sides. Do not inset the axis to fix the overhang: the
   header axis and the row axis must stay the same width or the two scales drift,
   which is the one thing AxisTicks exists to prevent. */
/* The shared NOW→10Y axis that used to live here is GONE (ADR-0029). It measured
   perfectly and could not be read: on a log axis anchored at today, every claim in a
   young register was filed days ago, so four bars filed across five days started
   within a 2px band. Comparability moved to the printed "149 DAYS OUT" label, which
   says it precisely and in words. Do not reintroduce gradations without reading
   ADR-0029 "Round two" first — the geometry was never the problem. */

.reg-row {
  padding: 20px 0 18px;
  border-bottom: 1px solid var(--border);
  align-items: start;
}
.reg-row:last-child { border-bottom: 0; }

/* FILED */
.reg-filed { display: flex; flex-direction: column; gap: 4px; font-family: var(--mono); font-size: var(--t-micro); line-height: 1.45; }
.reg-date { color: var(--fg); font-weight: 600; letter-spacing: 0.02em; }
/* The ellipsis guard is not optional: usernames run to 30 characters with no break
   opportunity, and the FILED track is capped at 112px, so without it a long handle
   ran straight across the gap into the claim. (.author, which this replaced,
   carried the same three declarations.) */
.reg-by { color: var(--muted); text-decoration: none; font-family: var(--font); font-size: var(--t-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.reg-by:hover { color: var(--accent); }
/* Attribution is free text up to 200 characters in a 112px column — clamp it the
   way .person-bio does, and break unbroken tokens rather than overflow. */
/* Visibility is not in ADR-0027 §4's field list — the imported design only drew a
   public register — so this is the one piece of invented design in the language. It is
   deliberately the quietest thing that works: the same mono metadata setting as the
   filed date, marking only the exception. Ruled, not filled: a private claim is not a
   warning. */
.reg-private {
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2);
  border: 1px solid var(--border); padding: 2px 6px; align-self: flex-start;
}
.reg-after {
  font-family: var(--serif); font-style: italic; font-size: var(--t-meta); color: var(--muted-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}

/* CLAIM — the one thing set in the serif */
.reg-claim-col { min-width: 0; }
/* REG-DESIGN-017 a FIFTH time, and this one was found by a false statement rather than
   a report: the first version of issue #51's fix justified `.pd-statement`'s
   overflow-wrap by saying `.reg-claim` "already breaks the same string". It did not —
   the register's serif claim had no break opportunity at all, and the six overflow-wrap
   declarations in this file never included it. Measured on the feed at 1400px, one
   67-character URL inks 53px PAST the claim column and across RESOLVES (scrollWidth 583
   against clientWidth 530); there is no `overflow` here to contain it, so unlike the
   detail page it does not even get a scrollbar — it just paints over the next column. */
.reg-claim {
  display: block;
  font-family: var(--serif);
  font-size: var(--t-claim);
  font-weight: 500;
  line-height: 1.4;
  color: var(--fg);
  text-decoration: none;
  max-width: 52ch;
  text-wrap: pretty;
  overflow-wrap: anywhere;
}
.reg-claim:hover { color: var(--accent); }
/* A settled Miss is struck from the register: form carries the verdict as well as
   the stamp's colour, so the two outcomes never rely on hue alone. A removed claim
   is struck the same way — the register annotates, it does not erase. It is NOT
   also dimmed with opacity: the old .card.anon did that, and 0.55 over --muted-2
   composites to 2.18:1, which fails AA. The struck form plus the standing pill
   already tell the two apart. */
.reg-row-removed .reg-claim,
.pred-miss .reg-claim { color: var(--muted-2); text-decoration: line-through; text-decoration-color: currentColor; text-decoration-thickness: 1px; }
/* A withheld row states its own condition, so it is set as prose, not struck. */
.reg-row-withheld .reg-claim { color: var(--muted); font-style: italic; font-size: var(--t-sub); }
/* Clamped to two lines, matching the prototype's line-clamp-2 on the card
   description and copying .reg-after's idiom above. NOT applied to .reg-claim:
   the prototype does not clamp the claim, and a 300-rune statement already
   wraps to about four lines today, which flat() leaves unchanged. */
.reg-desc {
  margin: 7px 0 0; font-size: var(--t-sub); line-height: 1.6; color: var(--muted); max-width: var(--measure);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  /* Clipped rather than escaping, so this is the mild end of REG-DESIGN-017 — but clipping a
     token MID-TOKEN when it could have wrapped first is a worse reading of the same string. */
  overflow-wrap: anywhere;
}
.reg-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; margin-top: 10px; font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
.reg-conf { letter-spacing: 0.06em; white-space: nowrap; }

/* RESOLVES */
.reg-resolves { display: flex; flex-direction: column; gap: 5px; font-family: var(--mono); }
.reg-target { font-size: var(--t-sub); font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; }
.reg-target-struck { color: var(--muted-2); text-decoration: line-through; font-weight: 500; }
.reg-target-open { color: var(--muted-2); font-weight: 500; }
.reg-days { font-size: var(--t-micro); color: var(--muted-2); letter-spacing: 0.04em; }
/* The TRACK is the claim's whole life, filed → deadline, always the full column
   width. Solid, not the dotted rule it was under the shared axis: a dotted hairline
   with a filled dot on it reads as a range input that failed to style, which is what
   "those look broken" was reporting.
   --muted-2, NOT --border-strong: the unfilled part of the track is the claim's
   REMAINING time, so it carries information and is held to the 3:1 non-text floor.
   --border-strong measured 1.79:1 on --bg in the /chrome pass — it is the decorative
   hairline token and belongs on rules that mean nothing. */
.reg-axis { position: relative; display: block; height: 9px; margin-top: 6px; }
.reg-axis::before { content: ""; position: absolute; left: 0; right: 0; top: 4px; border-top: 1px solid var(--muted-2); }
/* The fill is how far through its life the claim is, and its LEADING EDGE IS TODAY
   — which is why there is no separate today marker: it would restate the same x
   twice. 2px against the track's 1px is what separates "this claim" from "the
   scale". y is 3.5..5.5 so its centre line is the dot's centre (4.5). */
.reg-fill { position: absolute; top: 3.5px; left: 0; height: 2px; background: var(--accent); }
/* The deadline end. Without it the track just stops, and a long grey remainder
   ending at nothing is close to the truncated look that started this whole thread.
   Taller than the 1px track rule so it reads as a cap rather than as more track. */
.reg-cap { position: absolute; right: 0; top: 1px; bottom: 1px; width: 1px; background: var(--muted-2); }
.reg-dot { position: absolute; top: 0; width: 9px; height: 9px; margin-left: -4.5px; border-radius: 50%; background: var(--accent); }

/* STANDING */
.reg-standing { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; font-family: var(--mono); }
/* display + font-family are what make these two reusable off the register: the mono
   comes from .reg-standing's parent, and a rotate() on a display:inline box is a
   no-op per spec — inside the flex parent the span is blockified, anywhere else it
   is not. The detail page borrows both rather than inventing a third vocabulary. */
.reg-pill {
  display: inline-block; font-family: var(--mono);
  font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.12em;
  color: var(--muted); border: 1px solid var(--border-strong); padding: 5px 9px; white-space: nowrap;
}
/* A comment phase's state is a standing, so it is a pill too. */
.reg-pill.is-open { color: var(--good-fg); border-color: currentColor; }
/* EXPIRED is a standing, not a verdict — nobody resolved it — so it stays a pill
   rather than a stamp. But it must not read as one more pending row: the target
   passed unanswered, which is the one unresolved state worth noticing. */
.reg-pill.status-EXPIRED { color: var(--warn-fg); border-color: currentColor; }
/* The verdict is a rubber stamp — struck at a slight angle, the way a records
   office annotates a closed case. Only Hit and Miss are stamped, so there is no
   stamp-expired: it would be a rule nothing can ever match. */
.reg-stamp {
  display: inline-block; font-family: var(--mono);
  font-size: var(--t-micro); font-weight: 600; letter-spacing: 0.1em;
  border: 2px solid currentColor; padding: 6px 9px; white-space: nowrap;
}
.stamp-hit { color: var(--good-fg); transform: rotate(-2deg); }
.stamp-miss { color: var(--bad-fg); transform: rotate(1.5deg); }

/* Below the ledger's comfortable width the columns stack into one entry. The column
   WORDS have nothing to head once the columns stack, so they go — and since ADR-0029
   retired the shared axis there is no tick scale left to preserve, which is why the
   whole head collapses to nothing here. Each row's bar carries its own meaning. */
@media (max-width: 1180px) {
  /* The whole head goes, not just its words. Hiding the four .reg-h spans left the head
     BOX behind — zero content, 6px of padding and a --border-strong bottom rule — which
     painted a stray 7px strip above the register. It was masked while .reg-h-axis was
     still shown here; retiring the axis exposed it. */
  .reg-head { display: none; }
  .reg-row { grid-template-columns: 1fr; gap: 10px; padding: 18px 0 16px; }
  .reg-standing { flex-direction: row; align-items: center; justify-content: flex-start; gap: 14px; }
  .reg-resolves { flex-direction: row; align-items: baseline; flex-wrap: wrap; gap: 10px; }
  .reg-axis { flex: 1 1 100%; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE LANDING (ADR-0027 §4 — plan Task 8)
   The signed-out first impression: the register's masthead, ONE closed case drawn
   on its own span, then the live register. No counters (§4). These rules sit AFTER
   the register block on purpose — .land-axis::before overrides .reg-axis::before at
   EQUAL specificity, so source order is what decides. Do not reorder this file "by
   section": the span would silently revert to a dotted --muted-2 rule.
   The 11px type floor is asserted from the "THE REGISTER" marker to EOF, which
   includes this block, so every size here is a token.
   ══════════════════════════════════════════════════════════════════════════ */
.land-mast { margin: 0 0 24px; padding-bottom: 14px; border-bottom: 3px double var(--border-strong); }
.land-name {
  margin: 0; font-family: var(--serif); font-size: var(--t-display); font-weight: 500;
  line-height: 1.05; color: var(--fg);
}
.land-sub {
  margin: 9px 0 0; font-family: var(--mono); font-size: var(--t-meta); font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2);
}
/* The case is set as a register ENTRY, not a hero: same rules, same mono metadata,
   the same Hit stamp the feed draws — so the landing teaches the register's
   vocabulary before the reader reaches the first live row. No fill and no card
   (ADR-0026 §3: no white behind content), and no bottom rule, because the tab strip
   immediately below already draws one and a fourth hairline inside 60px reads as
   noise rather than as structure. */
.land-case { margin: 0 0 26px; }
.land-case-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 12px; }
.land-case-label {
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-2);
}
.land-prose {
  margin: 0; font-family: var(--serif); font-size: var(--t-claim); line-height: 1.55;
  color: var(--fg); max-width: var(--measure); text-wrap: pretty;
}
/* First consumer of the Spectral italic 400 face outside .reg-after. */
.land-prose em { font-style: italic; font-weight: 400; }
.land-span { display: flex; align-items: center; gap: 12px; margin-top: 20px; max-width: var(--measure); }
.land-year { font-family: var(--mono); font-size: var(--t-sub); font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; }
.land-axis { flex: 1 1 auto; height: 11px; margin: 0; }
/* Solid, not dotted: the register's dotted rule means "the unresolved future", which
   is the one thing a case closed in 1903 is not. */
.land-axis::before { top: 5px; border-top: 1px solid var(--accent); }
/* Finer than a row dot and centred on the 1px rule above (dot y 2-9 centres on 5.5;
   rule y 5-6 centres on 5.5). */
.land-dot { top: 2px; width: 7px; height: 7px; margin-left: -3.5px; }
.land-elapsed {
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-2); white-space: nowrap;
}
/* At phone width the display tier is too tall for the first screen, and the span's
   two dates plus its rule cannot share a line. */
@media (max-width: 600px) {
  .land-name { font-size: var(--t-h1); }
  .land-prose { font-size: var(--t-body); }
  .land-span { flex-wrap: wrap; }
  .land-axis { flex: 1 1 100%; order: 3; }
}


/* ── Registration confirmation ── the success state that replaces the signup form.
   Deliberately NOT `.hint`: the defect this fixes was that success rendered in the app's
   quietest type at the bottom of a still-live form while the error rendered in red at the
   top, so the loudest thing after a successful signup was the button that would fail. It
   is ruled like the register's own sections rather than filled — a confirmation is not a
   warning. ── */
.auth-done { border-top: 2px solid var(--border-strong); padding: 16px 0 4px; }
.auth-done-head {
  font-family: var(--mono); font-size: var(--t-meta); font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); margin: 0 0 8px;
}
.auth-done-body { margin: 0; font-size: var(--t-body); line-height: 1.5; color: var(--fg); }
.auth-done-body strong { font-family: var(--mono); font-size: var(--t-sub); color: var(--fg); }
.auth-done-note { margin: 10px 0 0; font-size: var(--t-meta); color: var(--muted); }
.auth-done-note form { display: inline; }

/* ══════════════════════════════════════════════════════════════════════════
   THE NOTICE BOARD (ADR-0062, ADR-0063)
   ONE node, three shapes, chosen entirely here. The element renders once, from
   inside FeedShell between the tune slot and the register head — which IS the
   strip shape, so the whole 768–1500 band is the base case below and needs no
   query at all. The two ends each take one.

   WHERE THE RAIL BEGINS, and why 1500 and not the artboards' ~1560:

     viewport   device                     content¹  register²  claim³
     1366       common Windows laptop        1046       714       208
     1440       MacBook Air (legacy)         1120       788       282
     1470       MacBook Air 13" M2/M3        1150       818       312
   → 1500                                    1180       848       342
     1512       MacBook Pro 14"              1192       860       354
     1560       (the artboards' number)      1240       908       402
     1728       MacBook Pro 16"              1368      1036       530
     1920+      external display             1368      1036       530

     ¹ min(W − 256, 1432) − 64 main padding, which is INSIDE the max-width.
     ² content − 300 rail − 32 gutter.  ³ register − 506 (the three fixed reg
     columns at their maxima, plus gutters).

   MEASURED by scripts/chrome-board.cjs: 1500 → 342, 1512 → 354, 1728+ → 530.
   The last figure is why 1432 is the right cap: with no board main caps at 1100
   and the register gets 1100 − 64 = 1036; with a board it gets 1432 − 64 − 332 =
   1036. The same 1036, to the pixel — the rail is paid for by the extra width and
   the register never surrenders a column to it.

   At 1560 no MacBook below the 16" ever sees the rail — the 14" Pro is 1512 and
   the Air 1470 — so the feature would be invisible on the machines most likely
   to be running it. 1500 is the lowest number that captures the 14" Pro while
   keeping the claim within 15% of the artboards' own design point. It is ONE
   number in ONE place: change it here and the three shapes follow.

   300px, 32px and top:24px are .pd-grid/.pd-rail's own numbers, reused rather
   than reinvented; 1432 = 1100 + 32 + 300 is what lets the rail be ADDITIVE, so
   the register keeps its full measure instead of surrendering 332px of it.

   DO NOT render the body twice. Breaks mode draws its line breaks with
   `white-space: pre-line`; the strip overrides that one property with `nowrap`,
   which collapses the newlines to spaces — i.e. to exactly what Flat would have
   produced. One string, one node, one property per mode.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── base = the STRIP (768.02–1500px) ── content only, on one line ── */

/* The head is the admin's, and it is hidden here because the strip has no room
   for it (ADR-0063 §5). `display: none` takes it out of the tab order too, which
   is the point: /admin → Notice board is the canonical management surface at
   exactly these widths, so nothing is unreachable — a convenience simply stops
   being offered where it does not fit. Same regime for the empty state, which
   would otherwise draw an admin-only band over every laptop register. */
/* `contents`, not `none`: the date lives in the head now (it read as a stray fragment
   trailing the body), and the strip still has to show it. `display: contents` removes the
   head's BOX while leaving its children in the flow, so the date becomes an ordinary flex
   item of `.board` below and lands at the end of the strip line. The eyebrow and the admin
   controls are what the strip has no room for, so those go individually.
   `.board-item` deliberately does NOT do this — it must stay a real box to keep the
   border-left the two kinds are told apart by. */
/* SCOPED TO `.board`, all of it. `.board-eyebrow` is also boardPanelRow's label class, so an
   unscoped hide rule here blanked the /admin panel's "Public notice" / "Sponsor" labels for
   the whole 769–1499px band — on the surface ADR-0062 §5 designates canonical precisely
   BECAUSE the rail's controls do not fit at those widths. Same class as the `.board-actions`
   leak BS-C7b caught, one page over. The typographic rules further down stay unscoped on
   purpose: sharing the ink is intended, sharing the layout is not. */
.board .board-head { display: contents; }
.board .board-eyebrow,
.board .board-actions,
.board .board-empty { display: none; }
/* .board is the strip's flex row, so the head's surviving child sits on the same line as the
   item. order: 1 puts the date after the claim rather than in front of it. */
.board { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
/* DESCENDANT, not child. `.board-date` is a child of `.board-head`; `display: contents` on the
   head removes its BOX and promotes the date to a flex item of `.board`, but it does not move
   the element, and the child combinator matches on the element tree. `.board > .board-date`
   therefore never matched at all, `order` stayed 0, and the date laid out in DOM order — so
   every strip read "21 Aug 2026 ▏Title — body…", the exact opposite of what this rule says it
   does. `.board-panel` is not inside `.board`, so the panel's own date is untouched. */
/* SCOPED TO THE STRIP by resetting it in both shapes below — this rule only means anything
   while `.board` is the flex row and the head is `display: contents`. Left unscoped it followed
   the date into the head's OWN flex context at ≤768 and ≥1500, where `justify-content:
   space-between` then laid out eyebrow → Edit Remove → date and stranded the controls mid-rule. */
.board .board-date { order: 1; }
.board .board-item { flex: 1; }

/* The rule lives on the ITEM, not on the container. An empty .board therefore
   draws nothing at all — no stray 1.5px accent bar above the register at the
   widths where the head is hidden — and the container is free to be a grid item
   in the rail without its border coming along. */
.board-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  padding: 7px 0 7px 10px;
  border-left: 1.5px solid var(--accent);
}
/* Dashed and unaccented: sponsor content must never be mistakable for the
   register's own voice, and the difference has to survive being seen at a
   glance in a one-line strip. */
.board-item.board-sponsor { border-left-style: dashed; border-left-color: var(--muted-2); }

.board-title {
  /* SHRINKABLE, and bounded. `flex-shrink: 0` was the strip's only shape that did not
     restore it, and a character cap does not bound a pixel width: `.board-item` has
     `min-width: 0` and no `overflow`, so an 80-character title overflowed the flex line, then
     `main`, then the document — a horizontal scrollbar on the home register at 769–1024px.
     The 50% cap keeps the body a share instead of letting it collapse to zero first. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 50%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* REG-DESIGN-017, and INERT IN STRIP MODE — which is the point. This selector was exempt from
     the derived break guard on the reason "same strip-mode containment", and both other shapes
     revert exactly that: the card (<=768px) and the rail (>=1500px) each set `max-width: none;
     overflow: visible; text-overflow: clip; white-space: normal`. `white-space: normal` contains
     ordinary text BY WRAPPING, which is the one thing an unbreakable token cannot do, and
     `overflow: visible` removes the fallback — so a title carrying a URL overflowed the item at
     both ends of the range and inside the 300px rail. `.board-item`'s comment above records this
     failing once already, for a character cap. Where `nowrap` holds, this declaration cannot fire. */
  overflow-wrap: anywhere;
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.35;
  color: var(--fg);
}
/* white-space is the ONLY property that changes between the three shapes. The
   overflow/ellipsis pair is harmless once the text wraps (nothing overflows a
   wrapping line), so it is declared once here rather than toggled twice.
   overflow-x, NOT overflow: the derived Breaks bound above sets overflow-y:auto
   on this selector, and the shorthand would silently take it back. */
.board-body {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--t-sub);
  line-height: 1.55;
  color: var(--muted);
  white-space: nowrap;
  /* Reverted in both shapes below. `.prose` sets `overflow-x: auto` (ADR-0054 §4 chose
     contained-not-clipped) and this beats it at every width unless undone: ~200 nested
     blockquotes at 12px of inset each, a long unbroken URL, or a wide code fence was CLIPPED
     with no scrollbar inside a 300px rail or a 390px card — unreachable, not merely ugly. */
  overflow-x: hidden;
  text-overflow: ellipsis;
  /* NO `text-wrap: pretty` HERE, and this is not a style preference.
     `text-wrap` is a sub-property of the `white-space` shorthand: `white-space: nowrap`
     sets text-wrap-mode to nowrap, and a later `text-wrap: pretty` sets it back to WRAP.
     It did — measured at 1366px, an 80/400 notice rendered a 42px body, two line boxes,
     inside a strip whose whole budget is one. The ellipsis still engaged, so the strip
     looked plausible and was a third taller than the design. Nothing in Go can see this;
     the /chrome layer caught it (BS-B12). `pretty` belongs only where the body actually
     wraps — the card and the rail below, after their `pre-line`. */
}
.board-body::before { content: "— "; }
/* Rich emits BLOCK elements (ADR-0064 §5) and a one-line strip cannot hold paragraphs. Run
   them inline so the wrapper's nowrap + text-overflow keeps doing what it already did; the
   hard-wrap <br>s go with them. Both are reset in the card and rail, where the body wraps
   and the blocks are the point. `.prose` sets overflow-x: auto for its own scrollers — the
   strip needs `hidden` or the ellipsis never engages, and this block is later in the file at
   equal specificity, which is what decides it. BS-B12 measures the ellipsis rather than
   trusting that ordering. */
/* ALL descendants, not just children. `> *` inlined the <ul> and left its <li> at
   `display: list-item`, which is block-level and forces anonymous block boxes inside the
   inline parent — a three-bullet notice measured 137px against a 25px budget, with
   `text-overflow` no longer applying at all once the container has block-level children.
   `white-space: inherit` is what brings <pre> under the strip's nowrap. */
.board-body *,
.board-body > * { display: inline; margin: 0; padding: 0; white-space: inherit; list-style: none; }
.board-body br { display: none; }

.board-tag,
.board-date {
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted-2);
}
.board-tag { font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.board-link {
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.board-link:hover { color: var(--accent-light); }

/* ── the head, wherever it is shown ── */
.board-head {
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 10px;
  border-bottom: 3px double var(--border-strong);
}
.board-eyebrow {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted-2);
}
/* NO `display` here. This rule sits AFTER the strip's `.board-actions { display: none }`
   at equal specificity, so declaring one silently un-hid the admin controls at every strip
   width — Remove came back into the tab order at 1366px with the head itself still hidden,
   which BS-C7b caught. The display belongs with the shapes that show the head. */
.board-actions { gap: 14px; }
.board-act {
  cursor: pointer;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.board-act:hover { background: none; color: var(--accent-light); transform: none; }
/* Removal is the destructive one, so it is inked --warning on hover rather than
   sharing the accent every other control uses. Not painted --error at rest: the
   board is not in an error state for having a notice on it. */
.board-remove { color: var(--muted-2); }
.board-remove:hover { background: none; color: var(--warn-fg); }

/* MEASURED, both themes, against --bg (the ground each actually sits on):
   --accent rule 7.42 / 6.67, --muted-2 rule 5.20 / 5.07 — both clear the 3:1
   non-text floor. The dashed box below is --border at 1.39 / 1.31, which does
   NOT and is not meant to: it is the register's own hairline token used
   decoratively, and the meaning here is carried by the words inside it, which
   are --muted-2 at 5.20 / 5.07. Recorded so the number is explained rather than
   rediscovered as a defect. */
.board-empty {
  margin-top: 14px;
  padding: 20px 16px;
  border: 1px dashed var(--border);
  text-align: center;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--muted-2);
}

/* ── the CARD (≤768px) ── above the tabs, stacked ── */
@media (max-width: 768px) {
  /* `.feed` carries NO display rule of its own, so it is a block container and
     its children have no `order` — a bare `order: -1` here would have been
     silently inert and the card would have rendered BELOW the tabs at every
     phone width with nothing failing. Gated on .board-item like its rail twin
     (ADR-0063 §2), so a page with nothing posted keeps today's block flow and
     today's margin collapsing exactly. */
  main:has(.board-item) .feed { display: flex; flex-direction: column; }
  main:has(.board-item) .board { order: -1; }

  .board { display: block; }
  .board .board-head { display: flex; }
  .board .board-eyebrow { display: revert; }
  .board .board-actions { display: inline-flex; }
  .board .board-empty { display: block; }
  .board .board-item { flex-direction: column; align-items: stretch; gap: 5px; margin-top: 14px; padding: 2px 0 2px 10px; border-left-width: 2px; }
  .board-title { flex: 0 1 auto; max-width: none; overflow: visible; text-overflow: clip; white-space: normal; }
  .board-body { flex: 0 1 auto; white-space: normal; text-wrap: pretty; overflow-x: revert; text-overflow: clip; }
  .board-body::before { content: none; }
  /* BOTH selectors and ALL five longhands. Reverting only `> *`, and only display+margin, left
     every NESTED element inlined and unpadded outside the strip: an <li> inside a <ul> kept
     `display: inline` with no marker, so a bulleted notice — legal content per ADR-0064 §1 —
     rendered as run-together text in the rail and on a phone. */
  .board-body *,
  .board-body > * { display: revert; margin: revert; padding: revert; white-space: revert; list-style: revert; }
  .board-body br { display: revert; }
  .board .board-date { order: revert; }
  .board-link,
  .board-date { align-self: flex-start; }
  /* The touch floor the phone shell asserts everywhere else. Padding rather
     than min-height alone so the underline stays on the word. */
  .board-act { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ── the RAIL (≥1500px) ── beside the register, sticky ── */
@media (min-width: 1500px) {
  /* The head and the empty state come back at rail width even without an item:
     an admin with nothing posted still needs somewhere to post FROM. Without an
     item there is no grid (below), so this renders in flow above the register —
     a modest band rather than a 300px column standing empty. */
  .board { display: block; }
  .board .board-head { display: flex; }
  .board .board-eyebrow { display: revert; }
  .board .board-actions { display: inline-flex; }
  .board .board-empty { display: block; }

  /* Widening and grid share the .board-item gate deliberately. Where :has() is
     unsupported (Chrome 105+/Safari 15.4+/Firefox 121+, see the drawer's note
     above) BOTH fail to parse together and the board degrades to the strip —
     a shape the design already specifies. A split gate would degrade instead to
     a 300px rail crushing the claim to 262px. */
  main:has(.board-item) { max-width: 1432px; }
  /* ...but only `.feed`'s children are placed into the grid below, so every other page-level
     block in `main` — the signed-out landing's masthead and case, above the register — would
     simply stretch the extra 332px and sit wider than the register they introduce. 1036px is
     what those blocks get when `main` is at its ordinary 1100px cap, padding excluded. */
  main:has(.board-item) > :not(.feed) { max-width: 1036px; }
  main:has(.board-item) .feed {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    column-gap: 32px;
    grid-template-areas: "tabs board" "zero board" "head board" "list board";
  }
  main:has(.board-item) .feed-tabs { grid-area: tabs; }
  main:has(.board-item) #tune-zero-slot { grid-area: zero; }
  main:has(.board-item) .reg-head { grid-area: head; }
  main:has(.board-item) #feed-region { grid-area: list; }
  /* align-self:start keeps the item its content's height inside a grid area that
     spans all four rows — a stretched item has nothing to stick within. */
  main:has(.board-item) .board { grid-area: board; position: sticky; top: 24px; align-self: start; }

  .board .board-item { flex-direction: column; align-items: stretch; gap: 6px; margin-top: 16px; padding: 0 0 2px 12px; border-left-width: 2px; }
  .board-title { flex: 0 1 auto; max-width: none; overflow: visible; text-overflow: clip; white-space: normal; }
  .board-body { flex: 0 1 auto; white-space: normal; text-wrap: pretty; overflow-x: revert; text-overflow: clip; }
  .board-body::before { content: none; }
  /* BOTH selectors and ALL five longhands. Reverting only `> *`, and only display+margin, left
     every NESTED element inlined and unpadded outside the strip: an <li> inside a <ul> kept
     `display: inline` with no marker, so a bulleted notice — legal content per ADR-0064 §1 —
     rendered as run-together text in the rail and on a phone. */
  .board-body *,
  .board-body > * { display: revert; margin: revert; padding: revert; white-space: revert; list-style: revert; }
  .board-body br { display: revert; }
  .board .board-date { order: revert; }
  .board-link,
  .board-date { align-self: flex-start; }
}

/* ── the composer (ADR-0062 §5) ── the .modal chrome, plus a form's own rhythm.
   .np-field / .np-label / .ghost are the filing form's, reused rather than
   restated: a second set of field styles is how two forms in one product start
   disagreeing about what an input looks like. ── */
/* `width`, not `max-width`. `.modal` declares `width: min(440px, 92vw)`, so a 520px ceiling was
   inert at every viewport — the composer rendered 440px and never the width this rule states.
   `.tune-modal` is the sibling that got this right. Three stacked fields plus a link row and a
   help line are why it was sized wider in the first place. */
.board-modal { width: min(520px, 92vw); max-width: none; }
.board-modal-head { margin: 0 0 14px; font-family: var(--serif); font-size: var(--t-h2); font-weight: 500; }
.board-modal form { display: flex; flex-direction: column; gap: 6px; }
.board-modal .np-label { margin-top: 8px; }
.board-modal .np-help { margin: 2px 0 0; }
.board-modal-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }

/* ── /admin → Notice board (ADR-0062 §5) ── the canonical management surface ── */
.board-panel-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 12px 0; border-bottom: 1px solid var(--border); }
.board-panel-what { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.board-panel-acts { display: inline-flex; gap: 14px; flex-shrink: 0; }
.board-panel-note { font-family: var(--mono); font-size: var(--t-micro); color: var(--muted-2); }
/* The /admin copy of the board title, and until round four it appeared in this stylesheet exactly
   once — in the bidi-isolation list. It renders at board.templ:220 as a bare <strong> flex item of
   `.board-panel-what`, so "it inherits the title's containment" (the reason it was exempt from the
   break guard) named containment that does not exist anywhere: no overflow, no max-width, no
   ellipsis. A notice titled with a URL overflowed the row on the surface ADR-0062 §5 designates
   canonical. */
.board-panel-title { overflow-wrap: anywhere; }
