/* MediaWipeTool — "atomic-age privacy tooling"
 *
 * Written fresh rather than forked, so the deployed stylesheet is not
 * source-diffable against the app this codebase descends from.
 *
 * COLOUR RULES ARE CONTRAST RULES. All ratios below are measured against
 * Graphite Black #0A0B0B and none of them survive a lighter surface — if you
 * introduce a lighter background, re-measure before reusing these tokens.
 *
 * THAT ALREADY HAPPENED. As of 2026-07-29 every template sets `page-light` and
 * these dark values are what the tokens fall back to, not what any page ships.
 * The live values, and a re-measured ratio for every pair, are in the LIGHT
 * SURFACE block at the end of this file. Read that one first.
 *
 *   --cream        ~15:1   all body copy. Safe everywhere.
 *   --orange       ~4.8:1  passes AA for normal text, but only just. Headings,
 *                          links, accents. Never long paragraphs.
 *   --civil-red    ~2.9:1  FAILS AA even for large text. Fill / rule / chip
 *                          background / icon ONLY. Never text on black.
 *   --steel        ~3.1:1  FAILS for body text. Borders, dividers and large
 *                          labels only (the tagline is large-label use).
 *   --alert-red    ~5.3:1  the ONLY red permitted as text on black. Errors and
 *                          failure states, so red text never has to fail
 *                          contrast to stay on-brand.
 *   --federal-blue         informational banner fills. Too dark for text here.
 */

/* ─── Fonts: self-hosted, no exceptions ───────────────────────────────────────
 * The deployed Caddyfile sets default-src 'self'. A fonts.googleapis.com link
 * is blocked outright and the page silently falls back to system fonts, which
 * looks like a CSS bug and is not one. Beyond that, linking Google Fonts leaks
 * every visitor's IP to Google on page load — indefensible on a privacy tool.
 *
 * font-src is not declared in the CSP and falls back to default-src 'self', so
 * these already pass. Do NOT widen the Caddyfile to "fix" fonts.
 *
 * See static/fonts/README.md for exactly which files to fetch. Until they are
 * in place the stacks below fall back to system faces and the app still works.
 */
/* OSWALD AND BARLOW SEMI CONDENSED WERE REMOVED 2026-08-04, with their three
 * woff2 files. They were the dark surface's display and body faces. The light
 * surface took over every template on 2026-07-30 and re-values --font-head and
 * --font-body to IBM Plex Sans, so from that day nothing rendered in either
 * face - verified by reading the computed font-family of every element on all
 * sixteen pages, which returns only IBM Plex Sans and IBM Plex Mono.
 *
 * They cost visitors nothing, because @font-face loading is lazy and no rule
 * matched. What they cost was truthfulness: `--font-head: "Oswald"` read as
 * though the headings were Oswald, and 56.6 KB shipped in every deploy.
 *
 * If a dark surface is ever rebuilt, add the faces back HERE and re-add the
 * files; do not resurrect them by pointing a token at a face with no
 * @font-face, which fails silently to a system fallback. */
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/static/fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --graphite: #0a0b0b;
  --panel: #131414;
  --panel-alt: #1b1c1c;
  --border: #2e3133;
  --border-hover: #5c6066;

  --cream: #e6e0d3;
  --cream-dim: #a8a294;
  --orange: #e34222;
  --civil-red: #b7161c;
  --alert-red: #e5504a;
  --steel: #5c6066;
  --federal-blue: #1f4a68;

  /* Same stack as the light surface below, since the two faces these used to
     name were removed with their files. Kept as tokens rather than deleted:
     roughly thirty rules read them, and a token that resolves to the real
     typeface is safer than one that resolves to nothing. */
  --font-head: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-body: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", "Consolas", monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--graphite);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.62;
  position: relative;
  /* clip, not hidden: stops horizontal scroll from the decorative chrome
     without creating a scroll container, which would break position:sticky. */
  overflow-x: clip;
  /* halftone ink texture */
  background-image: radial-gradient(rgba(230, 224, 211, 0.03) 1px, transparent 1px);
  background-size: 6px 6px;
}

a { color: var(--orange); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 {
  font-family: var(--font-head);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin: 0 0 12px;
}
h1 { font-size: 22px; }
h2 { font-size: 15px; margin: 32px 0 14px; }

/* ─── Decorative chrome — radar rings + hazard stripes ───────────────────── */

.radar-rings {
  position: fixed;
  top: -150px; right: -150px;
  width: 380px; height: 380px;
  border-radius: 50%;
  z-index: 0;
  pointer-events: none;
  opacity: 0.45;
  background: repeating-radial-gradient(
    circle,
    transparent 0, transparent 19px,
    rgba(230, 224, 211, 0.07) 20px, transparent 21px
  );
}

/* Descending "wipe" bars — the kit motif, used as quiet page furniture. */
.wipe-bars {
  position: fixed;
  bottom: 0; left: 0;
  width: 190px; height: 130px;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background:
    linear-gradient(var(--civil-red), var(--civil-red)) 0 0 / 180px 8px no-repeat,
    linear-gradient(var(--civil-red), var(--civil-red)) 0 20px / 140px 8px no-repeat,
    linear-gradient(var(--civil-red), var(--civil-red)) 0 40px / 100px 8px no-repeat,
    linear-gradient(var(--civil-red), var(--civil-red)) 0 60px / 62px 8px no-repeat,
    linear-gradient(var(--civil-red), var(--civil-red)) 0 80px / 30px 8px no-repeat;
}

/* ─── Header + wordmark lockup ────────────────────────────────────────────── */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 20px;
  border-bottom: 2px solid var(--orange);
  background: linear-gradient(180deg, #141515 0%, var(--graphite) 100%);
  flex-wrap: wrap;
}

.brand-icon { width: 22px; height: 22px; color: var(--orange); flex-shrink: 0; }

/* The lockup is the wordmark and its rule. The tagline sits OUTSIDE it — see
   .brand-tagline. Never box, badge or letter-lock the tagline to the mark. */
.brand-lockup { display: flex; align-items: center; gap: 10px; }

.wordmark {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--cream);
  line-height: 1;
  padding-bottom: 4px;
  border-bottom: 2px solid var(--orange);   /* the rule under the wordmark */
  white-space: nowrap;
}
/* Optical gap between the two halves of the wordmark.
   Done with margin, NOT a literal space in the markup: the mark stays the
   single token MEDIAWIPETOOL, so it still matches the domain when copied,
   read aloud by a screen reader, or indexed. Only the rendering separates. */
/* 0.4em ≈ one and a half word-spaces: unmistakably deliberate rather than a
   near-collision, while still reading as one lockup. In em so it scales with
   the smaller mobile wordmark without a second rule. */
.wordmark .tool { color: var(--orange); margin-left: 0.4em; }

/* THE TRADEMARK NOTICE. Declared on the BASE `.wordmark` rather than under
   `.page-light`, so it holds on any surface the lockup is ever rendered on
   rather than having to be restated per theme.
 *
 * `vertical-align: super` WITHOUT a font-size reduction would inherit the
 * wordmark's 19px and raise a full-size glyph above the cap line, which reads
 * as a mistake. Half size is what makes it a notice rather than a character in
 * the name.
 *
 * `letter-spacing: normal` cancels the wordmark's -0.01em tracking: negative
 * tracking on a two-letter superscript closes it into a smudge at 10px.
 *
 * `line-height: 0` keeps the raised glyph from growing the line box, which is
 * what would push a sticky header onto a second row. */
.wordmark .tm {
  font-size: 0.52em;
  vertical-align: super;
  line-height: 0;
  letter-spacing: normal;
  font-weight: 500;
  margin-left: 0.06em;
  /* --steel is what this system means by "subordinate", and it is already the
     tagline's colour for the same reason. Inheriting the wordmark's ink put a
     full-strength dark glyph immediately after the blue "Tool", which read as
     a third colour in a two-colour lockup. Defined on both surfaces. */
  color: var(--steel);
}
a.brand-lockup { color: inherit; text-decoration: none; }
a.brand-lockup:hover { text-decoration: none; }

/* "Reddit" is a TAGLINE, never part of the logo. It must stay visibly
   subordinate in size and weight, and must remain removable without the brand
   breaking. Steel Gray on purpose: Reactor Orange sits close enough to
   Reddit's own orange that pairing it with the word "Reddit" weakens the
   distinction this product depends on. */
.brand-tagline {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--steel);
  line-height: 1;
  align-self: flex-end;
  padding-bottom: 5px;
}

.topbar-spacer { margin-left: auto; }
.topbar-user {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.05em;
  color: var(--cream-dim);
  text-transform: uppercase;
}

/* ─── Layout ──────────────────────────────────────────────────────────────── */

main { position: relative; z-index: 1; flex: 1 0 auto; width: 100%; }
.wrap { max-width: 780px; margin: 48px auto; padding: 0 20px; }
.wrap-narrow { max-width: 380px; margin: 80px auto; padding: 0 20px; }
.wrap-wide { max-width: 940px; margin: 40px auto; padding: 0 20px; }

.lede { color: var(--cream); font-size: 16px; margin: 0 0 28px; }
.muted { color: var(--cream-dim); font-size: 13px; }

/* ─── Buttons ─────────────────────────────────────────────────────────────── */

button, .btn {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 2px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--cream);
  padding: 9px 16px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  /* cut corner — safety-paint detail */
  clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
}
button:hover, .btn:hover { border-color: var(--border-hover); text-decoration: none; }
button[type="submit"], .btn-primary {
  background: var(--orange);
  border-color: var(--orange);
  color: #17110c;
}
button[type="submit"]:hover, .btn-primary:hover { background: #ff5533; border-color: #ff5533; }
button:disabled, .btn[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }

/* Busy state for a long-running submit. The button keeps a readable label and
   gains a spinner; it deliberately does NOT get the 0.4 opacity above, because
   a faded-out control reads as "unavailable" when what we mean is "working".
   Progress lives in .scan-status; see scan.js. */
form.is-busy button:disabled { opacity: 1; cursor: progress; }
form.is-busy button:disabled::after {
  content: "";
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-left: 0.5em;
  vertical-align: -0.05em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: mw-spin 0.7s linear infinite;
}
@keyframes mw-spin { to { transform: rotate(360deg); } }

/* Someone who has asked not to see motion still needs to know it is working:
   keep the ring, stop it turning. */
@media (prefers-reduced-motion: reduce) {
  form.is-busy button:disabled::after { animation: none; }
}

/* Retry cooldown. Held for RETRY_COOLDOWN_SECONDS after an archive was asked,
   so an impatient click cannot become a request per click.

   NOT the 0.4 disabled opacity: "wait a moment" is a different statement from
   "unavailable", the same distinction the busy state above draws. The label
   already counts down in text, which is what carries the meaning; the bar is a
   second reading of the same fact and never the only one.

   The bar's duration is the constant, so it is only attached when the FULL
   window remains. Mid-window (a refresh two seconds in) the text still reads
   correctly and the bar simply does not run, rather than animating a duration
   that would be a lie. Setting it per-element would need an inline style, which
   this site's CSP blocks. */
button.is-cooling:disabled { opacity: 1; cursor: wait; }
/* Border softening is for the OUTLINE buttons only. The main Scan again button
   is a filled primary and recolouring its border reads as a different control
   rather than the same one waiting. */
.btn-ghost.is-cooling:disabled { border-color: var(--border-hover); }
button.is-cooling::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--orange);
  transform-origin: left center;
  animation: mw-cooldown 5s linear forwards;
}
button.is-cooling { position: relative; overflow: hidden; }
@keyframes mw-cooldown { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* Reduced motion: keep the bar as a static rule so the control still reads as
   held, and let the ticking number do the work. */
@media (prefers-reduced-motion: reduce) {
  button.is-cooling::after { animation: none; opacity: 0.5; }
}

/* ─── Live scan progress ──────────────────────────────────────────────────── */
/* Deliberately NO determinate bar. We do not know how many pages an account
   has until we have walked them: MAX_PAGES is a cap, not an expectation, so a
   percentage would read 20% for an account that finishes in three pages and
   sit at 100% while a slow source is still going. The sweep below says
   "working", which is all we honestly know. */
.scan-status {
  margin-top: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  background: var(--panel);
  position: relative;
  overflow: hidden;
}
.scan-status::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  height: 2px;
  width: 35%;
  background: var(--orange);
  animation: mw-sweep 1.6s ease-in-out infinite;
}
@keyframes mw-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(340%); }
}
.scan-status-summary { font-family: var(--font-mono); font-size: 13px; margin-bottom: 8px; }
.scan-status-row {
  display: flex;
  gap: 10px;
  align-items: baseline;
  font-family: var(--font-mono);
  font-size: 12px;
  padding: 3px 0;
}
.scan-status-name { min-width: 9em; color: var(--cream); }
.scan-status-state { flex: 1; color: var(--cream-dim); }
.scan-status-count { color: var(--cream); }
.scan-state-partial { color: #e3a33e; }
.scan-state-failed { color: var(--alert-red); }
/* Not asked during this scan. Deliberately the QUIETEST row in the panel and
   in no accent colour: it is not a finding and not a fault, it is last scan's
   figure sitting there while another archive is read. */
.scan-state-cached { color: var(--cream-dim); font-style: italic; }
.scan-status-note { font-size: 12px; margin-top: 8px; }

@media (prefers-reduced-motion: reduce) {
  .scan-status::before { animation: none; width: 100%; opacity: 0.35; }
}
.btn-danger { border-color: var(--civil-red); color: var(--alert-red); }
.btn-danger:hover { background: #241012; border-color: var(--alert-red); }
.btn-ghost { background: transparent; }

/* ─── Forms ───────────────────────────────────────────────────────────────── */

label {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--cream-dim);
  margin-bottom: 6px;
}

input[type="text"], input[type="password"], input[type="number"], select {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--cream);
  padding: 9px 10px;
  border-radius: 2px;
  /* 16px on every input: anything smaller makes iOS Safari zoom the page on
     focus, which reads as a layout bug.
     TWO RULES LATER IN THIS FILE OVERRIDE THIS for desktop density, and both
     are corrected back to 16px inside the mobile block at the bottom:
     `.row-actions input[type="password"]` and `input[type="file"]`. If you add
     a third override, correct it there too or you reintroduce the zoom. */
  font-size: 16px;
  font-family: var(--font-body);
  margin-bottom: 16px;
}
input:focus, select:focus { outline: none; border-color: var(--orange); }

.form-panel {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: end;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 16px;
  border-radius: 2px;
}
.form-panel label { display: flex; flex-direction: column; gap: 4px; margin-bottom: 0; }
.form-panel input { margin-bottom: 0; }
.form-panel .checkbox-row { flex-direction: row; align-items: center; gap: 6px; }
.form-panel .checkbox-row input { width: auto; }

/* ─── Notices ─────────────────────────────────────────────────────────────── */

.notice, .error-box, .info-box {
  padding: 11px 13px;
  border-radius: 2px;
  font-size: 14px;
  line-height: 1.62;
  margin-bottom: 18px;
}
.notice { background: var(--panel); border: 1px solid var(--orange); border-left: 3px solid var(--orange); }
/* #nothing-to-recheck is toggled by BOTH the server and scan.js, so it has to
   actually disappear. No .notice rule sets `display` today, but the donate
   popup shipped visible because `display: flex` silently beat the UA sheet's
   [hidden] rule. Guard it here rather than find out again. */
.notice[hidden] { display: none; }
/* Alert Red, not Civil Defense Red — this is TEXT, and Civil Defense Red fails
   AA on this background. Civil Defense Red is fine as the border/fill. */
.error-box { background: #1d1011; border: 1px solid var(--civil-red); border-left: 3px solid var(--civil-red); color: var(--alert-red); }
.info-box { background: #101a21; border: 1px solid var(--federal-blue); border-left: 3px solid var(--federal-blue); }

/* hazard-stripe banner, for states that need to stop someone */
.hazard-banner {
  padding: 11px 14px;
  border-radius: 2px;
  border: 1px solid var(--civil-red);
  color: var(--alert-red);
  font-family: var(--font-mono);
  font-size: 12px;
  margin-bottom: 18px;
  background: repeating-linear-gradient(
    135deg,
    #1d1011 0px, #1d1011 10px,
    #241416 10px, #241416 20px
  );
}

/* ─── Readouts, chips, tiles ──────────────────────────────────────────────── */

.readout {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cream);
}
.readout-fail { color: var(--alert-red); }

.chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 2px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-family: var(--font-mono);
}
/* Cream on a Civil Defense Red fill is correct and passes — it is red as a
   FILL, which is the only way this red may be used. */
.chip-alert { background: var(--civil-red); color: var(--cream); border: 1px solid var(--civil-red); }
.chip-active { background: var(--panel-alt); color: var(--cream-dim); border: 1px solid var(--border); }
.chip-admin { background: #2e1408; color: var(--orange); border: 1px solid #5a2810; }
.chip-pending { background: #221a0f; color: #e3a33e; border: 1px solid #6b4a1c; }
.chip-info { background: #101a21; color: #7fb2d4; border: 1px solid var(--federal-blue); }
/* The one genuinely earned good-news state in this product. Green is not in
   the kit palette, so this is Reactor Orange as a fill with cream on top —
   the same treatment the alert chip gets, which keeps it on-brand and passes
   contrast without inventing a colour. */
.chip-cleared { background: var(--orange); color: #17110c; border: 1px solid var(--orange); }

input[type="file"] {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--cream);
  padding: 8px 10px;
  border-radius: 2px;
  font-family: var(--font-body);
  font-size: 14px;
}
input[type="file"]::file-selector-button {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--cream);
  padding: 5px 12px;
  margin-right: 10px;
  border-radius: 2px;
  font-family: var(--font-head);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}

.tiles { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-bottom: 26px; }
.tile {
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 2px solid var(--steel);
  border-radius: 2px;
  padding: 12px 10px;
  text-align: center;
}
.tile .value { font-family: var(--font-mono); font-size: 22px; color: var(--orange); line-height: 1.2; }
.tile .label {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--cream-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-top: 4px;
}

/* ─── Tables ──────────────────────────────────────────────────────────────── */

.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
th {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--cream-dim);
  font-weight: 400;
}
/* The <td> stays a table cell; the flex container is the inner div.
   `display: flex` on a cell drops it out of table layout, so the element
   carrying the row's border-bottom shrinks to its own content height and the
   border stops lining up with the rest of the row. */
.row-actions { vertical-align: middle; }
.row-actions-flex { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.row-actions input[type="password"] { width: 140px; margin-bottom: 0; font-size: 13px; padding: 6px 8px; }
.row-actions button { padding: 6px 10px; font-size: 12px; }

/* ─── Log viewer ──────────────────────────────────────────────────────────── */

.log-controls { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; margin-bottom: 10px; }
.log-controls label { display: flex; flex-direction: column; gap: 4px; margin-bottom: 0; }
.log-controls input[type="number"] { width: 110px; margin-bottom: 0; }
.log-controls .checkbox-row { flex-direction: row; align-items: center; gap: 6px; }
.log-controls .checkbox-row input { width: auto; margin-bottom: 0; }
.log-box {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 12px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.62;
  max-height: 420px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
  margin: 0 0 24px;
}

/* ─── Footer ──────────────────────────────────────────────────────────────── */

/* The trademark disclaimer goes on EVERY page. It is part of the nominative
   fair-use posture, not decoration — do not drop it from a template. */
.site-footer {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  margin-top: 56px;
  padding: 18px 20px 24px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--cream-dim);
  text-align: center;
}
.site-footer .disclaimer { max-width: 620px; margin: 0 auto; }

/* Disclosure links, under the disclaimer on every page. THIS IS NOT GENERAL
   NAVIGATION and must not become it: the rule for earning a place here is that
   the disclosure applies to every visitor including logged-out ones, which is
   also why both routes are anonymous. Ordinary pages get linked from where
   they are relevant instead. A footer that accumulates links is how a
   disclosure turns into furniture nobody reads.
   Tap targets are 44px tall via the padding rather than the font size, so they
   stay legible at 12px. Wrapping is expected at 375px - the second label is
   long on purpose, because a coy one would not find the person looking for it. */
.site-footer .footer-links { margin: 6px auto 0; }
.site-footer .footer-links a { display: inline-block; padding: 12px 8px; }

/* ─── Per-source inventory cards ──────────────────────────────────────────── */

/* One card per archive. There is deliberately no "grand total" style, because
   there is deliberately no grand total — see archive_client.py. */
.source-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 2px solid var(--orange);
  border-radius: 2px;
  padding: 16px 18px;
  margin-bottom: 12px;
}
.source-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.source-name {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.source-count {
  font-family: var(--font-mono);
  font-size: 34px;
  line-height: 1.1;
  color: var(--orange);
}
/* "at least" must read as part of the figure, not decoration — the whole
   point is that the number is a floor, not an equality. */
.count-prefix {
  font-family: var(--font-body);
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--cream-dim);
  vertical-align: middle;
}
.source-detail { color: var(--cream-dim); margin-top: 4px; }
.sub-list { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 12px; }

/* ─── Filing kit ──────────────────────────────────────────────────────────── */

/* The generated filing text. Monospace and selectable — this is something a
   person copies out and sends themselves, never something we transmit. */
.filing-head { display: flex; align-items: center; gap: 10px; margin: 14px 0 6px; }
.btn-copy { padding: 5px 12px; font-size: 12px; min-width: 92px; }
/* Disabled only during the "Copied" flash — keep it legible rather than the
   0.4 opacity used for genuinely unavailable controls. */
.btn-copy:disabled { opacity: 1; border-color: var(--orange); color: var(--orange); cursor: default; }

.filing-text {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-left: 3px solid var(--orange);
  border-radius: 2px;
  padding: 12px 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.62;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0 0 12px;
  overflow-x: auto;
}

.gate-list { list-style: none; padding: 0; margin: 0 0 16px; }
.gate-list li {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}
.gate-mark { font-family: var(--font-mono); font-weight: 700; flex-shrink: 0; }
/* Alert Red, never Civil Defense Red: this is TEXT on black. */
.gate-unmet .gate-mark { color: var(--alert-red); }
.gate-met .gate-mark { color: var(--orange); }

code {
  font-family: var(--font-mono);
  font-size: 12px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 1px 5px;
}

/* ─── Utilities ───────────────────────────────────────────────────────────── */

/* These exist because the deployed CSP is `default-src 'self'` with NO
   style-src exception, which blocks inline style="..." attributes outright.
   A blocked inline style does not error visibly - the rule simply never
   applies - so it presents as a layout bug with a clean-looking template.
   Keep markup free of inline styles and the strict CSP stays strict. */
.mt-sm { margin-top: 12px; }
.mt-md { margin-top: 18px; }
.align-center { align-items: center; }
.inline-form { display: flex; gap: 4px; }
.steps { font-size: 14px; line-height: 1.62; padding-left: 20px; }
.steps li { margin-bottom: 6px; }

.hidden { display: none !important; }

/* A cue that a container scrolls sideways. Hidden by default; admin.js adds
   `is-shown` only when the container it describes is ACTUALLY overflowing.
   Deliberately not driven by a media query: the admin table's width depends on
   how long the usernames are, so any fixed breakpoint claims the table scrolls
   at widths where it does not. With JS off this stays hidden, which is the
   safe direction. */
.scroll-hint { display: none; }
.scroll-hint.is-shown { display: block; }

/* ─── Mobile ──────────────────────────────────────────────────────────────── */

@media (max-width: 700px) {
  .radar-rings { top: -110px; right: -110px; width: 240px; height: 240px; }

  /* HIDDEN on mobile, not shrunk. The bars are fixed at bottom-left with
     z-index 0, and the content above them carries no background of its own, so
     in a narrow column they paint UNDER body copy. Measured at 375px they sat
     beneath roughly a third of a 347px text column, and under the footer
     disclaimer on every page. It stays legible either way (cream ~10:1, muted
     ~5.3:1 over the blended bar colour, both pass AA), so this is polish rather
     than a contrast fix. Desktop keeps the motif.
     Do NOT try to solve this with z-index: -1 instead. `body` carries a
     background image, so a fixed child behind it disappears completely. */
  .wipe-bars { display: none; }

  /* The header stays sticky. It must never become `static`: a static header
     creates no stacking context, and the decorative chrome then paints over
     the wordmark. Hit-test with elementFromPoint rather than eyeballing it.
     What it must also not be is TALL, because sticky cost is permanent. This
     bar wraps, and before the rules below it measured 93px on every logged-in
     page at 375px and 141px on the main page: 17% of an 812px viewport given
     away before any content. */
  .topbar { padding: 8px 12px; gap: 6px; }
  .wordmark { font-size: 17px; }
  .brand-tagline { font-size: 12px; letter-spacing: 0.14em; }

  /* Scoped to .topbar on purpose, so the 40px body-wide minimum further down
     is untouched. Header controls are reached deliberately, not thumbed at
     while scrolling. */
  .topbar button, .topbar .btn { min-height: 34px; padding: 6px 10px; }

  /* A long username is what pushed the controls onto a THIRD row: at 11px mono
     an account name can run to ~160px, enough to break the row on its own.
     Truncating is right here because the name is a reassurance, not a control,
     and an ellipsised name still answers "am I signed in as me". */
  .topbar-user {
    max-width: 9em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .wrap, .wrap-wide { margin: 28px auto; padding: 0 14px; }
  .wrap-narrow { margin: 44px auto; padding: 0 18px; }

  .tiles { grid-template-columns: repeat(2, 1fr); }
  .form-panel label { width: 100%; }
  .form-panel input { width: 100%; }

  /* Widen the FLEX CHILD, not the button. Nearly every submit button in this
     app sits inside its own <form>, which is a shrink-to-fit flex item, so the
     old `.form-panel button[type="submit"] { width: 100% }` resolved to the
     form's content width and did nothing at all. Measured before this fix: the
     index action row rendered three controls at 152/143/147px inside a 332px
     panel, every one of which was meant to be full width. The `.btn` rule
     covers anchors, which a button-only selector never reached. */
  .form-panel > form { width: 100%; }
  .form-panel > form > button { width: 100%; }
  .form-panel > button { width: 100%; }
  .form-panel > .btn { width: 100%; }

  button, .btn { min-height: 40px; }

  /* iOS Safari zooms the page whenever a focused text field computes under
     16px. Both selectors below drop under it earlier in this file for real
     desktop density reasons, so they are corrected here rather than there.
     The password field is the worse of the two: it lives inside the
     horizontally scrolling admin table, so the zoom drops you somewhere
     disorienting with no obvious way back. */
  .row-actions input[type="password"] { font-size: 16px; }
  input[type="file"] { font-size: 16px; }

  /* 13x13 boxes inside a 17px label gave an effective tap target well under
     the 24px WCAG 2.2 minimum. The wrapping <label> is what receives the tap,
     so it is the element that carries the height. */
  .checkbox-row { min-height: 44px; }
  .form-panel .checkbox-row input[type="checkbox"],
  .log-controls .checkbox-row input[type="checkbox"] { width: 20px; height: 20px; }

  /* Links in running prose measured 16 to 17px tall. Vertical padding on an
     inline element grows the hit box without changing the line box. Hit areas
     on adjacent wrapped lines can overlap slightly, which is acceptable here
     because none of these sit in a dense run of competing links.
     `.btn` is excluded: it is already a 40px target, and padding on top of its
     own would make it 52px. */
  p a:not(.btn), li a:not(.btn) { padding-top: 6px; padding-bottom: 6px; }
}

/* A channel we could not measure and found no sign of. Collapsed, never
   removed: we never asked that platform anything, so we cannot say it is
   empty. Styled quieter than a full card, but still plainly present. */
.source-card-collapsed > summary {
  display: flex;
  gap: 10px;
  align-items: baseline;
  flex-wrap: wrap;
  cursor: pointer;
  list-style: none;
}
.source-card-collapsed > summary::-webkit-details-marker { display: none; }
.source-card-collapsed > summary::before {
  content: "+";
  font-family: var(--font-mono);
  color: var(--orange);
}
.source-card-collapsed[open] > summary::before { content: "\2212"; }

/* ─── The tablet gap in the sticky header ─────────────────────────────────
   Measured 2026-07-29 by sweeping 660 to 1120px in 20px steps: the main page
   header is 52px at 700px, jumps to 117px from 720 to 760px, then settles at
   64px from 780px up. iPad portrait at 768px lands inside that band, so the
   most common tablet width carries the WORST header in the product.

   Cause is the same one that pushed the controls onto a third row at 375px: a
   long username. `.topbar-user` renders 172px here because its 9em truncation
   lives in the max-width:700px block, so "Sign out" wraps to its own row. Only
   the main page has enough header content to trigger it.

   Sticky cost is permanent, not a top-of-scroll one, so 117px is 11% of an
   iPad viewport surrendered on every screen. Measured after: 60px across the
   band, and nothing at 840px and above moves.

   Deliberately NOT solved by widening the mobile block to 820px, which would
   also drag the 34px header buttons and the 16px input floor up the range
   where there is room for neither. Only the two properties that cause the wrap
   change here. */
@media (min-width: 701px) and (max-width: 820px) {
  .topbar { gap: 10px; padding: 10px 14px; }
  .topbar-user {
    max-width: 9em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ─── The NCII lane (/help/ncii) ──────────────────────────────────────────
   Scoped to `.page-plain`, which only ncii.html carries. Nothing here changes
   any other page, and that scoping is the point rather than tidiness: the rest
   of the product is a verification console and the instrumentation voice is
   right for it.

   The template opens with a tone rule saying that voice is WRONG on this page,
   and then the global rules above uppercased every heading and every button
   label, so the page shouted "IF INTIMATE IMAGES OF YOU ARE BEING SHARED" at
   someone having the worst week of their life. The CSS was overriding the
   written rule. Turning it off here is what makes the rule real.
   ───────────────────────────────────────────────────────────────────────── */

.page-plain h1,
.page-plain h2,
.page-plain h3 { text-transform: none; letter-spacing: 0.01em; }

/* Sentence case at the caps tracking reads loose, and 12px reads small once the
   ascenders and descenders come back. Both are compensation for the line above,
   not a separate opinion about button sizing. */
.page-plain .btn { text-transform: none; font-size: 13px; letter-spacing: 0.02em; }

/* Lets a service render as its real name — StopNCII.org, not STOPNCII.ORG. */
.page-plain .source-name { text-transform: none; letter-spacing: 0.02em; }

/* The age fork. Anchors, NOT a question-gate: both sections render in full
   below regardless, so this only saves scrolling. See the template comment. */
.page-plain .fork { display: flex; flex-direction: column; gap: 10px; margin: 0 0 8px; }
.page-plain .fork-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 15px;
  min-height: 56px;
  border: 1px solid var(--border-hover);
  border-left: 3px solid var(--orange);
  border-radius: 2px;
  background: var(--panel);
  color: var(--cream);
  text-decoration: none;
}
.page-plain .fork-card:hover { background: var(--panel-alt); text-decoration: none; }
.page-plain .fc-main {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.02em;
}
.page-plain .fc-sub {
  display: block;
  margin-top: 2px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: 0;
  color: var(--cream-dim);
}
.page-plain .fc-arrow { margin-left: auto; font-family: var(--font-mono); color: var(--orange); }
.page-plain .fork-note { font-size: 12px; color: var(--steel); margin: 0 0 18px; }

/* THE FORK LANDS THE CARD BELOW THE STICKY HEADER, NOT UNDER IT.
   Without this the anchor puts the card top at viewport y=0 and `.topbar`,
   which is sticky, paints over it: measured at 375px the "If you were under 18"
   heading landed at 18px and the red hazard banner at 46px, both behind a 52px
   bar. Someone taps "I was under 18" and the first thing they should see is the
   warning, so this is the same safety point as the banner's position, not
   cosmetics.

   ONE VALUE, NO BREAKPOINTS, ON PURPOSE. The header height does not track width
   monotonically - measured 81px at 320px (it wraps), 52px at 375px, 64px at
   1280px - so any breakpoint would be wrong on one side of itself. 96px clears
   the tallest observed header with room to spare and costs only a little air at
   the others. If the header ever gains a row, re-measure this. */
.page-plain .source-card[id] { scroll-margin-top: 96px; }

/* `who` is the line that answers "is this one for me". It used to be glued to
   the front of a 300-character description in the same 13px muted run, so the
   scannable half was invisible. Same two dataclass fields, rendered apart. */
.page-plain .who {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--cream-dim);
  margin: 9px 0 3px;
}
.page-plain .detail { font-size: 13px; color: var(--cream-dim); margin: 0; }

/* The one phone number in the product, given its own dialable target rather
   than sitting in a `.readout` span as undialable display text. */
.page-plain .tel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  /* Full width is right in a ~310px phone column and absurd in a 740px card,
     where it rendered a 702px button around an 11-character phone number.
     The cap lands above every mobile width, so the phone case is unaffected. */
  max-width: 340px;
  min-height: 46px;
  margin-top: 8px;
  border: 1px solid var(--border-hover);
  border-radius: 2px;
  background: var(--panel-alt);
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0.04em;
  text-decoration: none;
}
.page-plain .tel:hover { border-color: var(--orange); text-decoration: none; }
.page-plain .tel .ic { color: var(--orange); }

@media (max-width: 700px) {
  /* The existing full-width fix targets `.form-panel > .btn`. This page has no
     form panel, so its nine buttons were never covered and measured 129 to
     225px in a 332px column. These are anchors in normal flow, so they take
     the width directly rather than needing a flex parent widened. */
  .page-plain .btn-block { width: 100%; min-height: 46px; }
}


/* ─── Media package (/press) ──────────────────────────────────────────────
   Press and partner assets. Everything is scoped under `.kit`, which only
   press.html sets, so no other page can be reached by these rules.
   The tokens and faces are the site's own; nothing new is introduced.
   ───────────────────────────────────────────────────────────────────── */
.kit{max-width:900px;margin:0 auto;padding:0 28px 80px}

.kit h1,.kit h2,.kit h3{font-family:var(--font-head);font-weight:700;text-transform:uppercase;
  letter-spacing:.03em;margin:0}
.kit .hl{color:var(--orange)}
.kit b,.kit strong{font-weight:600;color:var(--cream)}

/* running head */
.kit .runhead{
  display:flex;flex-wrap:wrap;gap:0 22px;padding:14px 0 12px;
  border-bottom:1px solid var(--border);position:sticky;top:0;z-index:5;
  background:linear-gradient(180deg,#0a0b0b 70%,rgba(10,11,11,.86));
  font-family:var(--font-mono);font-size: 12px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--steel);
}
.kit .runhead span:first-child{color:var(--cream-dim)}

/* ── cover ───────────────────────────────────────────────────────────── */
.kit .cover{position:relative;overflow:hidden;padding:64px 0 52px;border-bottom:1px solid var(--border)}
.kit .cover-rings{
  position:absolute;top:-160px;right:-150px;width:420px;height:420px;
  border-radius:50%;pointer-events:none;opacity:.5;
  background:repeating-radial-gradient(circle,transparent 0,transparent 19px,
    rgba(230,224,211,.07) 20px,transparent 21px);
}
.kit .eyebrow{font-family:var(--font-mono);font-size: 12px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--orange);margin:0 0 18px}
.kit .cover h1{font-size:clamp(46px,9vw,84px);line-height:.92;letter-spacing:.01em;margin:0 0 24px}
.kit .cover-lede{font-size:18px;color:var(--cream-dim);max-width:56ch;margin:0 0 38px}

.kit .cover-lockup{
  display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;
  border:1px solid var(--border);border-left:3px solid var(--orange);
  background:var(--panel);padding:22px 24px;margin-bottom:34px;
  clip-path:polygon(9px 0,100% 0,100% calc(100% - 9px),calc(100% - 9px) 100%,0 100%,0 9px);
}
.kit .mark{width:26px;height:26px;color:var(--orange);flex:none}
.kit .mark-lg{width:44px;height:44px}
/* The illustrated mark in the press specimens and the cover lockup. A raster
   file, so it cannot inherit the accent the way .mark does - which is exactly
   the distinction /press section 01 is describing. Sized here rather than by
   the width/height attributes so the two uses stay in step. */
.kit .mark-img{width:64px;height:64px;flex:none}
.kit .cover-lockup .mark-img{width:44px;height:44px}
.kit .wordmark{
  font-family:var(--font-head);font-weight:700;font-size:24px;letter-spacing:.05em;
  text-transform:uppercase;color:var(--cream);line-height:1;padding-bottom:5px;
  border-bottom:2px solid var(--orange);white-space:nowrap;
}
.kit .wordmark .tool{color:var(--orange);margin-left:.4em}
.kit .wordmark-lg{font-size:30px}
.kit .tagline{font-size: 12px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--steel);line-height:1;padding-bottom:7px}

.kit .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
  gap:1px;background:var(--border);border:1px solid var(--border);margin:0}
.kit .facts > div{background:var(--panel);padding:14px 16px}
.kit .facts dt{font-family:var(--font-mono);font-size: 12px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--steel);margin-bottom:5px}
.kit .facts dd{margin:0;font-size:14px;color:var(--cream)}
.kit .wipe{
  position:absolute;bottom:0;left:-4px;width:190px;height:104px;opacity:.45;
  pointer-events:none;
  background:
    linear-gradient(var(--civil-red),var(--civil-red)) 0 0/180px 8px no-repeat,
    linear-gradient(var(--civil-red),var(--civil-red)) 0 20px/140px 8px no-repeat,
    linear-gradient(var(--civil-red),var(--civil-red)) 0 40px/100px 8px no-repeat,
    linear-gradient(var(--civil-red),var(--civil-red)) 0 60px/62px 8px no-repeat,
    linear-gradient(var(--civil-red),var(--civil-red)) 0 80px/30px 8px no-repeat;
}

/* ── sections ────────────────────────────────────────────────────────── */
.kit section{padding:56px 0;border-bottom:1px solid var(--border)}
.kit section:last-child{border-bottom:none}
.kit .secnum{font-family:var(--font-mono);font-size: 12px;letter-spacing:.16em;
  color:var(--orange);margin:0 0 10px}
.kit h2{font-size:26px;margin:0 0 16px}
.kit .lede{font-size:17px;color:var(--cream-dim);max-width:66ch;margin:0 0 30px}

/* logo specimens */
.kit .specimens{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin-bottom:26px}
.kit .spec{margin:0}
.kit .spec-art{
  display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;
  background:var(--panel-alt);border:1px solid var(--border);
  padding:34px 26px;min-height:150px;align-items:center;
}
.kit .spec-art-icon{justify-content:center}
.kit figcaption{font-family:var(--font-mono);font-size: 12px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--steel);margin-top:9px}

.kit .rules p{margin:0 0 14px;color:var(--cream-dim);max-width:74ch}
.kit .rules .k{color:var(--cream);font-weight:600}

/* palette */
.kit .tablewrap{overflow-x:auto;border:1px solid var(--border);background:var(--panel)}
.kit table.pal{border-collapse:collapse;width:100%;min-width:660px;font-size:14px}
.kit .pal th{
  font-family:var(--font-mono);font-size: 12px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--steel);font-weight:400;text-align:left;
  padding:11px 14px;border-bottom:1px solid var(--border);
}
.kit .pal td{padding:12px 14px;border-bottom:1px solid #1e2122;color:var(--cream-dim);vertical-align:middle}
.kit .pal tr:last-child td{border-bottom:none}
.kit .pal td:nth-child(2){color:var(--cream);white-space:nowrap}
.kit .pal td:nth-child(3){font-family:var(--font-mono);font-size:12px;color:var(--cream-dim);white-space:nowrap}
.kit .pal td.r{font-family:var(--font-mono);font-size: 12px;white-space:nowrap;color:var(--steel)}
.kit .pal td.r.ok{color:#5fa46a}
.kit .pal td.r.warn{color:#e3a33e}
.kit .pal td.r.bad{color:var(--alert-red)}
.kit .sw{display:block;width:26px;height:26px;border:1px solid var(--border)}
.kit .sw[data-c=graphite]{background:#0a0b0b}
.kit .sw[data-c=panel]{background:#131414}
.kit .sw[data-c=cream]{background:#e6e0d3}
.kit .sw[data-c=creamdim]{background:#a8a294}
.kit .sw[data-c=orange]{background:#e34222}
.kit .sw[data-c=alert]{background:#e5504a}
.kit .sw[data-c=civil]{background:#b7161c}
.kit .sw[data-c=steel]{background:#5c6066}
.kit .sw[data-c=blue]{background:#1f4a68}

/* hazard + notice blocks */
.kit .hazard{
  margin-top:22px;padding:15px 17px;border:1px solid var(--civil-red);
  color:var(--alert-red);font-family:var(--font-mono);font-size: 12px;line-height: 1.62;
  background:repeating-linear-gradient(135deg,#1d1011 0,#1d1011 10px,#241416 10px,#241416 20px);
}
.kit .hazard b{color:var(--alert-red)}
.kit .notice{
  margin-top:22px;padding:15px 17px;background:var(--panel);
  border:1px solid var(--orange);border-left:3px solid var(--orange);
  font-size:15px;color:var(--cream-dim);
}

/* typography specimens */
.kit .typespec{display:grid;gap:12px}
.kit .ts{background:var(--panel);border:1px solid var(--border);
  border-left:3px solid var(--border-hover);padding:20px 22px}
.kit .ts-label{font-family:var(--font-mono);font-size: 12px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--steel);margin:0 0 12px}
.kit .ts-sample{margin:0 0 12px;color:var(--cream)}
.kit .s-head{font-family:var(--font-head);font-weight:700;font-size:30px;
  text-transform:uppercase;letter-spacing:.03em;line-height:1.1}
/* The rules that used to live here were lost to a splice: this line read
   `.kit .s-.dodont{...}`, the head of a `.s-body` rule welded to the tail of a
   `.dodont` rule. Both, and the five between them, are restored at the end of
   this file under "the rules a corrupted line ate". */
.kit .col{background:var(--panel);border:1px solid var(--border);padding:20px 22px}
.kit .col.do{border-top:2px solid #5fa46a}
.kit .col.dont{border-top:2px solid var(--civil-red)}
.kit .col h3{font-size:14px;margin:0 0 12px;letter-spacing:.06em}
.kit .col.do h3{color:#5fa46a}
.kit .col.dont h3{color:var(--alert-red)}
.kit .col ul{margin:0;padding-left:19px;color:var(--cream-dim);font-size: 14px}
.kit .col li{margin-bottom:9px}

.kit .foot{margin:30px 0 0;font-size: 13px;color:var(--steel);max-width:70ch}
.kit .signoff{
  margin:26px 0 0;font-family:var(--font-head);font-weight:700;font-size:20px;
  text-transform:uppercase;letter-spacing:.08em;color:var(--orange);
}

@media (max-width:640px){
.kit{padding:0 16px 56px}
.kit .cover{padding:40px 0 40px}
.kit .cover-rings{width:280px;height:280px;top:-110px;right:-100px}
.kit section{padding:40px 0}
.kit .runhead span:nth-child(3),.kit .runhead span:nth-child(4){display:none}
.kit .wipe{display:none}
}
@media print{
.kit .runhead{position:static}
.kit section{break-inside:avoid}
}

/* ═══ THE LIGHT SURFACE ═══════════════════════════════════════════════════
 *
 * Everything below is scoped to `.page-light`, set via the `body_class` block
 * in _base.html. A template that does not set it is untouched, which is what
 * makes this safe to land one page at a time.
 *
 * THE TOKEN NAMES DO NOT CHANGE. `--orange` is blue here and `--cream` is
 * near-black. That reads wrong and is right: the names are load bearing across
 * twelve templates and three JS files, and a parallel set would mean every
 * component existed twice. Read them as roles, not colours. If the whole site
 * ever goes light, remap `:root` and delete the wrapper.
 *
 * CONTRAST, measured against the surface each pair actually sits on, not
 * against the page ground:
 *   #17191C on #FFFFFF   17.4:1  body copy, safe anywhere
 *   #5C6570 on #FFFFFF    5.8:1  secondary copy
 *   #6B747E on #FFFFFF    4.7:1  labels and eyebrows. DO NOT LIGHTEN
 *   #1A5F9E on #FFFFFF    6.7:1  accent as text, and as a fill under white
 *   #1F6B52 on #EAF4EF    5.7:1  ok chip
 *   #8A5A00 on #FBF3E4    5.4:1  amber chip and partial-state text
 *   #A32A21 on #FAEDEB    6.3:1  alert chip, red banner, danger control
 *   #8C949B on #FFFFFF    3.1:1  LARGE TEXT ONLY. The 30px+ no-data dash
 *   #8B9491 on #FFFFFF    3.1:1  non-text only. Control borders, the 3:1 floor
 *   #D9776E on #FFFFFF    3.0:1  BORDER AND FILL ONLY. Never text
 *
 * Three of those were darkened during design because the softer versions
 * failed: label grey from #77808A, no-data grey from #9AA2A9, control border
 * from #D4D8D6. Do not revert them for looking gentler.
 * ═══════════════════════════════════════════════════════════════════════ */

/* IBM Plex Sans ships as a VARIABLE face, so one 45KB file covers 400 to 600
   rather than three static files. IBM Plex Mono has no variable build on
   Google Fonts, so 400 and 500 stay separate. Self-hosted for the two reasons
   in fonts/README.md: the CSP blocks a font CDN outright, and linking one
   would hand every visitor IP to a third party on a privacy tool. */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("/static/fonts/ibm-plex-sans-var.woff2") format("woff2");
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/static/fonts/ibm-plex-mono-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

.page-light {
  --graphite: #fbfbf9;
  --panel: #ffffff;
  --panel-alt: #f3f7fb;
  --border: #e3e5e4;
  --border-hover: #8b9491;
  --cream: #17191c;
  --cream-dim: #5c6570;
  --steel: #6b747e;
  --orange: #1a5f9e;
  --civil-red: #d9776e;
  --alert-red: #a32a21;
  --federal-blue: #1a5f9e;

  /* New roles the dark surface expresses with literals. --amber replaces the
     hard-coded #e3a33e in the partial-scan and pending rules. */
  --amber: #8a5a00;
  --rule-soft: #eff0ef;
  --inset: #fafaf8;
  --no-data: #8c949b;
  /* The progress rail's not-yet grey. MEASURED 1.56:1 on --graphite, so it is
     DECORATIVE ONLY: the ring, the icon strokes and the dashes. Never any text,
     including the ordinal, which is why the not-yet ordinal is re-coloured to
     --steel below. Compare --no-data at 3.1:1, which carries the dash. */
  --rail-idle: #c6ccd1;
  --info-border: #cfdeea;
  --amber-border: #e8d6b4;
  --amber-bg: #fbf3e4;
  --red-border: #e7b3ad;
  --red-bg: #faedeb;
  --ok-ink: #1f6b52;
  --ok-bg: #eaf4ef;

  --font-head: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-body: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", "Consolas", monospace;

  /* The shorthand is doing real work: it clears the halftone dot texture that
     `body` sets as a background-image. A separate `background-color` would
     leave the dots on a white ground, where they are grubby rather than inky. */
  background: var(--graphite);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

/* The radar rings, the wipe bars and the 7px chamfer are dark-instrument
   motifs. On paper the chamfer in particular reads as military hardware. */
.page-light .radar-rings,
.page-light .wipe-bars { display: none; }

/* ─── Frame ──────────────────────────────────────────────────────────────
   Header and footer are full bleed white with a hairline on the inner edge,
   while their CONTENTS line up with the text column. That is what the two
   wrappers are for. They are `display: contents` everywhere else, so the dark
   pages keep the header children as direct flex items of `.topbar` and not one
   pixel moves. */

.topbar-inner,
.site-footer-inner { display: contents; }

.page-light .wrap { max-width: 880px; margin: 0 auto; padding: 0 28px; }
.page-light .wrap-narrow { max-width: 720px; margin: 0 auto; padding: 0 28px; }
.page-light .wrap-wide { max-width: 1040px; margin: 0 auto; padding: 0 28px; }

/* NOT STICKY, and that is a decision rather than an omission. The dark header
   is sticky because it carries controls; here it carries a wordmark and a sign
   out. Nothing needs to follow the reader, so the permanent sticky height
   budget documented in the mobile block does not apply and every control can
   take the full tap target. */
.page-light .topbar {
  position: static;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  padding: 0;
  display: block;
}
.page-light .topbar-inner {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  max-width: 880px;
  margin: 0 auto;
  padding: 18px 28px;
}

.page-light .brand-lockup { display: flex; align-items: center; gap: 10px; }
.page-light .brand-icon {
  width: 40px;
  height: 40px;
  object-fit: contain;
  flex: none;
}
/* Sentence case, no rule under the mark, no condensed caps. The markup is
   already sentence case and the dark theme uppercases it in CSS, so turning
   the transform off is the whole change. */
.page-light .wordmark {
  font-family: var(--font-head);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--cream);
  border-bottom: none;
  padding-bottom: 0;
  line-height: 1.2;
}
.page-light .wordmark .tool { color: var(--orange); margin-left: 0; }
.page-light .brand-tagline {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--steel);
  align-self: center;
  padding-bottom: 0;
}
.page-light .topbar-spacer { flex: 1; margin-left: 0; }
.page-light .topbar-user {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--cream-dim);
  max-width: 11em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE HORIZONTAL PADDING LIVES ON THE INNER COLUMN, NOT THE BAR.
   `.wrap` is `max-width: 880px` WITH `padding: 0 28px`, so its text starts
   28px inside an 880px box. When the footer put the padding on the full-bleed
   bar and gave the inner an 880px max-width, the inner centred inside
   (viewport - 56) and its text began 28px to the LEFT of every other band on
   the page. Measured at 1280px: body and header brand at 221px, footer text
   at 193px. Match `.wrap` exactly and the three line up. */
.page-light .site-footer {
  margin-top: 56px;
  background: var(--panel);
  border-top: 1px solid var(--border);
  padding: 28px 0 44px;
  font-size: 15px;
  color: var(--cream-dim);
  text-align: left;
}
.page-light .site-footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: baseline;
  justify-content: space-between;
  max-width: 880px;
  margin: 0 auto;
  padding: 0 28px;
}
/* THE BANDS FOLLOW THE PAGE'S OWN COLUMN.
   Header and footer default to the 880px measure, which is right for most
   pages and wrong for the three that do not use it. Measured at 1280px before
   this block: on /admin the table column started at 141px while the wordmark
   above it started at 221px, an 80px step; /help/ncii was 80px the other way,
   and /press 10px. Each looked like the header was misaligned rather than the
   page being deliberately wider or narrower.

   `:has()` rather than a per-page class, because the wrapper that decides the
   measure lives inside <main> and cannot reach the header any other way, and
   because it keeps the width defined in exactly one place per page. If a
   browser without `:has()` ever matters here, it falls back to 880 and the
   step comes back; it does not break. */
.page-light:has(.wrap-wide) .topbar-inner,
.page-light:has(.wrap-wide) .site-footer-inner { max-width: 1040px; }
.page-light:has(.kit) .topbar-inner,
.page-light:has(.kit) .site-footer-inner { max-width: 900px; }
.page-light.page-plain .topbar-inner,
.page-light.page-plain .site-footer-inner { max-width: 720px; }

.page-light .site-footer .disclaimer { margin: 0; max-width: 58ch; }
.page-light .site-footer .footer-links {
  margin: 0;
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}
.page-light .site-footer .footer-links a { padding: 12px 0; }

/* ─── Type ───────────────────────────────────────────────────────────────
   Small type is scaled UP from the dark system deliberately. The 10 to 13px
   instrument sizes are right on a dark panel and read as cramped on paper. */

.page-light a { color: var(--orange); }
.page-light a:hover { color: var(--cream); }

.page-light h1,
.page-light h2,
.page-light h3 {
  font-family: var(--font-head);
  font-weight: 600;
  text-transform: none;
}
.page-light h1 {
  font-size: 32px;
  line-height: 1.2;
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 0 0 12px;
}
.page-light .hero h1 { font-size: 36px; line-height: 1.2; margin-bottom: 20px; }
.page-light h2 { font-size: 24px; letter-spacing: -0.015em; margin: 0 0 12px; }
.page-light h3 { font-size: 18px; letter-spacing: -0.01em; margin: 0 0 7px; }

/* SPACE ABOVE A SECTION HEADING, added 2026-08-04. h2 carried no top margin at
 * all, so every gap above one was really the PREVIOUS element's bottom margin.
 * Measured across the site it ranged 0 to 26px, and it was 0 on /admin twice,
 * where an h2 follows a form panel and a table - both of which end their own
 * box flush, so the heading welded to the block above it.
 *
 * THE `* +` IS LOAD BEARING, twice over. It cannot match a first child, so a
 * heading that opens a `.wrap` keeps the 56px from
 * `main > .wrap > :first-child`, and a heading that opens a `<section>` keeps
 * that section's 56px padding-top instead of adding to it. And because these
 * are adjacent siblings in normal flow, the two margins COLLAPSE: a previous
 * element already leaving 26px still leaves 26px, not 52. This only ever fills
 * a gap that was too small.
 *
 * Declared after the h2 rule above deliberately - equal specificity (0,1,1),
 * so the later one wins on margin-top and the shorthand above does not reset
 * it. Do not move a competing h2 rule below this line. */
.page-light * + h2 { margin-top: 26px; }
.page-light p { text-wrap: pretty; }

.page-light .lede { font-size: 19px; line-height: 1.62; margin: 0 0 16px; color: var(--cream); }
.page-light .muted { color: var(--cream-dim); font-size: 17px; }
.page-light .note { font-size: 15px; line-height: 1.62; color: var(--steel); margin: 0; max-width: 70ch; }
.page-light .intro { max-width: 60ch; }

/* The section eyebrow. Mono still means "instrument label", the same rule the
   dark surface follows: a measured value or a label is mono, a sentence is not. */
.page-light .spec {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--steel);
  border-top: 1px solid var(--border);
  padding-top: 14px;
  margin: 0 0 18px;
}
/* An eyebrow that keeps the size, colour and rule but drops the uppercase.
   It exists for one line: the route for intimate images shared without
   consent. The house voice is instrumentation and it is wrong for that
   subject, which is the whole reason `page-plain` turns the transform off on
   /help/ncii. A console eyebrow shouting the same phrase would reintroduce
   exactly what that rule removes. */
.page-light .spec-sentence { text-transform: none; letter-spacing: 0.02em; font-size: 13px; }

/* AN EYEBROW AND ITS HEADING ARE ONE UNIT, so the generic
 * `.page-light * + h2 { margin-top: 26px }` must not get between them. That
 * rule is for separating a heading from the block BEFORE it; `.spec` is not a
 * preceding block, it is the label of the heading underneath it, and its own
 * `margin-bottom: 18px` is the spacing that pair was designed with.
 *
 * Added 2026-08-04 after the typography pass pushed the pair 18px -> 26px on
 * /how and on the console. The console is under a standing "don't adjust"
 * instruction, so this is a repair there, not a change. */
.page-light .spec + h2,
.page-light .spec-sentence + h2 { margin-top: 0; }

.page-light section { padding-top: 56px; }

/* THE PAGE TITLE BLOCK IS NOT A CONTENT SECTION, and the gap after it should
 * not be the full inter-section 56px. On /how the title block is an h1 and a
 * lede alone in a <section>, so the space between the lede and the rule that
 * opens the next section came to 72px: 56px of section padding plus the lede's
 * own 16px trailing margin. Owner reported it from the rendered page.
 *
 * 40px, declared once rather than left as 24 + whatever the last child
 * happens to trail. Zeroing that trailing margin is what makes the number
 * mean what it says; without it, changing the lede's margin silently changes
 * this gap too.
 *
 * SCOPED TO `.page-head`, which only how.html carries. The 56px above is load
 * bearing across twelve pages and is deliberately in step with
 * `main > .wrap > :first-child:not(section) { margin-top: 56px }` below - do
 * not lower it globally to fix one page. The console in particular is under a
 * standing "Main page is fine, don't adjust" instruction. */
.page-light .page-head > :last-child { margin-bottom: 0; }
.page-light .page-head + section { padding-top: 40px; }

/* The first block under the header gets the SAME 56px whether or not the
 * template wrapped it in a <section>.
 *
 * Measured 2026-07-30 after the owner flagged /about/logs: pages whose first
 * heading sits in a <section> (/how, the console hero) had 56px under the
 * header, and the EIGHT pages whose h1 sits directly in the wrap (/about/logs,
 * /about/data, /help/ncii, /kit, its question page, /verify, /upgrade, /admin)
 * had 0px, with the heading welded to the header rule.
 *
 * :not(section) because on the pages that already have the section padding this
 * would otherwise ADD 56px margin on top of 56px padding. MARGIN, not padding:
 * a first child can be a notice or error box with its own background, and
 * padding would open a 56px hole inside that box.
 *
 * .wrap-narrow is deliberately absent: /login and /signup centre vertically and
 * set their own rhythm. /press is untouched too - its first child is its own
 * <header> with its own inset, inside .kit rather than a wrap. */
.page-light main > .wrap > :first-child:not(section),
.page-light main > .wrap-wide > :first-child:not(section) { margin-top: 56px; }

.page-light .readout {
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--cream);
}
.page-light .readout-fail { color: var(--alert-red); }
.page-light .meta {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: baseline;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--cream-dim);
  margin-bottom: 26px;
}
.page-light .meta .sep { color: var(--no-data); }

/* ─── Controls ─────────────────────────────────────────────────────────── */

.page-light button,
.page-light .btn {
  clip-path: none;
  border-radius: 4px;
  min-height: 48px;
  padding: 12px 24px;
  font-family: var(--font-head);
  font-size: 17px;
  font-weight: 500;
  /* EXPLICIT, and it is the difference between a matching pair of controls and
     a mismatched one. A `<button>` gets `line-height: normal` from the UA
     sheet, while an `<a class="btn">` inherits the body's 1.62. Measured in
     the header: Admin (an anchor) came out 45.9px against Sign out (a button)
     at 40px, and the same 6px gap appeared in every row that mixes the two,
     including Download case file beside Verify removal. */
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: none;
  border: 1px solid var(--border-hover);
  background: transparent;
  color: var(--cream);
}
.page-light button:hover,
.page-light .btn:hover { border-color: var(--orange); color: var(--orange); }

/* THE `:not()` CHAIN IS LOAD BEARING, not tidiness. `button[type="submit"]`
   outranks a bare `.btn-danger` or `.btn-ghost` on specificity, and almost
   every control in this app is a submit inside its own form. Without this,
   Unlink and Sign out both render as the loudest control on the page. Exclude
   a variant here when you add one, or it silently becomes the primary. */
.page-light button[type="submit"]:not(.btn-ghost):not(.btn-danger):not(.btn-copy),
.page-light .btn-primary {
  background: var(--orange);
  border-color: var(--orange);
  color: #ffffff;
}
.page-light button[type="submit"]:not(.btn-ghost):not(.btn-danger):not(.btn-copy):hover,
.page-light .btn-primary:hover {
  background: var(--cream);
  border-color: var(--cream);
  color: #ffffff;
}
.page-light .btn-ghost { background: transparent; color: var(--cream); border-color: var(--border-hover); }
.page-light .btn-ghost:hover { background: transparent; border-color: var(--orange); color: var(--orange); }
/* Civil red as the BORDER with alert red as the ink. Never red text on a red
   fill, and never civil red as text: it is 3.0:1 and fails at every size. */
.page-light .btn-danger {
  background: transparent;
  border-color: var(--civil-red);
  color: var(--alert-red);
}
.page-light .btn-danger:hover { background: var(--red-bg); border-color: var(--alert-red); color: var(--alert-red); }
.page-light button:disabled,
.page-light .btn[aria-disabled="true"] { opacity: 0.45; }
.page-light .topbar button,
.page-light .topbar .btn { min-height: 40px; padding: 9px 18px; font-size: 16px; }

/* A running submit keeps its label and gains a spinner. It must NOT take the
   disabled opacity: a faded control reads as "unavailable" when it means
   "working", and someone waiting needs to read what they are waiting for. */
.page-light form.is-busy button:disabled { opacity: 1; }

/* Same argument for a held retry, and it needs restating HERE rather than
   relying on the base rule. `button.is-cooling:disabled` and
   `.page-light button:disabled` have identical specificity (0,2,1), so the one
   further down the file wins and the light surface would take the control back
   to 0.45 - a countdown nobody can read on a button that looks dead. */
.page-light button.is-cooling:disabled { opacity: 1; }
.page-light .btn-ghost.is-cooling:disabled { border-color: var(--border-hover); }

/* The prototypes draw no focus ring at all. 2px of accent at 6.7:1 clears the
   3:1 floor on every surface on the page. */
.page-light a:focus-visible,
.page-light button:focus-visible,
.page-light .btn:focus-visible,
.page-light input:focus-visible,
.page-light summary:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 2px;
}

.page-light .actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }

/* ─── Admin control rows ──────────────────────────────────────────────────
 * The two admin pages (/admin and /admin/people) are dense operator screens:
 * one row can carry five buttons, and the account table already sizes its
 * controls at 44px/14px. The light surface's default control is 48px/17px with
 * 24px of side padding, which is right for a page with two or three things on
 * it and wrong beside a table of them. Owner, 2026-08-04, looking at the signup
 * row: "bottons are too big and not matching".
 *
 * SCOPED TO THE PANELS, NOT THE PAGE. `.page-admin button` would take the
 * topbar with it, and the header is the one thing that must look identical
 * across every page of the site.
 */
.page-admin .form-panel button,
.page-admin .form-panel .btn,
.page-admin .log-controls button {
  min-height: 44px;
  padding: 8px 14px;
  font-size: 14px;
}

/* The input matches the button it sits beside. Bottom-aligned controls of
 * 48px and 44px leave a 4px step that reads as a mistake rather than a size.
 * FONT-SIZE IS DELIBERATELY UNTOUCHED at 16px: anything smaller makes iOS
 * Safari zoom the page on focus, which is the guard the mobile block restores
 * on two other inputs. Only the box shrinks. */
.page-admin .form-panel input:not([type="checkbox"]),
.page-admin .log-controls input:not([type="checkbox"]) {
  /* MIN-HEIGHT AS WELL AS HEIGHT, and it is not belt and braces. `.page-light
     input` sets `min-height: 48px`, and a min-height always beats a height no
     matter which rule is more specific - so `height: 44px` alone applied its
     padding, changed nothing about the box, and left the 4px step exactly where
     it was. Measured that way round before this line existed. */
  min-height: 44px;
  height: 44px;
  padding: 8px 12px;
}

/* ─── The account table: give the width to the actions ───────────────────
 * Owner, 2026-08-04, on the live table: "too busy, lets make dates smaller
 * fonts and make action part wider. On actions lets also make the buttons
 * smaller."
 *
 * The two timestamp columns were the problem. At the table's 16px they each
 * wrapped "2026-08-04 13:59 UTC" onto TWO lines, which set the height of every
 * row, and they took width from the actions column until Reset and Revoke could
 * not sit on one line either. So each row was two lines of date beside two rows
 * of buttons, for one account.
 *
 * `white-space: nowrap` is what actually frees the width: a wrapped cell asks
 * the layout for its widest WORD, so the column keeps a share it does not need
 * while forcing the wrap. One line at 13px is narrower than two at 16px.
 */
.page-admin td:nth-child(3),
.page-admin td:nth-child(4) {
  font-size: 13px;
  color: var(--steel);
  white-space: nowrap;
}

/* The only width hint in the table, and it is deliberately the only one. Auto
 * table layout hands the slack to the column that asks; username and status
 * still size to their content, so a long username is never clipped - which is
 * what `table-layout: fixed` would have risked for the sake of tidier CSS.
 *
 * 58%, and the number is measured rather than chosen. A full admin row carries
 * five controls (Reset plus its field, Make admin, Make moderator, Allow test
 * link, Revoke) and needs 663px to sit on one line. At 45% of the standard
 * container it had 442px, so 84 of 100 rows wrapped to two lines and every one
 * of them was 40px taller for it. */
.page-admin th:nth-child(5) { width: 58%; }

/* THE ADMIN PAGES ARE WIDER THAN THE REST OF THE SITE. 1320px against 1040.
 *
 * Reallocating inside the standard container could not fix the wrap: the four
 * other columns need ~450px of the 1040, which leaves 590 against a 663px
 * need. The width has to come from the page. These two pages are an operator's
 * table, viewed on a desktop, on a site whose other pages are prose and forms
 * that a 1040px measure suits - so this is scoped to them and nothing else.
 *
 * ALL THREE SELECTORS OR NONE. The header and footer inners carry the same
 * max-width so the page lines up with its own chrome; widening only the body
 * puts the content 140px left of the header, measured. This changes their
 * WIDTH and not their height, so the 69px header budget at 375px is untouched -
 * and below ~1360px everything here is viewport-limited anyway, so a phone
 * never sees this rule.
 *
 * `:has(.wrap-wide)` and both body classes, because the rule this overrides is
 * `.page-light:has(.wrap-wide) .topbar-inner` at (0,3,0) - a plain
 * `.page-admin .topbar-inner` is (0,2,0) and loses on specificity no matter
 * where it sits in the file. Written that way first, and the header stayed at
 * 1040 while the body went to 1320. */
.page-light.page-admin .wrap-wide,
.page-light.page-admin:has(.wrap-wide) .topbar-inner,
.page-light.page-admin:has(.wrap-wide) .site-footer-inner { max-width: 1320px; }

/* SMALLER ACTION BUTTONS, AND THE MEDIA QUERY IS THE WHOLE POINT.
 *
 * `.page-light .row-actions button` is 44px AT EVERY WIDTH, raised from 38px
 * earlier on 2026-08-04 precisely because this was the one table in the product
 * with controls under the tap floor. Shrinking it back everywhere would undo
 * that fix within the day.
 *
 * `pointer: fine` is the condition that makes both true at once: a mouse can
 * hit a 34px target comfortably, a fingertip cannot. A touch laptop, a tablet
 * and a phone all keep 44px no matter how wide the window is, because they are
 * `pointer: coarse`. The width test stays as well - a fine pointer in a narrow
 * window is still a cramped row.
 */
/* BOTH BODY CLASSES IN THE SELECTOR, and it is not decoration. `.page-admin
 * .row-actions button` and `.page-light .row-actions button` have identical
 * specificity (0,2,1), and the light one sits ~700 lines further down, so it
 * wins on order alone. Written the obvious way first, this whole block matched
 * and changed nothing: measured 44px/14px with the media query reporting true.
 * `.page-light.page-admin` is (0,3,1) and cannot be beaten by position. */
@media (min-width: 701px) and (pointer: fine) {
  .page-light.page-admin .row-actions button { min-height: 34px; padding: 5px 11px; font-size: 13px; }
  .page-light.page-admin .row-actions input[type="password"] {
    min-height: 34px;
    height: 34px;
    padding: 5px 10px;
    /* Font size untouched at 16px. It fits inside 34px, and shrinking input
       text is the one change that would make the row harder to read rather
       than tidier. */
  }
  .page-light.page-admin .row-actions-flex { gap: 6px; }
}

/* The checkbox row is the third thing in that line, and at 28px against 44px
 * controls a bottom alignment leaves its centre 8px low - which is what "off
 * centre" looks like once the button itself is fixed. Giving it the control
 * height lets its own `align-items: center` do the centring. */
.page-admin .log-controls .checkbox-row,
.page-admin .form-panel .checkbox-row { min-height: 44px; }

/* THE FIX FOR "Refresh is off center of others", AND IT WAS NOT A BUTTON BUG.
 *
 * `.log-controls label` and `.form-panel label` both clear the global label's
 * bottom margin. `.page-light label` then sets `margin-bottom: 8px` again, at
 * the SAME specificity (0,1,1) and further down the file, so it wins - the
 * documented light-surface trap, hit again.
 *
 * The row aligns with `align-items: end`, and flex aligns MARGIN boxes. So the
 * labelled controls ended 8px above where their margin box said they were,
 * while the bare Refresh button (no label, no margin) sat at the true bottom
 * and looked dropped. It was the labels that were wrong, not the button.
 *
 * Measured before: label bottom 369, button bottom 377, container 85px tall for
 * 77px of content. After: every control in the row ends on the same line.
 */
.page-light .log-controls label,
.page-light .form-panel label { margin-bottom: 0; }

/* An `.inline-form` is a control plus its own submit. Without an explicit
 * cross-axis alignment the flex default is `stretch`, so the button grows to
 * whatever the tallest sibling is - and when that sibling is a LABEL COLUMN
 * (caption above input) the button matches the caption too, standing 30px
 * taller than every other button on the page. Measured: Save limit came out
 * 78px beside a 48px Turn off approval. This is the "not matching" half. */
.inline-form { align-items: end; }

/* ─── The post-scan step list (console) ──────────────────────────────────
 * A COLUMN of controls, each with the sentence that says what it does. It
 * replaced a three-across `.actions` row on 2026-07-31, where there was no
 * room to caption anything and the single note underneath described only the
 * first control.
 *
 * `list-style: none` on an `<ol>` is deliberate and is not "an unordered list
 * written wrong": the order carries meaning for anyone reading the markup and
 * for a screen reader, while the console already numbers this journey 01 to 04
 * in the rail below. A second visible 1-2-3 beside it would be a different
 * count in the same notation.
 *
 * The li is a GRID, not a flex row, so the control and its note stack without
 * the note being pulled onto the control's line. `justify-self: start` keeps a
 * button at its natural width instead of stretching across the column, and the
 * shared min-width lines the three of them up: three ragged left-aligned
 * buttons in a column read as three unrelated things.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .next-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 20px; }
.page-light .next-steps > li { display: grid; gap: 8px; }
.page-light .next-steps > li > form,
.page-light .next-steps > li > .btn { justify-self: start; }
.page-light .next-steps > li > form > button,
.page-light .next-steps > li > .btn { min-width: 210px; }
.page-light .next-steps .note { margin: 0; }
/* Step 1's button shares a row with its Optional tag. The wrapper exists
   because `> li > form` selectors above address a DIRECT child, so the form
   keeps its own rules through the wrapper's copies below. */
.page-light .next-steps > li > .step-head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.page-light .next-steps > li > .step-head > form > button { min-width: 210px; }
/* Uppercase mono, the instrument voice used by every other label of this
   kind. --steel measures 4.58:1 here, the same value the progress rail's
   not-yet ordinal was moved to when 1.56:1 failed the contrast pass. */
.page-light .step-optional {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel);
}

/* ─── Forms ────────────────────────────────────────────────────────────── */

.page-light label {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--cream);
  margin-bottom: 8px;
}
.page-light input[type="text"],
.page-light input[type="password"],
.page-light input[type="number"],
.page-light select {
  background: var(--panel);
  border: 1px solid var(--border-hover);
  border-radius: 4px;
  color: var(--cream);
  min-height: 48px;
  padding: 10px 14px;
  /* 16px floor, or iOS Safari zooms the page on focus. */
  font-size: 16px;
  font-family: var(--font-body);
}
.page-light input:focus { border-color: var(--orange); }
.page-light input::placeholder { color: var(--no-data); }

/* The row that carries a readout and its control. White card, not a sunken
   panel: on a light ground a filled well reads as disabled. */
.page-light .form-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 18px 20px;
  gap: 14px;
}
.page-light .form-panel label { margin-bottom: 0; }

/* A panel that pairs a CAPTIONED field with a button aligns their BOTTOMS, not
 * their centres.
 *
 * `.align-center` is `align-items: center`, which is right when every child is
 * a bare control of the same height, as on /kit's two answer buttons. It is
 * wrong the moment one child is a `<label>` wrapping a caption ABOVE its input:
 * centring then matches the midpoint of caption-plus-input against the midpoint
 * of the button, and the button floats above the field it belongs to.
 *
 * Measured at 1280 before the fix: the console's admin lane button sat 15px
 * high, and /verify's sat 20px high because a file input is 58px rather than
 * 48px. Owner spotted the admin one by looking; both were the same rule.
 *
 * Scoped with :has(> label) so it only touches the captioned case. Affects
 * three forms: /admin/link and /link/manual on the console, and /verify. */
.page-light .form-panel.align-center:has(> label) { align-items: flex-end; }

/* ─── Cards, rows, steps ───────────────────────────────────────────────── */

.page-light .card {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  padding: 22px;
}
.page-light .card-lg { padding: 26px 24px; }
.page-light .card h2 { font-size: 20px; letter-spacing: -0.01em; margin: 0 0 10px; }
.page-light .card h3 { margin: 0 0 14px; font-size: 17px; }
.page-light .card p { max-width: 62ch; margin: 0 0 8px; }
.page-light .card p:last-of-type { margin-bottom: 0; }

/* The two-column Kept / Not kept pair. A middle dot rather than a bullet, so
   the list reads as a set of statements and not as instructions. */
.page-light .record-list { display: flex; flex-direction: column; gap: 11px; font-size: 16px; }
.page-light .record-list > div { display: flex; gap: 10px; }
.page-light .record-list .dot { color: var(--no-data); flex: none; }
.page-light .col-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
.page-light .col-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; }
/* Four abreast at 880px, then two, then one. Kept for the col-N family even
   though nothing uses it right now: /how's four step block was its only caller
   and was removed 2026-07-30. Its two descendant rules went with it, because
   they tuned a font size for step blurbs that no longer exist and the comment
   explaining them had become a lie. `.ordinal` went for the same reason: the
   rail on the console uses `.flow-ordinal`, which is a separate rule. */
.page-light .col-4 { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 20px; }

/* One card per archive, and still no combined-total style, because there is
   still no combined total. */
.page-light .source-card {
  border: 1px solid var(--border);
  border-top: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  padding: 22px;
  margin-bottom: 12px;
}
/* WRAPS, since 2026-07-31. The head row carried a name and at most one chip
   and could not overflow; the filing kit now adds a reply estimate at the far
   end, and three items do not fit a 284px column. Wrapping is the only honest
   option here: a nowrap row would either clip the estimate or push the card
   into horizontal overflow, and the estimate is a whole phrase rather than
   something an ellipsis can shorten. */
.page-light .source-head { align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
.page-light .source-name {
  font-family: var(--font-head);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}
.page-light .source-count {
  font-family: var(--font-mono);
  font-size: 34px;
  font-weight: 500;
  line-height: 1.1;
  color: var(--orange);
  font-variant-numeric: tabular-nums;
}
/* The no-data dash. #8C949B is 3.1:1, which is large-text only, and this is
   the only place it appears: 34px, and never a zero. */
.page-light .source-count.readout-fail { color: var(--no-data); }
.page-light .count-prefix {
  font-family: var(--font-body);
  font-size: 17px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--cream-dim);
}
.page-light .source-detail { font-family: var(--font-mono); font-size: 14px; color: var(--cream-dim); margin-top: 6px; }
.page-light .source-card p { color: var(--cream-dim); font-size: 16px; }
/* Who fetched this archive's figure, shown only when it was not this server.
   Quiet on purpose: nothing has gone wrong, so it must not compete with the
   "no answer" copy in the same card, which is a real finding. It sits directly
   under the detail line it qualifies, for the same reason the PullPush coverage
   note on /kit sits under the count rather than at the foot of the card.
   `.source-card p` is (0,2,1) and this is (0,2,0), so the size is restated
   here rather than inherited, or the rule above silently wins at 16px. */
.page-light .source-card p.source-origin {
  font-size: 14px;
  color: var(--steel);
  margin-top: 6px;
}
.page-light .sub-list { gap: 6px; margin-top: 14px; }
/* A subreddit name is a name, not a state label. The uppercase treatment that
   is right on a status chip renders r/PLC as R/PLC, which is not what the
   subreddit is called. Same reasoning as StopNCII.org on the NCII page. */
.page-light .sub-list .chip { text-transform: none; letter-spacing: 0.02em; font-size: 12px; }

.page-light .source-card-collapsed { margin-bottom: 12px; }
.page-light .source-card-collapsed > summary::before { color: var(--orange); }

/* ─── The reply estimate on a filing kit channel ─────────────────────────
 * DELIBERATELY NOT A `.chip`, and that is a content rule wearing a CSS coat.
 * The chip beside a channel name means "items held at this channel": a count
 * of the person's own material, measured by us. This is neither measured nor
 * about them, it is how long the operator is likely to take. Rendered in the
 * chip treatment it would read as a second measurement, which is the same
 * mistake as putting the RedGifs link count in that slot.
 *
 * So: no fill, a hairline, muted label, and the value in full-strength ink.
 * It reads as an annotation on the row rather than a state of the archive.
 *
 * `margin-left: auto` pushes it to the far end of the head row, away from the
 * chip it must not be confused with. It is on this element rather than on a
 * `.topbar-spacer` div because the row now wraps: an empty spacer would be a
 * flex item of its own and can wrap onto a line by itself.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .channel-eta {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--cream-dim);
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 4px 9px;
}
/* THE VALUE IS WHAT MUST NOT BREAK, NOT THE WHOLE PILL. `nowrap` on the
   element measured 236px and spilled past the card's inner edge at 320px,
   where the content box is about 250px. Scoped to the value, the label wraps
   to a second line inside the pill and "1 to 2 weeks" still reads as one
   quantity instead of splitting across lines as "1 to 2" and "weeks". */
.page-light .channel-eta strong { color: var(--cream); font-weight: 600; white-space: nowrap; }

/* ─── Coverage note ──────────────────────────────────────────────────────
 *
 * What an archive actually covers, rendered directly under the count it
 * qualifies. See the block comment in kit.html for why it sits there.
 *
 * DELIBERATELY QUIETER THAN `.info-box`. Nothing has gone wrong when this
 * renders: the archive answered correctly, about a smaller window than the
 * reader assumes. Amber would say "warning" and the blue info-box is already
 * this card's action warning, so this is a plain block with a steel rule,
 * which reads as an annotation on the number rather than a second alert.
 *
 * The date is a `<span>`, not a separate paragraph, so it cannot be separated
 * from the sentence it dates by any later spacing rule. It stays on its own
 * line at every width via `display: block`, and carries its own smaller size
 * so the claim reads first and its provenance second.
 * ──────────────────────────────────────────────────────────────────────── */
.page-light .coverage-note {
  font-size: 15px;
  line-height: 1.62;
  color: var(--cream-dim);
  background: var(--inset);
  border-left: 3px solid var(--steel);
  border-radius: 3px;
  padding: 10px 14px;
  max-width: 70ch;
}
.page-light .coverage-note-date {
  display: block;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--steel);
}

/* ─── Chips ────────────────────────────────────────────────────────────── */

.page-light .chip {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.06em;
  padding: 3px 8px;
  border-radius: 3px;
  border: 0;
}
.page-light .chip-active { background: var(--ok-bg); color: var(--ok-ink); }
.page-light .chip-pending { background: var(--amber-bg); color: var(--amber); }
.page-light .chip-alert { background: var(--red-bg); color: var(--alert-red); }
.page-light .chip-admin { background: var(--panel-alt); color: var(--orange); }
.page-light .chip-info { background: var(--panel-alt); color: var(--orange); }
.page-light .chip-cleared { background: var(--ok-bg); color: var(--ok-ink); }

/* ─── Banners ──────────────────────────────────────────────────────────── */

/* The ladder is by what the reader must DO: info nothing, notice something,
   error a failure, hazard a stop. Every one of them renders ABOVE the thing it
   is about. */
.page-light .notice,
.page-light .error-box,
.page-light .info-box,
.page-light .hazard-banner {
  border-radius: 4px;
  padding: 16px 18px;
  font-size: 16px;
  line-height: 1.62;
  margin-bottom: 26px;
}
.page-light .info-box {
  background: var(--panel-alt);
  border: 1px solid var(--info-border);
  border-left: 3px solid var(--orange);
}
.page-light .notice {
  background: var(--amber-bg);
  border: 1px solid var(--amber-border);
  border-left: 3px solid var(--amber);
  color: var(--cream);
}
.page-light .error-box {
  background: var(--red-bg);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--alert-red);
  color: var(--alert-red);
}
/* The hazard stripes go. A barber-pole fill on paper reads as a warning
   sticker; the weight here comes from the red ink and the rule, and the words
   are doing the work either way. */
.page-light .hazard-banner {
  background: var(--red-bg);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--civil-red);
  color: var(--alert-red);
  font-family: var(--font-body);
  font-size: 16px;
}
.page-light .info-box strong,
.page-light .notice strong,
.page-light .error-box strong,
.page-light .hazard-banner strong { font-weight: 600; }

/* ─── Scan status ──────────────────────────────────────────────────────── */

.page-light .scan-status {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  padding: 20px 22px;
  max-width: 540px;
}
/* Still indeterminate, and for the same reason as the dark surface: MAX_PAGES
   is a cap rather than an expectation, so a percentage would read 20% for an
   account that finishes in three pages. */
.page-light .scan-status::before { background: var(--orange); animation-duration: 1.8s; }
@media (prefers-reduced-motion: reduce) {
  .page-light .scan-status::before { width: 100%; opacity: 0.5; }
}
.page-light .scan-status-summary { font-family: var(--font-body); font-size: 16px; font-weight: 600; margin-bottom: 12px; }
.page-light .scan-status-row { font-family: var(--font-mono); font-size: 14px; gap: 14px; padding: 4px 0; }
.page-light .scan-status-name { flex: 1; min-width: 0; color: var(--cream); }
.page-light .scan-status-state { flex: none; color: var(--cream-dim); }
.page-light .scan-status-count { color: var(--cream); font-variant-numeric: tabular-nums; }
.page-light .scan-state-partial { color: var(--amber); }
.page-light .scan-state-failed { color: var(--alert-red); }
/* Restated for the light surface. `.page-light .scan-status-state` above sets
   the colour at equal specificity and later in the file, so the base
   .scan-state-cached rule loses here exactly the way the retry cooldown's
   opacity did. */
.page-light .scan-state-cached { color: var(--steel); font-style: italic; }
.page-light .scan-status-note { font-size: 15px; color: var(--steel); margin: 12px 0 0; }

/* ─── Lists, code, tables ──────────────────────────────────────────────── */

.page-light .steps { font-size: 17px; line-height: 1.62; }
.page-light .steps li { margin-bottom: 10px; }
.page-light code {
  background: var(--inset);
  border: 1px solid var(--border);
  font-size: 14px;
  color: var(--cream);
}
.page-light .table-wrap {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
}
.page-light table { font-size: 16px; }
.page-light th {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.07em;
  color: var(--steel);
  background: var(--inset);
  border-bottom: 1px solid var(--border);
  padding: 12px 16px;
}
.page-light td { padding: 12px 16px; border-bottom: 1px solid var(--rule-soft); }

/* ─── The hero ─────────────────────────────────────────────────────────── */

.page-light .hero {
  display: grid;
  grid-template-columns: minmax(320px, 62ch) auto;
  gap: 36px;
  align-items: start;
}
/* THE ARTWORK IS CLIPPED IN THE SUPPLIED FILE, and this only softens it.
   Both source PNGs were cut out of a PDF page by alpha knockout, and the
   eraser runs off the left and bottom edges of the frame: measured on
   `mascot.png`, 33 rows of column 0 and 25 columns of the bottom row are
   fully opaque, so the illustration ends in a razor-straight line rather
   than a drawn edge. The icon variant is cut too, at 19 rows.

   A mask cannot invent the missing art. It fades the cut so it reads as the
   image dissolving instead of as a rendering fault. THE REAL FIX IS THE
   ORIGINAL ASSET, which the handoff README also asks for: replace both files
   if a vector or high-resolution source turns up, then delete this rule and
   re-measure the edges. */
/* The hero control sits between the lede and the password note, so its two
   gaps have to match or it reads as attached to whichever side is tighter.
   Measured before this rule: 18px above, 0px below, because the note carried
   `mt-sm` and `.page-light .note { margin: 0 }` outranks it. Setting both
   margins here keeps the pair symmetric in one place instead of relying on
   two utility classes that were never both in effect. */
.page-light .hero .actions { margin: 24px 0; }

.page-light .hero-mascot {
  width: 230px;
  height: auto;
  justify-self: end;
  margin-top: 4px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 13px);
  mask-image: linear-gradient(to right, transparent 0, #000 13px);
}

/* The three answer states, shown as one grouped list rather than three cards:
   they are a legend, not three findings. */
.page-light .source-list {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  overflow: hidden;
}
.page-light .source-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 16px;
  align-items: baseline;
  padding: 20px 22px;
  border-bottom: 1px solid var(--rule-soft);
}
.page-light .source-row:last-child { border-bottom: none; }
.page-light .source-row p { margin: 0; color: var(--cream-dim); font-size: 16px; }
.page-light .source-row .source-count { font-size: 30px; }
.page-light .source-row .source-name { font-family: var(--font-body); font-size: 17px; }

/* ─── Small screens ────────────────────────────────────────────────────── */

/* The auto-fit grids collapse on their own. The hero is the exception, because
   its second column is an image and will happily squeeze the headline first. */
@media (max-width: 760px) {
  .page-light .hero { grid-template-columns: 1fr; gap: 20px; }
  .page-light .hero-mascot { justify-self: start; width: 180px; }
}

@media (max-width: 700px) {
  /* gap 6, not 10, and it is measured rather than chosen. /admin is the only
     header carrying two controls and no username: mark 160 + Back 60 +
     Sign out 82 = 302px of content in a 324px line. At gap 10 the three gaps
     took it to 332 and the header wrapped to two rows and 121px. At 6 it is
     320 and stays one row at 69px.

     THIS IS A 4px MARGIN, so treat it as fragile. It is affordable only
     because the light header is not sticky, which makes a wrap a one-time
     cost rather than a permanent one. If a page ever adds a third control,
     let it wrap rather than shaving this further, and never solve it by
     truncating the wordmark. */
  .page-light .topbar-inner { padding: 12px 18px; gap: 6px; }

  /* MEASURED AT 375px BEFORE THIS BLOCK: the header wrapped to three rows and
     stood 305px tall, 37% of an 812px viewport. It is not sticky, so that is a
     one-time cost rather than a permanent one, but it still pushed the whole
     tool below the fold on the page whose job is the tool.
     The tagline goes first because the brand rules already say it is plain
     text, visibly subordinate, and removable without the mark breaking. The
     mark and the username both survive at a smaller size. */
  .page-light .brand-tagline { display: none; }
  .page-light .brand-icon { width: 32px; height: 32px; }
  .page-light .wordmark { font-size: 17px; }
  /* 5.5em, measured. At 7em a 12-character account name renders 88px and the
     console header wrapped to two rows and 107px; the old 4-character fixture
     hid that, which is its own lesson about fixture choice. At 5.5em the row
     is 328px inside 339px and holds. The name is a reassurance rather than a
     control, so an ellipsis still answers "am I signed in as me".
     /admin's console view carries a third control and still wraps. That is
     accepted: the header is not sticky, so a wrap costs one screenful once. */
  .page-light .topbar-user { max-width: 5.5em; font-size: 12px; }
  .page-light .topbar button,
  .page-light .topbar .btn { padding: 8px 12px; font-size: 15px; }
  .page-light .wrap,
  .page-light .wrap-narrow,
  .page-light .wrap-wide { padding: 0 18px; }
  /* Same split as at desktop: the bar bleeds, the column carries the inset. */
  .page-light .site-footer { padding: 24px 0 36px; }
  .page-light .site-footer-inner { padding: 0 18px; }
  /* /press sets its own 16px inset at this width, which left its text 2px
     inside the header and footer. Match the rest of the site. */
  .page-light .kit { padding-left: 18px; padding-right: 18px; }
  .page-light section { padding-top: 44px; }
  /* Keep the sectionless first block in step with the section value above. */
  .page-light main > .wrap > :first-child:not(section),
  .page-light main > .wrap-wide > :first-child:not(section) { margin-top: 44px; }
  .page-light .hero h1 { font-size: 30px; }
  .page-light h1 { font-size: 28px; }

  /* Every body control takes the 48px floor. The 34px header exception exists
     to defend a sticky height budget, and this header is not sticky. */
  .page-light button,
  .page-light .btn { min-height: 48px; }
  .page-light .topbar button,
  .page-light .topbar .btn { min-height: 44px; }

  /* Content-width controls stack into a ragged column. Full width matches the
     pattern the rest of the product already uses at this size, and it is why
     the dark sheet widens `.form-panel > form` rather than the button: a form
     is a shrink-to-fit flex item, so widening the button alone does nothing. */
  .page-light .actions { gap: 10px; }
  .page-light .actions > form,
  .page-light .actions > form > button,
  .page-light .actions > button,
  .page-light .actions > .btn { width: 100%; }

  /* Same treatment for the post-scan column. The form needs the width itself:
     `justify-self: start` makes it shrink to fit, so widening only the button
     inside it resolves against the form's own content width and does nothing.
     That exact no-op is recorded twice in this file already. */
  .page-light .next-steps > li > form,
  .page-light .next-steps > li > .btn { justify-self: stretch; width: 100%; }
  .page-light .next-steps > li > form > button { width: 100%; }
  /* Step 1's wrapper takes the same stretch, and its form goes full width so
     the Optional tag wraps UNDERNEATH the button rather than being squeezed
     beside it. Without the form rule the flex row would shrink the button to
     make room for the tag. */
  .page-light .next-steps > li > .step-head { justify-self: stretch; width: 100%; gap: 8px; }
  .page-light .next-steps > li > .step-head > form,
  .page-light .next-steps > li > .step-head > form > button { width: 100%; }

  /* The head row wraps at this width, and `margin-left: auto` on a wrapped
     item strands it right-aligned on a line of its own. Let it sit under the
     name with everything else. */
  .page-light .channel-eta { margin-left: 0; }

  /* State and count stay on one line with the name; the name is what gives. */
  .page-light .scan-status-row { gap: 10px; }
  .page-light .scan-status-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}


/* ═══ LIGHT SURFACE, PART TWO: the other eleven pages ═════════════════════
 * Added 2026-07-29 when the rollout went site-wide. Same scope, same tokens,
 * same rule: read `--orange` as "the accent" and `--cream` as "the ink".
 * ═══════════════════════════════════════════════════════════════════════ */

/* ─── Measures ───────────────────────────────────────────────────────────
   880px is a reading column and is wrong for a sign-in card: at that width the
   two fields and the button float in the middle of an empty page. `wrap-narrow`
   is used by exactly login and signup, so it can be the form measure. */
/* `width: 100%` is required once the parent is a flex column. `margin: 0 auto`
   sets auto margins on what is now the CROSS axis, and auto cross margins
   disable stretch, so the card fell back to its shrink-to-fit size and
   measured 366px instead of 460px. An explicit width restores the intended
   measure and the auto margins still centre it. */
.page-light .wrap-narrow { max-width: 460px; width: 100%; }

/* THE SIGN-IN CARD IS CENTRED VERTICALLY, and that is also what fixes the
   complaint that it looked right-justified.

   It never was. Measured in the owner's own browser at 1427px: card centre
   713.67, page centre 713.5, off by 0.17px. What made it read as pushed right
   is that the page shows two left edges. The wordmark and the footer links sit
   at the 880px band's inner edge, the card 182px inside that, and with 295px
   of dead space below there was nothing for the eye to read it as centred
   WITHIN. A card floating in the middle of the page is obviously centred; one
   stranded at the top gets compared to the header instead.

   The vertical gap was also a real regression: the dark sheet had
   `margin: 80px auto` here and the light rewrite dropped it to `0 auto`, so
   the heading sat flush against the header rule.

   FLEX COLUMN, NOT GRID, and that is a measured choice rather than taste.
   `display: grid` with `align-content: safe center` sized the implicit column
   to max-content and the card came out 366px instead of 460px. A flex column
   leaves the cross axis on `stretch`, so the child still fills the width and
   its own `margin: 0 auto` does the horizontal centring exactly as before.
   Do not touch the horizontal axis here: it is measured correct at 0.17px.

   `safe` IS LOAD BEARING. Without it a form taller than the viewport centres
   by overflowing equally in both directions, and the top of /signup goes above
   the scroll origin where it cannot be reached. `safe` falls back to start. */
.page-light:has(.wrap-narrow) main {
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  padding: 48px 0;
}
/* The tone-exception page wants a slightly tighter column than the console,
   per the design bundle. Someone reading it is not scanning a table. */
.page-light.page-plain .wrap { max-width: 720px; }

/* ─── File input, the only one in the product (/verify) ─────────────────── */

.page-light input[type="file"] {
  background: var(--panel);
  border: 1px solid var(--border-hover);
  border-radius: 4px;
  color: var(--cream);
  padding: 10px 12px;
  font-family: var(--font-body);
  /* 16px floor. The dark sheet drops this to 14px for desktop density and
     corrects it back inside its mobile block; there is no reason to repeat
     that dance on a surface with more room. */
  font-size: 16px;
}
.page-light input[type="file"]::file-selector-button {
  background: var(--inset);
  border: 1px solid var(--border-hover);
  border-radius: 4px;
  color: var(--cream);
  padding: 8px 14px;
  margin-right: 12px;
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
}
.page-light input[type="file"]::file-selector-button:hover { border-color: var(--orange); color: var(--orange); }

/* ─── Admin: tiles ───────────────────────────────────────────────────────
   The markup here is `.tile > .value` and `.tile > .label`, NOT the
   `.tile-value` / `.tile-label` the design bundle assumes. Matching the
   markup rather than renaming it in the template keeps this change to the
   stylesheet, which is the smaller diff on a page full of live forms. */
.page-light .tiles { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 10px; margin-bottom: 26px; }
.page-light .tile {
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 1px solid var(--border);
  border-radius: 6px;
  padding: 16px 14px;
  text-align: left;
}
.page-light .tile .value {
  font-family: var(--font-mono);
  font-size: 26px;
  font-weight: 500;
  line-height: 1.1;
  color: var(--orange);
  font-variant-numeric: tabular-nums;
}
.page-light .tile .label {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--steel);
  margin-top: 6px;
}

/* ─── Admin: the people table and its controls ───────────────────────────
   THE FLEX CONTAINER IS `.row-actions-flex`, NEVER THE `<td>`. A flexed cell
   drops out of table layout: the browser wraps it in an anonymous cell, the
   real <td> carrying the row's border-bottom shrinks to its content height,
   and the border misaligns by about 13px depending on how many buttons wrap.
   Already fixed once. Do not "simplify" it back. */
.page-light .row-actions { vertical-align: middle; }
.page-light .row-actions-flex { gap: 8px; }

/* A row of five 48px controls turns one account into a paragraph of buttons.
   These are dense administrative controls reached deliberately with a mouse,
   which is the same reasoning the dark sheet uses for its header exception. */
/* 44px AT EVERY WIDTH, raised from 38px on 2026-08-04. A narrow-width media
   query already raised these two to 44 and the desktop value was left behind,
   so the densest table in the product had the only controls on the site under
   the tap floor while every other control is 48. Nothing else on the page
   moves: the row height was already set by the 44px-tall cells around them. */
.page-light .row-actions button { min-height: 44px; padding: 7px 12px; font-size: 14px; }
.page-light .row-actions input[type="password"] { min-height: 44px; width: 150px; font-size: 16px; padding: 7px 10px; }
.page-light .inline-form { gap: 6px; }

/* 28px, because the label IS the target and an 18px box inside a 23px label
   misses the 24px WCAG 2.2 minimum by a pixel. The mobile block raises the
   same rule to 44px; this is the desktop floor, not a replacement for it. */
.page-light .checkbox-row { flex-direction: row; align-items: center; gap: 8px; min-height: 28px; }
.page-light .checkbox-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--orange); }
.page-light .log-controls { gap: 14px; align-items: end; margin-bottom: 12px; }
.page-light .log-controls label { font-size: 14px; font-weight: 500; gap: 6px; }
.page-light .log-controls input[type="number"] { width: 120px; }
.page-light .scroll-hint { color: var(--steel); font-size: 15px; }

/* Machine output, not prose. The one place a denser mono is right, because a
   journald line is long and wrapping it costs more than the size gains. */
.page-light .log-box {
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.62;
  padding: 16px;
}

/* ─── The filing kit ─────────────────────────────────────────────────────
   `.filing-text` is what a person selects, copies and sends themselves. It has
   to read as a block of text to be taken away, not as a pull quote. */
.page-light .filing-head { gap: 10px; margin: 18px 0 8px; align-items: center; }
/* 44px at every width, raised from 38px on 2026-08-04 for the same reason as
   `.row-actions button`: a narrow-width query already raised it to 44 and the
   desktop value was left behind. This is the Copy control beside every filing
   block on /kit, so it is one of the most-pressed buttons in the product. */
.page-light .btn-copy { min-height: 44px; padding: 7px 14px; font-size: 14px; min-width: 96px; }
.page-light .btn-copy:disabled { opacity: 1; border-color: var(--orange); color: var(--orange); }
.page-light .filing-text {
  background: var(--inset);
  border: 1px solid var(--border);
  border-left: 3px solid var(--orange);
  border-radius: 4px;
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.62;
  padding: 16px 18px;
  margin: 0 0 16px;
}

.page-light .gate-list li { padding: 12px 0; border-bottom: 1px solid var(--rule-soft); gap: 12px; }
.page-light .gate-list li:last-child { border-bottom: none; }
/* Alert red, never civil red: this is TEXT, and civil red is 3.0:1 on white
   and fails at every size. Same rule the dark sheet follows for the same
   reason, at a different pair of values. */
.page-light .gate-unmet .gate-mark { color: var(--alert-red); }
.page-light .gate-met .gate-mark { color: var(--ok-ink); }

/* Quieter than a card, never absent: we never asked RedGifs anything, so we
   can never say it is empty. */
.page-light .source-card-collapsed {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 18px 22px;
}
/* The disclosure toggle measured 29px, under the tap floor, because a summary
   sizes to its text and this one is the only control on the card. It is how
   someone with RedGifs content reaches the process for it, so it is not a
   control to leave hard to hit. */
.page-light .source-card-collapsed > summary { min-height: 44px; align-items: center; }

/* ─── The NCII lane, /help/ncii ──────────────────────────────────────────
   The one tone exception. Same palette, same components, plain sentences, no
   uppercase, no klaxons, no mascot. Someone reading this page is having one of
   the worst weeks of their life and the instrumentation voice would be
   flippant. Scoping is what makes that rule real rather than a comment. */

.page-light.page-plain { line-height: 1.62; }
.page-light.page-plain h1 { font-size: 30px; line-height: 1.2; letter-spacing: -0.015em; }
.page-light.page-plain h2 { font-size: 20px; letter-spacing: -0.01em; margin-top: 8px; }
.page-light.page-plain .spec { display: none; }
/* Lets a service render as its real name: StopNCII.org, not STOPNCII.ORG. */
.page-light.page-plain .source-name { text-transform: none; letter-spacing: 0; }

/* THE HEADER IS NO LONGER STICKY, so the fork anchors no longer land a card
   underneath it. The old value was 96px, sized to clear the tallest sticky
   header measured across every width. Kept small rather than dropped to zero
   so the card does not sit flush against the viewport edge. RE-MEASURE THIS
   IF THE HEADER EVER BECOMES STICKY AGAIN, and measure whether the warning is
   VISIBLE, not whether the element offset is zero. */
.page-light.page-plain .source-card[id] { scroll-margin-top: 24px; }

.page-light.page-plain .fork { display: flex; flex-direction: column; gap: 12px; margin: 0 0 10px; }
.page-light.page-plain .fork-card {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 64px;
  padding: 16px 18px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--orange);
  border-radius: 6px;
  color: var(--cream);
}
.page-light.page-plain .fork-card:hover { background: var(--panel-alt); border-color: var(--orange); }
.page-light.page-plain .fc-main { font-family: var(--font-head); font-weight: 600; font-size: 17px; letter-spacing: 0; }
.page-light.page-plain .fc-sub {
  display: block;
  margin-top: 3px;
  font-weight: 400;
  font-size: 15px;
  color: var(--cream-dim);
}
.page-light.page-plain .fc-arrow { margin-left: auto; font-family: var(--font-mono); color: var(--orange); }
.page-light.page-plain .fork-note { font-size: 15px; color: var(--steel); margin: 0 0 22px; }

/* `who` answers "is this one for me" at a glance. It used to be glued to the
   front of a 300-character description in the same muted run, where the
   scannable half was invisible. */
.page-light.page-plain .who {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--steel);
  margin: 14px 0 4px;
}
.page-light.page-plain .detail { font-size: 16px; color: var(--cream-dim); margin: 0; }

/* The one phone number in the product, given a dialable target rather than
   sitting in a readout span as undialable display text. */
.page-light.page-plain .tel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  max-width: 340px;
  min-height: 48px;
  margin-top: 10px;
  background: var(--panel);
  border: 1px solid var(--border-hover);
  border-radius: 4px;
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 15px;
}
.page-light.page-plain .tel:hover { border-color: var(--orange); color: var(--orange); }
.page-light.page-plain .tel .ic { color: var(--orange); }

/* The two service links are the only thing anyone came to this page to press. */
.page-light.page-plain .btn-block { display: flex; width: 100%; min-height: 52px; }

@media (max-width: 700px) {
  /* The three 44px overrides that were here are gone: `.row-actions button`,
     `.row-actions input[type=password]` and `.btn-copy` are all 44px in their
     base rules now, so these only restated it at narrow widths. */
  .page-light .tiles { grid-template-columns: repeat(2, 1fr); }
}


/* ═══ /press: the rules a corrupted line ate ══════════════════════════════
 * `.kit .s-.dodont { ... }` was a `.s-body` rule spliced onto a `.dodont`
 * rule with everything between them lost, so the do/don't grid never applied
 * and seven classes the template uses had no styling at all. Restored below
 * and kept separate from the light work so the two are reviewable apart.
 * ═══════════════════════════════════════════════════════════════════════ */

.kit .s-body { font-family: var(--font-body); font-size: 19px; line-height: 1.62; }
.kit .s-mono { font-family: var(--font-mono); font-size: 15px; letter-spacing: .04em; }
.kit .ts-use { margin: 0; font-size: 14px; color: var(--cream-dim); }

.kit .feat { margin: 0 0 26px; padding-left: 20px; color: var(--cream-dim); font-size: 15px; }
.kit .feat li { margin-bottom: 9px; }

.kit .boiler {
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--border-hover); padding: 18px 20px; margin-bottom: 14px;
}
.kit .boiler p { margin: 0; color: var(--cream-dim); font-size: 15px; max-width: 74ch; }
.kit .bl {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--steel); margin: 0 0 10px !important;
}

.kit .traits { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 26px; }
.kit .traits span {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--cream-dim);
  border: 1px solid var(--border); padding: 6px 11px;
}

.kit .dodont { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }


/* ═══ /press on the light surface ═════════════════════════════════════════
 * The `.kit` rules are almost all `var()` driven, so they re-value on their
 * own. What needs saying here is the handful of literals, plus the two dark
 * motifs, plus the specimen colours that this page DOCUMENTS rather than
 * merely uses. A swatch showing the wrong hex is worse than an ugly page.
 * ═══════════════════════════════════════════════════════════════════════ */

/* Dark-instrument motifs, same decision as the console. */
.page-light .kit .cover-rings,
.page-light .kit .wipe { display: none; }

/* Was a literal graphite gradient. */
.page-light .kit .runhead {
  background: linear-gradient(180deg, var(--panel) 70%, rgba(255, 255, 255, 0.86));
  color: var(--steel);
}

/* Sentence case, no rule beneath, matching the header lockup. */
.page-light .kit h1,
.page-light .kit h2,
.page-light .kit h3 { text-transform: none; letter-spacing: -0.015em; }
.page-light .kit .wordmark {
  text-transform: none;
  border-bottom: none;
  padding-bottom: 0;
  letter-spacing: -0.01em;
}
.page-light .kit .wordmark .tool { margin-left: 0; }
.page-light .kit .cover-lockup { clip-path: none; border-radius: 6px; }
.page-light .kit .eyebrow,
.page-light .kit .secnum,
.page-light .kit .runhead,
.page-light .kit .bl,
.page-light .kit .ts-label,
.page-light .kit figcaption,
.page-light .kit .traits span { text-transform: uppercase; }

.page-light .kit .spec-art,
.page-light .kit .facts > div,
.page-light .kit .boiler,
.page-light .kit .ts,
.page-light .kit .col,
.page-light .kit .tablewrap { background: var(--panel); border-radius: 6px; }
.page-light .kit .facts { background: var(--border); }
.page-light .kit .facts > div { border-radius: 0; }

/* The literal row divider in the palette table. */
.page-light .kit .pal td { border-bottom: 1px solid var(--rule-soft); }

/* Green is not a token in either palette. The dark sheet reaches for #5fa46a
   as a one-off; on white that fails, so these use the chip ink, which is
   measured at 5.7:1 on its own fill and 6.4:1 on white. */
.page-light .kit .pal td.r.ok,
.page-light .kit .col.do h3 { color: var(--ok-ink); }
.page-light .kit .col.do { border-top: 2px solid var(--ok-ink); }
.page-light .kit .pal td.r.warn { color: var(--amber); }
.page-light .kit .col.dont { border-top: 2px solid var(--civil-red); }

/* THE SWATCHES DOCUMENT THE PALETTE, so they carry literal values on purpose
   and must be the values the site actually ships. These are the light ones.
   If a token changes, this block and the table in press.html both become
   wrong, and the page is the one place where that is a factual error rather
   than a cosmetic one. */
.page-light .kit .sw[data-c=graphite] { background: #fbfbf9; }
.page-light .kit .sw[data-c=panel]    { background: #ffffff; }
.page-light .kit .sw[data-c=cream]    { background: #17191c; }
.page-light .kit .sw[data-c=creamdim] { background: #5c6570; }
.page-light .kit .sw[data-c=orange]   { background: #1a5f9e; }
.page-light .kit .sw[data-c=alert]    { background: #a32a21; }
.page-light .kit .sw[data-c=civil]    { background: #d9776e; }
.page-light .kit .sw[data-c=steel]    { background: #6b747e; }
.page-light .kit .sw[data-c=blue]     { background: #f3f7fb; }
/* Three swatches the dark table never had, because these roles were literals
   there rather than tokens. */
.page-light .kit .sw[data-c=amber]    { background: #8a5a00; }
.page-light .kit .sw[data-c=nodata]   { background: #8c949b; }
.page-light .kit .sw[data-c=railidle] { background: #c6ccd1; }
.page-light .kit .sw[data-c=ctrl]     { background: #8b9491; }

/* The hazard block loses its barber-pole fill here for the same reason the
   console's did: on paper a striped fill reads as a warning sticker. */
.page-light .kit .hazard {
  background: var(--red-bg);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--civil-red);
  color: var(--alert-red);
  font-family: var(--font-body);
  font-size: 15px;
  border-radius: 4px;
}
.page-light .kit .notice {
  background: var(--panel-alt);
  border: 1px solid var(--info-border);
  border-left: 3px solid var(--orange);
  color: var(--cream-dim);
  border-radius: 4px;
}
.page-light .kit .signoff { text-transform: none; letter-spacing: -0.01em; }


/* A long token in inline <code> must be allowed to break, or it clips.
 * Measured on /kit at 320 to 340px: the "Also reachable at:" line cut 40px off
 * the Arctic Shift contact address with no ellipsis and no scroller, so it
 * LOOKED complete and was not - the worst failure mode for a filing contact
 * route. `anywhere` rather than `break-word` because a bare email has no break
 * opportunities at all. Scoped to prose code: the filing <pre> blocks keep
 * their own scroller and are untouched.
 *
 * That address was removed on 2026-08-04 and the line now carries a Discord
 * handle, which is shorter. THE RULE STAYS: the next long token put on that
 * line would clip exactly the same way, and the failure is silent. */
.page-light .muted code, .page-light .note code { overflow-wrap: anywhere; }


/* ─── Progress steps rail, on the console ─────────────────────────────────
 * TWO TREATMENTS ONLY, and the rule is about what can be SEEN rather than
 * about progress. Owner, 2026-07-29: "Show the status that we can see is my
 * opinion. The ones we cannot see keep grey." A step is accent only when the
 * request can PROVE it happened, so steps 03 and 04 are grey permanently.
 * Spec: PROMPT_mediawipetool-progress-steps.md.
 *
 * --rail-idle is 1.56:1 on this surface, far below any text threshold, so it
 * is confined to the ring, the icon strokes and the dashes. THE TITLE NEVER
 * DIMS, and since 2026-07-30 the ordinal does not either: it sits in --steel
 * on a not-yet step. Only the graphics carry state.
 *
 * `Done` is real visible text, not a hidden status word, because .flow-rail is
 * display:none below 900px. Under a hidden-only scheme hue would be the sole
 * cue for every reader on a phone.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .flow-track { position: relative; --flow-gap: 30px; }

/* The rail spans centre to centre of the outer two nodes.
 *
 * 12.5% ALONE IS WRONG AND LOOKS ALMOST RIGHT, which is why this is spelled
 * out. With four 1fr columns and three gaps, a column is (W - 3g)/4 wide, so
 * the first centre sits at (W - 3g)/8, not at W/8. The difference is a constant
 * 3g/8, which is 11.25px at a 30px gap: the rail would start 11px inside the
 * first node's centre at every width, and the dashes would read as offset.
 *
 * Derived from --flow-gap rather than hard-coded, so changing the gap keeps the
 * rail aligned instead of quietly breaking it.
 *
 * 47px is half the 94px icon, which is what puts the line through the centres. */
.page-light .flow-rail {
  position: absolute;
  top: 47px;
  left: calc(12.5% - (var(--flow-gap) * 3 / 8));
  right: calc(12.5% - (var(--flow-gap) * 3 / 8));
  height: 2px;
  z-index: 0; display: flex; pointer-events: none;
}
/* Dashed rather than a lighter solid, so the not-yet state survives greyscale
   and does not rely on hue. */
.page-light .flow-seg {
  flex: 1; height: 2px;
  background: repeating-linear-gradient(90deg, var(--rail-idle) 0 6px, transparent 6px 13px);
}
.page-light .flow-seg.is-done { background: var(--orange); }

/* The gap comes from --flow-gap so the rail's inset above stays derived from
   the same number. Change it in ONE place. */
.page-light .flow-steps {
  position: relative; z-index: 1; display: grid;
  grid-template-columns: repeat(4, 1fr); gap: var(--flow-gap);
  margin: 0; padding: 0; list-style: none;
}

/* The step's own `color` is what the icon inherits: every stroke and fill
   inside the SVG is currentColor, which is what lets ONE icon file serve both
   states with no second copy to keep in step. */
.page-light .flow-step { text-align: center; color: var(--rail-idle); }
.page-light .flow-step.is-done { color: var(--orange); }

.page-light .flow-icon { width: 94px; height: 94px; margin: 0 auto 14px; }
.page-light .flow-icon svg { display: block; width: 94px; height: 94px; }

.page-light .flow-meta {
  display: flex; justify-content: center; align-items: baseline;
  gap: 8px; margin-bottom: 5px; min-height: 18px;
}
.page-light .flow-ordinal {
  font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.09em;
}
/* The one contrast failure the 2026-07-30 audit found in the whole product was
 * this ordinal inheriting --rail-idle: visible 13px text at 1.56:1. It is
 * aria-hidden and duplicates the list position, but a sighted reader still sees
 * a smudge. Steel is 4.58:1, and the ordinal now reads as a quiet label on both
 * states rather than as part of the greyed graphic. The RING AND ICON keep
 * inheriting --rail-idle from the step, which is what still carries not-yet. */
.page-light .flow-step:not(.is-done) .flow-ordinal { color: var(--steel); }
/* Accent at 6.5:1, and it is the only text in the component that appears or
   disappears with the state. */
.page-light .flow-done {
  font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.09em;
  color: var(--orange);
}

.page-light .flow-step h3 {
  margin: 0; font-family: var(--font-head); font-size: 19px;
  font-weight: 600; letter-spacing: -0.01em; text-transform: none;
  color: var(--cream); line-height: 1.3;
}

/* The rail is HIDDEN below 900px, not redrawn vertically: a wrapped two column
   grid has no single line to follow, and a vertical rail through one is worse
   than none. This is the reason `Done` has to be visible text. */
@media (max-width: 900px) {
  .page-light .flow-steps { grid-template-columns: 1fr 1fr; row-gap: 34px; }
  .page-light .flow-rail { display: none; }
}
/* At one-up the steps lay out ACROSS rather than stacked.
 *
 * MEASURED, and it is the reason this block exists: a 94px icon centred above a
 * two word title costs about 230px per step, so the four of them plus the
 * heading and the note came to 923px at 375px wide. That was 22.6% of the whole
 * mobile page for a readout, on the page that was deliberately shortened.
 * Icon left, ordinal and title right, and it drops to roughly a third of that.
 *
 * Done with grid areas rather than flex so NO MARKUP CHANGE IS NEEDED: the icon
 * takes column 1 across both rows, and .flow-meta and the h3 auto-place into
 * column 2. A flex row would have put all three children side by side. */
@media (max-width: 520px) {
  .page-light .flow-steps { grid-template-columns: 1fr; row-gap: 18px; }
  .page-light .flow-step {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    column-gap: 14px;
    align-items: center;
    text-align: left;
  }
  .page-light .flow-icon {
    grid-row: 1 / 3;
    width: 54px; height: 54px; margin: 0;
  }
  .page-light .flow-icon svg { width: 54px; height: 54px; }
  .page-light .flow-meta {
    justify-content: flex-start; margin-bottom: 1px; min-height: 0;
  }
  .page-light .flow-step h3 { font-size: 18px; }
}


/* ─── /support payment-source rows ────────────────────────────────────────
 * One bordered row per donation source: official brand icon + name, nothing
 * else (owner instruction 2026-08-01, replacing full cards with description
 * lines). The base .page-light .btn already carries the 48px min-height,
 * border and line-height 1.2, so this only adds the row layout.
 *
 * The icons are the providers' OFFICIAL assets, self-hosted under
 * /static/img/ because the CSP (default-src 'self') silently blocks
 * hotlinking. Nominative use: unmodified, never recoloured - which is also
 * why the row hover recolours the BORDER and TEXT only, never the icon.
 *
 * Column stack at every width on purpose: three 48px rows are compact
 * enough, and a horizontal row would need its own wrap handling at 320px. */
.page-light .support-options {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 420px;
}
.page-light .support-options .btn-pay {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
}
.page-light .btn-pay img { display: block; width: 22px; height: 22px; }
/* A source whose account is not set up yet: same row geometry so the list
 * does not jump when a link goes live, but visibly not a control - muted
 * text, dashed border, no hover, and a real "Coming soon" label because
 * grey alone is not a statement. */
.page-light .pay-pending {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  min-height: 48px;
  padding: 12px 24px;
  border: 1px dashed var(--border-hover);
  border-radius: 4px;
  font-family: var(--font-head);
  font-size: 17px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--cream-dim);
}
.page-light .pay-pending img { opacity: 0.55; }
.page-light .pay-soon {
  margin-left: auto;
  font-family: var(--font-mono, monospace);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel);
}
/* The header Donate link wraps the light topbar below ~380px (measured
 * 2026-08-01, with/without sweep). Hidden under 400px rather than shrinking
 * the header gaps: the pages that carry it still have their body ask one
 * tap away, and a wrapped sticky-adjacent header is a permanent cost. The
 * console never shows it at all - see _base.html. */
@media (max-width: 400px) {
  .topbar-donate { display: none; }
}

/* The kit's donate popup - the product's ONLY popup, per its own copy.
 * Placement is the owner's 2026-08-01 sketch: the box sits at the top of
 * the kit content and the arrow reaches UP out of it toward the Donate
 * button in the header. Elevated (shadow + accent border) so it reads as a
 * popup, not as another info box. Ships `hidden`; kit.js reveals it, so
 * no-JS readers never see an undismissable box. */
/* The popup card. Owner 2026-08-01: "make it pop out way more" - so it is
 * deliberately LOUDER than every other box on the surface: brighter ground
 * than the page, a border in the artwork's own blue, a real shadow, larger
 * type, and a pop-in. This is the product's ONLY popup; nothing else may
 * borrow this treatment.
 * #8fbde6 is a literal on purpose: it is sampled from the owner's swoosh
 * artwork (donate-arrow.webp), not a system token - same rule as the
 * swatch literals on /press. */
.page-light .donate-callout {
  position: relative;
  display: flex;
  align-items: center;
  gap: 20px;
  margin: 30px 0;
  padding: 22px 56px 22px 24px;
  background: linear-gradient(180deg, var(--panel), var(--panel-alt));
  border: 2px solid #8fbde6;
  border-radius: 16px;
  box-shadow: 0 16px 40px rgba(26, 95, 158, 0.22), 0 3px 10px rgba(26, 95, 158, 0.14);
  font-size: 17px;
  line-height: 1.62;
}
.page-light .donate-callout p { margin: 0; color: var(--cream); }
/* [hidden] MUST WIN over the display:flex above. An author `display` rule
 * beats the UA sheet's `[hidden] { display: none }` no matter the
 * specificity, so without this the x sets the attribute, kit.js stores the
 * dismissal, and the card KEEPS RENDERING - which shipped on 2026-08-01
 * and is what "why can I not close the popup" was. It also broke the
 * no-JS guarantee, since the card ships with the attribute set.
 * Any new rule that sets `display` on this element needs the same guard. */
.page-light .donate-callout[hidden] { display: none; }
/* Pop-in on reveal. Keyed to [hidden] being removed by kit.js, so it plays
 * exactly once, at the moment the popup appears. Honours reduced motion. */
@keyframes donate-pop-in {
  from { opacity: 0; transform: translateY(16px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
.page-light .donate-callout:not([hidden]) {
  animation: donate-pop-in 0.45s cubic-bezier(0.2, 0.8, 0.3, 1);
}
@media (prefers-reduced-motion: reduce) {
  .page-light .donate-callout:not([hidden]) { animation: none; }
}
.page-light .donate-callout-mascot { flex: 0 0 auto; display: block; }
/* The swoosh, per the owner's mock: a soft gradient band curving up from
 * the card's right side to the header Donate button, mascot riding the
 * tip. It paints over empty margin and the lede's right edge;
 * pointer-events none so it never intercepts a click. It hides with the
 * header link it points at (max-width 400px below). */
.page-light .donate-callout-arrow {
  position: absolute;
  right: 8px;
  top: -208px;
  width: 282px;
  height: 215px;
  pointer-events: none;
}
@media (max-width: 400px) {
  .page-light .donate-callout-arrow { display: none; }
}
.page-light .donate-callout-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--steel);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.page-light .donate-callout-close:hover { color: var(--orange); }


/* ─── The paste field on /link/claim ──────────────────────────────────────
 * `.form-panel` is a flex row and none of its children grow, which is right
 * for the admin lane, where the field holds a Reddit username of at most 20
 * characters. Here the field holds a pasted POST URL of about 60, and at 1100
 * the input rendered ~200px inside an ~820px panel: the user could see a
 * fifth of what they had pasted and could not check it before submitting.
 *
 * SCOPED to this form deliberately. `.form-panel` has several users and the
 * lesson from the align-center fix is to grep for all of them before changing
 * a shared utility, not to widen it for everyone because one instance was
 * shown to me. `min-width: 0` is what actually lets the label shrink inside a
 * flex row; without it the input overflows instead of fitting.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .claim-form label { flex: 1 1 auto; min-width: 0; }
.page-light .claim-form input[type="text"] { width: 100%; }

/* ─── The verification sentence, on /link/claim ───────────────────────────
 * The one thing on that page a person has to reproduce exactly on Reddit, so
 * it is sized to be read off a phone screen and copied without a mistake.
 *
 * THIS REPLACED A 12-CHARACTER TOKEN and every value here changed with it.
 * Reddit removed posts whose title was a bare base32 code, so the payload is
 * now an ordinary sentence and the code is the first letter of each word. The
 * old rule was mono, 26px, 0.14em tracking, `word-break: break-all` and a
 * 360px cap, all of which are actively wrong for prose:
 *
 *   * `word-break: break-all` splits words mid-word. On a token that only
 *     ever affected where a line ended; here a broken word LOOKS like two
 *     words, and the first letter of each word is the entire payload. It is
 *     now `normal`, with `overflow-wrap` as the safety net for the one case
 *     that matters, a single word too long for the column.
 *   * heavy tracking and a mono face slow prose down and are what make a
 *     string read as a token rather than as a sentence to be copied whole.
 *   * 360px wrapped thirteen words into six short lines. The cap is now a
 *     text measure, wide enough for two or three lines at desktop.
 *
 * `user-select: all` survives all of it and matters more than before: one
 * click or tap takes the whole sentence, which is what claim.js's copy button
 * then falls back to when the clipboard API is unavailable.
 *
 * NOT the donate-callout treatment. That card is deliberately the loudest
 * element in the product and nothing else may borrow it. This is a readout:
 * prominent because it is the payload, not because it is selling anything.
 *
 * Placed BEFORE the utility block on purpose. Anything after it loses to the
 * utilities at equal specificity, which is what that block exists to do.
 * ─────────────────────────────────────────────────────────────────────── */
.claim-phrase {
  display: block;
  font-size: 20px;
  line-height: 1.5;
  text-align: center;
  word-break: normal;
  overflow-wrap: anywhere;
  user-select: all;
  -webkit-user-select: all;
  padding: 18px 20px;
  max-width: 34em;
  margin: 0 auto 10px;
  border-radius: 4px;
}
.page-light .claim-phrase {
  background: var(--inset);
  border: 1px solid var(--border);
  border-left: 3px solid var(--orange);
  color: var(--cream);
}
.claim-copy-line { text-align: center; margin: 0 auto 4px; }

/* ─── "Found it", the settled result on the check button ──────────────────
 * The check runs for up to a minute and then, on success, the server
 * redirects. Without a state here the button counts up and the page changes
 * underneath the person; claim.js holds the navigation back so this can be
 * read first.
 *
 * SPECIFICITY IS THE WHOLE FIGHT, and this is the trap the utility layer
 * already lost once. The primary fill is
 * `.page-light button[type=submit]:not(.btn-ghost):not(.btn-danger):not(.btn-copy)`,
 * which counts (0,5,1) because each :not() contributes its argument. A
 * readable `.page-light .claim-form button.is-found:disabled` is only (0,4,1)
 * and loses the background silently, leaving orange behind the green ink.
 * The selectors below are (0,5,2). Count before editing them.
 *
 * `:disabled` is included because the button IS disabled at that point, and
 * `.page-light button:disabled { opacity: 0.45 }` would otherwise fade a
 * result that has just arrived - the same argument already written out for
 * `.is-cooling` and `form.is-busy`.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light form.claim-form button[type="submit"].is-found:disabled,
.page-light form.claim-form button[type="submit"].is-found:disabled:hover {
  opacity: 1;
  background: var(--ok-bg);
  border-color: var(--ok-ink);
  color: var(--ok-ink);
  cursor: default;
}

@media (max-width: 430px) {
  /* Smaller, and the side padding goes with it: at 320px the sentence needs
     the column more than it needs the inset. */
  .claim-phrase { font-size: 18px; padding: 16px 12px; }
}

/* ─── Terms acceptance on /signup ─────────────────────────────────────────
 * The scrollable terms box, its sentinel, and the consent row beneath it.
 * Behaviour and the reason the gate fails open are in signup.js.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .terms-block { margin-top: 18px; }

/* Sits directly above the box it names, and is what `aria-labelledby` on the
   region points at, so the accessible name and the visible one cannot drift. */
.page-light .terms-heading { font-size: 17px; margin: 0 0 8px; }

/* THE BODY TEXT IS NOT SHRUNK. `.muted` paragraphs render at 17px here, the
   same size they render at on /terms, so the document is not made to look
   shorter than it is on the one screen where that would be a trick. An earlier
   version of this block claimed 16px and set it on the container, where it did
   nothing at all: `.muted` carries its own size and simply won. Measured.

   `clamp` rather than a flat height: 46vh keeps the box proportional on a
   laptop, the 200px floor stops it collapsing to a slot on a short window, and
   the 360px ceiling stops it swallowing the whole page on a tall monitor.
   Whatever it resolves to, signup.js re-measures after fonts load and on
   resize, so a box that stops scrolling still releases the gate. */
.page-light .terms-scroll {
  max-height: clamp(200px, 46vh, 360px);
  overflow-y: auto;
  padding: 16px 18px;
  border: 1px solid var(--border-hover);
  border-radius: 4px;
  background: var(--panel);
}

/* The box is `tabindex="0"` so a keyboard can scroll it, which means it takes
   focus and must show it. */
.page-light .terms-scroll:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 2px;
}

/* The first child carries the "Last updated" line, and its own margin would
   otherwise sit on top of the box's padding. */
.page-light .terms-scroll > :first-child { margin-top: 0; }

/* EVERYTHING IN THE BOX IS 17px, and the hierarchy is carried by weight and by
   the 26px `* + h2` gap instead of by size.

   Measured and fixed: the headings were 16px against 17px body text, so a
   heading was SMALLER than the paragraph under it. Weight hid the inversion
   well enough to survive a look at the render, which is why this was found by
   dumping computed sizes rather than by reading the page. The lede comes down
   from its 19px page value for the opposite reason: in a panel it is the first
   paragraph, not the opening of a document. It keeps `--cream` so it still
   leads. */
.page-light .terms-scroll h2 { font-size: 17px; }
.page-light .terms-scroll .lede { font-size: 17px; }

/* The IntersectionObserver target. 1px rather than zero height, because a
   zero-height element has no box to intersect with and the observer would
   never fire, silently removing the keyboard and screen reader path to
   releasing the gate. */
.page-light .terms-end { height: 1px; }

/* THE HINT KEEPS ITS SPACE ONCE IT HAS BEEN SHOWN.
   Measured: hiding it outright moved the consent row up 36px and the button
   37px at the exact moment the reader finishes the terms and goes to click.
   `visibility` holds the space so nothing under the cursor moves. It ships with
   the `hidden` ATTRIBUTE and signup.js only reveals it when the gate is really
   holding, so with JavaScript off there is no reserved gap for a hint that is
   never shown. */
.page-light .terms-hint.is-met { visibility: hidden; }

/* `flex-start`, not the shared row's `center`. This label runs to two or three
   lines at every width the card is used at, and centring puts the box against
   the middle of the paragraph instead of beside its first line. The 3px nudge
   is optical alignment to the cap height of that line.

   `margin-bottom` matches the top: the button sat 8px under a three-line
   sentence, which read as attached to it. */
.page-light .terms-accept { align-items: flex-start; gap: 10px; margin: 18px 0 10px; }
.page-light .terms-accept input[type="checkbox"] { margin-top: 3px; flex: 0 0 auto; }
/* NORMAL WEIGHT. The row is a `<label>`, and this surface makes labels 600,
   which is right for the one-word admin checkboxes it was written for and
   wrong for a sentence. This is the sentence the person is agreeing to, so it
   should read as prose. */
.page-light .terms-accept span { font-size: 16px; line-height: 1.5; font-weight: 400; }

/* ─── The held submit button ──────────────────────────────────────────────
 * MEASURED AT 1.44:1 before this rule. `.page-light button:disabled` fades to
 * 0.45 opacity, which is right for a control that is simply unavailable and
 * wrong here: this button is temporarily held, the label is the only thing
 * naming it, and the page is asking the reader to do something to release it.
 * Third time this exact problem has appeared on the light surface, after the
 * retry cooldown and the scan countdown, and it is fixed the same way: full
 * opacity, plus a treatment that reads as not-yet rather than as broken.
 *
 * SPECIFICITY IS THE WHOLE FIGHT. The primary fill is
 * `.page-light button[type="submit"]:not(.btn-ghost):not(.btn-danger):not(.btn-copy)`
 * at (0,5,1), because every `:not()` contributes its argument. A sensible
 * looking `.page-light button.is-held:disabled` is (0,5,1)'s inferior at
 * (0,3,1) and loses the background silently, leaving unreadable text on blue.
 * The selector below is (0,5,2). Count before editing it, and note this is why
 * the form carries a class at all.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light form.signup-form button[type="submit"].is-held:disabled,
.page-light form.signup-form button[type="submit"].is-held:disabled:hover {
  opacity: 1;
  background: var(--rule-soft);
  border-color: var(--border-hover);
  color: var(--cream-dim);
  cursor: default;
}

/* ─── Spacing utilities, last so they actually win ────────────────────────
 * `.mt-sm` and `.mt-md` are defined near the top of this file at (0,0,1,0).
 * Every light component rule is (0,0,2,0), so any component that sets a
 * `margin` shorthand silently beat them: `.page-light .note { margin: 0 }`
 * zeroed the top margin on all five elements written as `note mt-sm` or
 * `note mt-md`, and the owner spotted the symptom in the hero, where a
 * control ended up with 18px above it and 0 below.
 *
 * Re-declaring them here at (0,0,2,0) AFTER the components is what a utility
 * layer is for: same specificity, later in the cascade, so it wins. Rules
 * that are deliberately more specific still beat it, which is why
 * `.page-light .hero .actions` at (0,0,3,0) keeps its symmetric 24px.
 *
 * KEEP THIS BLOCK LAST. Move a component rule below it and that component
 * starts losing again, silently, exactly as before.
 * ─────────────────────────────────────────────────────────────────────── */
.page-light .mt-sm { margin-top: 12px; }
.page-light .mt-md { margin-top: 18px; }
