/* Neritation OS — internal design language
 * ============================================================================
 * Spec: .interface-design/system.md and docs/DESIGN.md §9.
 * Tokens: reports/tokens.py (CORE + OS_ONLY), inlined as :root by _layout.html.
 *
 * There are ZERO colour literals in this file. Every colour is a var().
 *
 * ── the checkpoint the interface-design skill requires ──────────────────────
 * Intent      Neri, alone, 07:00, five minutes, deciding whether to approve
 *             changes that spend his clients' money. Feel: a lit control room —
 *             deep carbon, soft glass, light coming from somewhere.
 * Hierarchy   One focal element per view, winning by size + weight + the data
 *             accent. Chrome recedes; numbers lead.
 * Palette     TWO ROLES, never mixed. --accent (hazard yellow) owns DATA:
 *             numbers, stat values, stake bars, chart series. The indigo->pink
 *             --spectrum owns CHROME: nav, rings, aurora, gradient rules. A
 *             spectrum colour on a number, or yellow on a nav item, is a bug.
 * Depth       GLASS, committed, one strategy: a 1px inset highlight, a hairline
 *             ring, a deep soft blur. On dark the ring does the separating.
 * Surfaces    OKLCH carbon stack 0.07 -> 0.13 -> 0.18. Perceptually even steps,
 *             which hex cannot give you.
 * Typography  Sora display, Manrope body, Heebo for Hebrew. Three levers —
 *             size, weight, colour — never size alone. tabular-nums on numbers.
 * Spacing     4px base, --s1..--s8. Panels --s5, controls --s3.
 *
 * ── forbidden, and why ─────────────────────────────────────────────────────
 * Never put a --spec-* colour on data. Never tint a number. Charts read
 * --accent and --secondary only. Numbers stay tabular so columns never shift.
 * ========================================================================== */

/* ── reset and base ─────────────────────────────────────────────────────── */

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

html, body { margin: 0; padding: 0; }

/* No aurora.
 *
 * Two purple radial washes drifting behind the content for 24 seconds on a loop.
 * It was the single biggest change from the flat surface, and it was the wrong
 * one: an ambient animated gradient is decoration that never stops moving behind
 * numbers people are trying to read, and it is the backdrop of every generated
 * landing page of the last two years.
 *
 * The ground is hard black. What sits on it earns its own contrast. */

body {
  background: var(--bg);
  color: var(--text);
  min-block-size: 100vh;
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: 1.55;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}

h1, h2, h3 { text-wrap: balance; margin: 0; font-family: var(--font-display); }

/* Body links are NOT accent. The accent is a scarce resource spent on the primary
 * action, the current nav item and the one headline stat — three per view. It
 * previously painted seven element types and therefore signalled nothing. */
a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--text-faint);
  text-underline-offset: 2px;
  transition: color var(--dur) var(--ease),
              text-decoration-color var(--dur) var(--ease);
}
a:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* One focus rule for everything focusable. The previous CSS set outline:none on
 * inputs and replaced it with a 1px border-colour change — invisible on a dark
 * surface and a WCAG 2.4.11 failure — and gave buttons and .btn anchors no focus
 * style at all. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--control-radius);
}

::selection { background: var(--accent); color: var(--ink); }

code, .mono {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--secondary);
}

/* ── accessibility utilities ────────────────────────────────────────────── */

.sr-only {
  position: absolute; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip {
  position: absolute; inset-block-start: var(--s2); inset-inline-start: var(--s2);
  z-index: 100; padding: var(--s2) var(--s4);
  background: var(--accent); color: var(--ink);
  font-size: var(--t-label); font-weight: 700; text-decoration: none;
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

/* ── shell ──────────────────────────────────────────────────────────────── */

.shell { display: flex; min-block-size: 100vh; }

.rail {
  inline-size: var(--rail-w);
  flex: 0 0 var(--rail-w);
  background: oklch(0.07 0.012 250 / .72);
  backdrop-filter: blur(18px);
  border-inline-end: 1px solid var(--hairline);
  padding-block: var(--s5) var(--s6);
  position: sticky;
  inset-block-start: 0;
  block-size: 100vh;
  overflow-y: auto;
  /* A column, so the account block can sit at the bottom rather than trailing the
     last nav item. In flow it landed 32px below Settings — which on a 720px-tall
     screen is below the fold, so signing out meant scrolling a navigation rail to
     find the control that is always in the same place in every other application. */
  display: flex;
  flex-direction: column;
}
/* Nothing in the rail may be squeezed to make room. Column flex shrinks children
   by default, so a tall nav would compress the links towards each other instead of
   scrolling — which is what overflow-y is for. */
.rail > * { flex: none; }

.brand {
  padding: 0 var(--s5) var(--s5);
  font-size: var(--t-lead);
  font-weight: 800;
  letter-spacing: -.01em;
  line-height: 1.1;
}
.brand em { font-style: normal; color: var(--accent); }
.brand small {
  display: block;
  margin-block-start: var(--s1);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.rail__group {
  padding: var(--s5) var(--s5) var(--s2);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The command cursor. A clip-path triangle rather than a font glyph, because
 * there is no icon set in this repo and ▶ renders inconsistently across
 * platforms. It marks hover AND :focus-visible, so it doubles as a second,
 * non-colour focus cue. */
.rail__link {
  display: flex;
  align-items: center;
  gap: var(--s2);
  /* 44px hit target at 15px type: 12px block padding + line-height. */
  margin-inline: var(--s3);
  padding: var(--s3) var(--s4);
  min-block-size: 44px;
  border-radius: var(--control-radius);
  color: var(--text-dim);
  font-size: var(--t-body);
  text-decoration: none;
  border-inline-start: 2px solid transparent;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.rail__link::before {
  content: "";
  inline-size: 7px; block-size: 10px;
  flex: 0 0 7px;
  background: currentColor;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.rail__link:hover { color: var(--text); background: var(--hover-wash); }
.rail__link:hover::before { opacity: .5; transform: none; }
.rail__link[aria-current="page"] {
  color: var(--edge);
  border-inline-start-color: transparent;
  background: oklch(0.72 0.20 275 / .16);
  box-shadow: inset 0 0 0 1px oklch(0.72 0.20 275 / .30);
  font-weight: 600;
}
.rail__link[aria-current="page"]::before { opacity: 1; transform: none; }
.rail__link:focus-visible::before { opacity: 1; transform: none; }

.badge {
  margin-inline-start: auto;
  min-inline-size: 20px;
  padding: 0 var(--s2);
  background: var(--accent);
  color: var(--ink);
  font-size: var(--t-micro);
  font-weight: 700;
  text-align: center;
  border-radius: var(--pill-radius);
}

main {
  flex: 1;
  min-inline-size: 0;
  padding: 0 0 var(--s8);
}

/* ── page head: the second --ink band ───────────────────────────────────── */

.page-head {
  display: flex;
  align-items: baseline;
  gap: var(--s4);
  flex-wrap: wrap;
  padding: var(--s5) var(--s6);
  position: relative;
  background: oklch(0.09 0.014 250 / .55);
  backdrop-filter: blur(14px);
  border-block-end: 1px solid var(--hairline);
  margin-block-end: var(--s5);
}
.page-head h1 {
  font-size: var(--t-h1);
  font-weight: 800;
  letter-spacing: -.01em;
}
.page-head .sub { color: var(--text-dim); font-size: var(--t-label); }
.page-head .spacer { margin-inline-start: auto; }
.page-head .identity {
  inline-size: 3px;
  align-self: stretch;
  background: var(--client-accent, var(--accent));
}

.wrap { padding-inline: var(--s6); padding-block-end: var(--s7);
        max-inline-size: 1440px; }

/* ── panel: the framed surface ──────────────────────────────────────────── */

.panel {
  position: relative;
  background: var(--card);
  border: 1px solid var(--hairline);
  border-radius: var(--panel-radius);
  padding: var(--s5);
  box-shadow: var(--glass);
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.panel:hover { box-shadow: var(--glass-hover); }

/* A hairline spectrum rule across the top edge, revealed on hover. Replaces
 * the bracket ornaments: chrome, not data, so it takes the spectrum. */
.panel::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: var(--s5);
  block-size: 1px;
  background: var(--spectrum);
  opacity: .35;
  transition: opacity var(--dur) var(--ease), inset-inline var(--dur) var(--ease);
  pointer-events: none;
  border-radius: var(--pill-radius);
}
.panel:hover::before { opacity: .9; inset-inline: var(--s3); }

/* Max ONE per view — same rule DESIGN.md §5 imposes on reports. The accent
 * bar is yellow because it marks the panel that carries the money. */
.panel--accent { box-shadow: var(--glass), var(--glow); }
.panel--accent::before { opacity: 1; background: var(--accent); }

/* A panel title and a footnote were both 12px --text-dim, separated only by
 * weight and caps — which is what made every panel read the same. The title now
 * sits a step up in size AND colour, so the three tiers (title / body / hint) are
 * distinguishable by two levers each, not one. */
.panel__title {
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-block-end: var(--s4);
}
.panel__title .n { color: var(--text-faint); font-weight: 500; }

/* ── layout utilities: the abstractions that were missing ───────────────── */

/* Replaces 21 hand-written margin-block-start values at 8/10/14/16/18/20/22px. */
.stack > * + * { margin-block-start: var(--s4); }
/* Section-level air. The measured page used only 12/16/24 — three values
 * out of eight — so nothing read as a *group*. --s6 between major blocks
 * and --s2 within a cluster is what creates the rhythm. */
.stack--section > * + * { margin-block-start: var(--s6); }
.stack--tight > * + * { margin-block-start: var(--s2); }
.stack--loose > * + * { margin-block-start: var(--s5); }

.actions {
  display: flex;
  gap: var(--s3);
  flex-wrap: wrap;
  align-items: center;
  margin-block-start: var(--s5);
}

.inline { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }

.grid { display: grid; gap: var(--s4); }
.grid.k2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid.k3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid.k4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* minmax(0, 1fr), never a bare 1fr.
 *
 * `1fr` is shorthand for `minmax(auto, 1fr)`, and `auto` floors the track at the
 * item's MIN-CONTENT width. A panel holding a 12-column table with nowrap
 * headers therefore reports a min-content of ~640px, the track grows to fit it,
 * and the grid — then .wrap, then the page — grows with it.
 *
 * Measured at 375px before this: the dashboard document was 473px wide against a
 * 375px viewport, so the body scrolled 98px sideways, and /clients/<id> scrolled
 * 107px. .table-wrap's overflow-x never engaged because the wrap had been handed
 * every pixel it asked for; a scroll container only scrolls once something
 * upstream refuses to grow. This is that refusal. */
.row { display: grid; gap: var(--s4);
       grid-template-columns: repeat(2, minmax(0, 1fr)); }
.row.k3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* ── stat block ─────────────────────────────────────────────────────────── */

.stat__label {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.stat__value {
  font-size: var(--t-stat);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  margin-block-start: var(--s1);
}
/* The single headline number. One per view. */
.stat__value--accent { color: var(--accent); }
.stat__foot {
  font-size: var(--t-label);
  color: var(--text-dim);
  margin-block-start: var(--s1);
}
.stat__delta { font-weight: 600; }
.stat__delta--up { color: var(--positive); }
.stat__delta--down { color: var(--negative); }

/* ── the stake bar: the signature element ───────────────────────────────── */
/*
 * This product's thesis is "rank by money at stake, never by rule number" — a
 * warn on ₪8,000/mo outranks a critical on ₪200/mo. The bar draws that thesis:
 * scanning the findings list becomes scanning a bar chart of what is bleeding.
 * Width is set inline from the row's share of the largest stake in view, because
 * it is data, not style.
 */
.stake { display: flex; align-items: center; gap: var(--s2); }
.stake__value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}
.stake__track {
  flex: 1;
  min-inline-size: 40px;
  block-size: 6px;
  background: var(--raised);
  border: 1px solid var(--hairline);
}
.stake__fill {
  display: block;
  block-size: 100%;
  background: var(--accent);
  min-inline-size: 2px;
}
.stake--critical .stake__fill { background: var(--sev-critical); }

/* ── severity tags: colour + glyph + weight, never colour alone ─────────── */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  padding: 2px var(--s2);
  border: 1px solid currentColor;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
  color: var(--text-dim);
  border-radius: var(--pill-radius);
  background: oklch(1 0 0 / .03);
}
.tag::before { font-size: .85em; line-height: 1; }
.tag--critical { color: var(--sev-critical); font-weight: 700; }
.tag--critical::before { content: "\25A0"; }        /* ■ */
.tag--warn { color: var(--sev-warn); }
.tag--warn::before { content: "\25B2"; }            /* ▲ */
.tag--info { color: var(--sev-info); }
.tag--info::before { content: "\25CF"; }            /* ● */
.tag--ok { color: var(--sev-ok); }
.tag--ok::before { content: "\2713"; }              /* ✓ */
.tag--idle { color: var(--sev-idle); }
.tag--idle::before { content: "\2013"; }            /* – */

/* ── tables ─────────────────────────────────────────────────────────────── */

/* The missing .scroll-x equivalent. Without it the 12-column campaigns table
 * scrolls the whole page body on a phone instead of scrolling itself. */
.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border-block-start: 1px solid var(--hairline);
  /* A flex or grid item's automatic minimum size is its min-content, which for a
     wide table is the whole table. Without this the wrap is never smaller than
     its contents and so never has anything to scroll — it just makes its parent
     wider. Belt to the minmax(0, 1fr) braces: the tracks are fixed above, and
     this keeps any future flex parent from reintroducing the same bug. */
  min-inline-size: 0;
}

table { inline-size: 100%; border-collapse: collapse; }

th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--card);
  text-align: start;
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding: var(--s3);
  white-space: nowrap;
  border-block-end: 1px solid var(--border);
}

td {
  padding: var(--s3);
  border-block-end: 1px solid var(--hairline);
  vertical-align: middle;
}
tr:last-child td { border-block-end: none; }
tbody tr { transition: background var(--dur) var(--ease); }
tbody tr:hover { background: var(--raised); }

td.num, th.num { text-align: end; font-variant-numeric: tabular-nums; }
.num--dim { color: var(--text-dim); }

/* A critical row is findable while scanning a 12-column table. */
tr.is-critical td:first-child {
  box-shadow: inset 3px 0 0 var(--sev-critical);
}

.cell-name { max-inline-size: 24ch; }
.col-stake { min-inline-size: 180px; }
td .hint { white-space: normal; }

/* ── buttons ────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-block-size: 40px;
  padding: var(--s2) var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--pill-radius);
  background: var(--raised);
  color: var(--text);
  font: inherit;
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn:hover { border-color: var(--text-dim); color: var(--text); text-decoration: none; }
/* Tactile confirmation the UI heard the click. Never below .95. */
.btn:active { transform: scale(.97); }

/* The primary action is one of the three sanctioned accent uses.
 *
 * Solid hazard yellow with black text, square. It was an indigo-to-pink gradient
 * with a coloured glow on a 999px pill — which is not this system's brand, it is
 * the house style of generated interfaces, and it sat on the single most
 * important control on every page.
 *
 * Yellow also means the primary action and the headline number are now the same
 * colour, which is correct: both answer "what should I look at", and having them
 * disagree was the reason the accent rule felt arbitrary. */
.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);
  font-weight: 700;
  box-shadow: none;
}

.btn--primary:hover {
  background: var(--edge);
  border-color: var(--edge);
  color: var(--ink);
}
.btn--primary:active { transform: scale(.97); }

.btn--ghost { background: transparent; color: var(--text-dim); font-weight: 500; }
.btn--ghost:hover { color: var(--accent); border-color: var(--accent); }

/* Existed in the old CSS and was never used anywhere; the ✕ remove-target
 * control was styled as a ghost button while being destructive. */
.btn--danger { background: transparent; color: var(--negative); border-color: var(--negative); }
.btn--danger:hover { background: var(--negative); color: var(--ink); border-color: var(--negative); }

.btn--sm { min-block-size: 32px; padding: var(--s1) var(--s3); font-size: var(--t-micro); }

.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .5; cursor: not-allowed; transform: none;
}

/* ── forms ──────────────────────────────────────────────────────────────── */

/* The global `label { text-transform: uppercase }` is gone. It was aggressive
 * enough that two places carried style="text-transform:none" purely to undo it —
 * the override was the symptom, the global rule was the defect. */
.field { display: block; }
.field + .field { margin-block-start: var(--s4); }

.field__label {
  display: block;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-block-end: var(--s1);
}

input, select, textarea {
  inline-size: 100%;
  /* 44px, not 42. Two pixels short of the WCAG target on every credential,
   * target and code field in the console, for no reason anyone recorded. */
  min-block-size: 44px;
  /* Inputs are inset — they receive content. Darker than their surround signals
   * "type here" without a heavy border. */
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--control-radius);
  padding: var(--s2) var(--s3);
  font: inherit;
  /* --t-control (16px), not --t-body (15px). Mobile Safari zooms the viewport
   * when a focused control computes below 16px, so the login form zoomed the
   * page on the first tap and every form after it did the same. See the token's
   * comment in reports/tokens.py for why this is not a viewport-meta fix. */
  font-size: var(--t-control);
  transition: border-color var(--dur) var(--ease);
}
input:hover, select:hover, textarea:hover { border-color: var(--text-faint); }
input::placeholder, textarea::placeholder { color: var(--text-faint); }

/* Replaces style="inline-size:auto" fighting the 100% rule. */
.field--auto, .field--auto select, .field--auto input { inline-size: auto; }

.check {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-block-size: 40px;
  font-size: var(--t-body);
  color: var(--text);
  cursor: pointer;
}
.check input { inline-size: 18px; block-size: 18px; min-block-size: 0; flex: 0 0 18px; accent-color: var(--accent); }

.form--inline { display: inline; }

.hint {
  font-size: 12px;
  color: var(--text-dim);
  margin-block-start: var(--s1);
}
.hint a { color: var(--secondary); }

/* ── notes ──────────────────────────────────────────────────────────────── */

.note {
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--text-dim);
  background: var(--card);
  padding: var(--s3) var(--s4);
  font-size: var(--t-body);
}
.note + .note { margin-block-start: var(--s2); }
.note--good { border-inline-start-color: var(--positive); }
.note--bad { border-inline-start-color: var(--negative); }
.note--warn { border-inline-start-color: var(--accent); }
.note a { color: var(--accent); font-weight: 600; }

/* ── empty states: a designed state, not a blank card ───────────────────── */

.empty { text-align: center; padding: var(--s7) var(--s5); }
.empty__title {
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--text);
  margin-block-end: var(--s2);
}
.empty__body { color: var(--text-dim); max-inline-size: 46ch; margin-inline: auto; }
.empty__action { margin-block-start: var(--s5); }

/* ── result screen ──────────────────────────────────────────────────────── */

.result { text-align: center; padding: var(--s6) var(--s5); }
.result__verdict {
  font-size: var(--t-verdict);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1;
  animation: verdict var(--dur-slow) var(--ease) both;
}
.result__verdict--complete { color: var(--positive); }
.result__verdict--partial { color: var(--accent); }
.result__verdict--failed { color: var(--negative); }
/* While it runs there is no verdict word — showing one and changing it later is
   how a status page teaches people to stop reading it. The elapsed count is the
   only number that means anything yet, so it takes the verdict's size. */
.result__running { display: grid; gap: var(--s3); justify-items: center; }
.result__running .running__rail { inline-size: min(28rem, 100%); }
.result__elapsed {
  font-size: var(--t-stat);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  line-height: 1;
}
.result__stats {
  display: flex; justify-content: center; gap: var(--s6);
  flex-wrap: wrap; margin-block-start: var(--s5);
}
.result__tail {
  text-align: start;
  margin-block-start: var(--s5);
  padding: var(--s3);
  background: var(--ink);
  border: 1px solid var(--border);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-label);
  color: var(--text-dim);
  white-space: pre-wrap;
  overflow-x: auto;
}

@keyframes verdict {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* ── running indicator (os.js) ──────────────────────────────────────────── */

.running { display: none; margin-block-start: var(--s3); }
[aria-busy="true"] .running { display: block; }
.running__rail {
  /* display is explicit because this is a <span> in the nav pill and a <div> in
     the form indicator — an inline box ignores block-size and the bar vanishes. */
  display: block;
  block-size: 3px;
  background: var(--raised);
  overflow: hidden;
}
.running__rail::after {
  content: "";
  display: block;
  block-size: 100%;
  inline-size: 35%;
  background: var(--accent);
  animation: sweep 1.1s var(--ease) infinite;
}
@keyframes sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(340%); }
}

/* The running-job pill in the nav. Lives at the foot of the rail, below every
   link, because it is a transient state and must never displace navigation. */
.rail__running {
  display: block;
  margin: var(--s5) var(--s3) 0;
  padding: var(--s3);
  border: 1px solid var(--border);
  background: var(--raised);
  color: var(--text);
  text-decoration: none;
}
.rail__running:hover { border-color: var(--accent); }
.rail__running-label {
  display: block;
  margin-block-start: var(--s2);
  font-size: var(--t-label);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail__running .hint { font-size: var(--t-micro); }

/* Scan verdicts. A grid of present/absent, read as a block rather than a list —
   the shape of what is missing is the finding, not any single row. */
.verdicts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s3);
  margin-block: var(--s4);
}
.verdict {
  display: flex; gap: var(--s3); align-items: start;
  padding: var(--s3);
  border-inline-start: 3px solid var(--border);
  background: var(--raised);
}
.verdict__state { font-size: var(--t-lead); line-height: 1; flex: none; }
.verdict__label { font-weight: 600; font-size: var(--t-label); }
.verdict.is-on { border-inline-start-color: var(--positive); }
.verdict.is-on .verdict__state { color: var(--positive); }
.verdict.is-off { border-inline-start-color: var(--negative); }
.verdict.is-off .verdict__state { color: var(--negative); }

/* The ranking column in an agency-wide triage table. One per table: it is the
   column the rows are sorted by, and accenting a second would make the sort
   ambiguous. */
.num--accent { color: var(--accent); font-weight: 600; }

/* ── the hero ───────────────────────────────────────────────────────────── */
/*
 * ONE per page. The focal element: the number the page exists to show, at a size
 * nothing else competes with.
 *
 * Written because the OS was reading as competent and characterless, and the
 * cause was structural rather than decorative: every page was a grid of
 * identically weighted panels, so the layout made no argument about what to look
 * at. A dashboard where "spend" — a fact you cannot act on — is typeset exactly
 * like "₪10,890 you could recover" is a dashboard that has declined to have an
 * opinion. Four equal boxes is not neutrality; it is four claims of equal
 * importance, and they are not equally important.
 *
 * So the hero states the argument and .strip carries the supporting facts at a
 * weight that says supporting.
 */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s5);
  align-items: end;
  padding: var(--s6) var(--s5);
  background: var(--ink);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--accent);
  position: relative;
}
.hero__label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.hero__value {
  font-family: var(--font-display);
  font-size: var(--t-verdict);
  font-weight: 800;
  line-height: .95;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  margin-block-start: var(--s2);
}
/* Nothing at stake is not an emergency, and painting it in hazard yellow teaches
   the reader that the colour means nothing. */
.hero--calm { border-inline-start-color: var(--border); }
.hero--calm .hero__value { color: var(--text-dim); }
.hero__note {
  margin-block-start: var(--s3);
  color: var(--text-dim);
  font-size: var(--t-body);
  max-inline-size: 52ch;
}
.hero__aside { display: grid; gap: var(--s2); justify-items: end; text-align: end; }

@media (max-width: 767px) {
  .hero { grid-template-columns: 1fr; padding: var(--s5) var(--s4); }
  .hero__value { font-size: var(--t-stat); }
  .hero__aside { justify-items: start; text-align: start; }
}

/* ── the strip ──────────────────────────────────────────────────────────── */
/*
 * Secondary numbers, as a rule-separated row rather than boxes. A box is a claim
 * that something is a separate object worth its own frame; four of them in a row
 * makes four such claims and the reader has to check each one to find out none
 * of them was the point.
 */
.strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  border: 1px solid var(--border);
  background: var(--card);
}
.strip__cell {
  padding: var(--s4) var(--s5);
  border-inline-start: 1px solid var(--hairline);
}
.strip__cell:first-child { border-inline-start: none; }
.strip__label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.strip__value {
  font-size: var(--t-h2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  margin-block-start: var(--s1);
}
.strip__foot {
  font-size: var(--t-label);
  color: var(--text-dim);
  margin-block-start: var(--s1);
}

@media (max-width: 767px) {
  .strip__cell { border-inline-start: none; border-block-start: 1px solid var(--hairline); }
  .strip__cell:first-child { border-block-start: none; }
}

/* ── deltas ─────────────────────────────────────────────────────────────── */
/*
 * A number against the same window a period earlier. Every card carries its own
 * direction, because "up" is not a compliment for every metric: rising spend is
 * the input, not the result, and rising CPA is a problem. A component that
 * assumes green means up congratulates an account for getting more expensive.
 */
.delta {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--t-label);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}
.delta::before { font-size: .8em; }
.delta--up::before { content: "\25B2"; }              /* ▲ */
.delta--down::before { content: "\25BC"; }            /* ▼ */
.delta--flat::before { content: "\2013"; }            /* – */
.delta--good { color: var(--positive); }
.delta--bad { color: var(--negative); }

/* ── the funnel ─────────────────────────────────────────────────────────── */
/*
 * Impressions -> clicks -> conversions -> revenue, with the rate between each
 * pair. The rates are where a campaign actually breaks and none of them were
 * shown anywhere: a halved CTR and a halved CVR produce the same drop in
 * conversions and need opposite fixes — one is the creative, one is the landing
 * page.
 *
 * Laid out as steps with the rate ON the join, so the reader sees the loss
 * between two numbers rather than being left to divide them.
 */
.funnel {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: start;
  gap: 0;
  /* Room for the rate badges, which straddle the top edge of each divider. They
     were positioned above their own container and landed on the panel title. */
  padding-block-start: var(--s4);
  margin-block-start: var(--s3);
  border-block-start: 1px solid var(--hairline);
}
.funnel__step {
  padding: var(--s4) var(--s5);
  border-inline-start: 1px solid var(--hairline);
  position: relative;
}
.funnel__step:first-child { border-inline-start: none; }
.funnel__label {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-faint);
}
.funnel__value {
  font-size: var(--t-h2); font-weight: 700; font-variant-numeric: tabular-nums;
  margin-block: var(--s1) var(--s2);
}
/* The rate sits on the boundary it describes. */
.funnel__rate {
  position: absolute;
  /* On the join it describes: centred on this step's leading divider, straddling
     the funnel's own top rule. The reader sees the loss BETWEEN two numbers
     rather than being left to divide them. */
  inset-block-start: 0;
  inset-inline-start: 0;
  transform: translate(-50%, calc(-50% - var(--s4)));
  padding: 2px var(--s2);
  background: var(--ink);
  border: 1px solid var(--border);
  font-size: var(--t-micro);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.funnel__rate-note { display: block; font-weight: 400; color: var(--text-faint); }

@media (max-width: 900px) {
  .funnel { grid-template-columns: repeat(2, minmax(0, 1fr));
            padding-block-start: 0; }
  /* Below this width the badges would overlap each other, so they drop into the
     flow. The rate still belongs to the step it opens. */
  .funnel__rate { position: static; transform: none; display: inline-block;
                  margin-block-end: var(--s2); }
}
@media (max-width: 520px) {
  .funnel { grid-template-columns: 1fr; }
  .funnel__step { border-inline-start: none;
                  border-block-start: 1px solid var(--hairline); }
  .funnel__step:first-child { border-block-start: none; }
}

/* ── share bars ─────────────────────────────────────────────────────────── */
/*
 * Where the money went. A percentage in a cell is a number to compare by
 * reading; a bar is a number to compare by looking, and this table exists to be
 * scanned.
 */
.bar { display: block; block-size: 4px; background: var(--raised); margin-block-start: var(--s1); }
.bar__fill { display: block; block-size: 100%; background: var(--text-dim); }
.bar__fill--accent { background: var(--accent); }

/* ── movers ─────────────────────────────────────────────────────────────── */
.mover { display: flex; align-items: baseline; gap: var(--s3);
         padding-block: var(--s3); border-block-end: 1px solid var(--hairline); }
.mover:last-child { border-block-end: none; }
.mover__name { flex: 1; min-inline-size: 0; }
.mover__name b { font-weight: 600; }
.mover__delta { font-variant-numeric: tabular-nums; font-weight: 700; flex: none; }

/* ── the scope bar ──────────────────────────────────────────────────────── */

.scope {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s6);
  border-block-end: 1px solid var(--hairline);
  background: var(--ink);
}
.scope__label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
  flex: none;
}
.scope__set {
  display: flex;
  gap: var(--s1);
  overflow-x: auto;
  scrollbar-width: none;
}
.scope__set::-webkit-scrollbar { display: none; }
.scope__chip {
  flex: none;
  padding: var(--s2) var(--s3);
  font-size: var(--t-label);
  color: var(--text-dim);
  text-decoration: none;
  white-space: nowrap;
  border-block-end: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.scope__chip:hover { color: var(--text); }
/* The client's own accent, and only here. The OS keeps one accent so that yellow
   always means the same thing; a per-client tint on the whole shell would destroy
   that. A 2px underline is enough to tell two clients apart at a glance. */
.scope__chip.is-on {
  color: var(--text);
  border-block-end-color: var(--client-accent, var(--accent));
}

/* One alert. The disclosure indents under its own line rather than the panel, so
   the answer stays attached to the question. */
.alert + .alert { margin-block-start: var(--s3); }
.alert__detail { margin-block-start: var(--s1); margin-inline-start: var(--s6); }

/* The change cell on the approval screen. The new value is the one being
   approved, so it carries the weight; the old one is context. */
.cell-change { min-inline-size: 12ch; }
.change { display: inline-flex; align-items: baseline; gap: var(--s2); }
.change__from { color: var(--text-dim); font-variant-numeric: tabular-nums; }
.change__arrow { color: var(--text-faint); }
.change__to {
  color: var(--text);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ── connection checklist ───────────────────────────────────────────────── */

.conn { padding-block: var(--s4); border-block-end: 1px solid var(--hairline); }
.conn:last-of-type { border-block-end: none; }

.steps { list-style: none; margin: var(--s3) 0 0; padding: 0; }
.steps li {
  position: relative;
  padding-inline-start: var(--s5);
  font-size: var(--t-label);
  color: var(--text-dim);
}
.steps li + li { margin-block-start: var(--s1); }
.steps li::before {
  content: "\25CB";                                  /* ○ */
  position: absolute;
  inset-inline-start: 0;
  color: var(--text-faint);
}
.steps li.done { color: var(--text); }
.steps li.done::before { content: "\25CF"; color: var(--positive); }

/* ── charts ─────────────────────────────────────────────────────────────── */

/* Chart.js with maintainAspectRatio:false fills its parent. Without a bounded
 * parent it resolves to an unbounded height and the canvas grows to thousands of
 * pixels — which looks exactly like a chart that failed to render. This cost a
 * debugging session once; do not remove the height. */
.chart-box { position: relative; block-size: 240px; inline-size: 100%; }

/* ── Hebrew ─────────────────────────────────────────────────────────────── */

/* The Telegram digest is the one Hebrew block in an otherwise LTR interface.
 * Heebo first here: Inter has no Hebrew subset. */
.digest {
  font-family: 'Heebo', system-ui, sans-serif;
  font-size: var(--t-body);
  line-height: 1.8;
  white-space: pre-wrap;
  color: var(--text-2);
}
.he { font-family: 'Heebo', system-ui, sans-serif; }
/* Numbers stay LTR inside Hebrew or they reorder around the comma. */
.n { unicode-bidi: isolate; direction: ltr; display: inline-block; }

details > summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: var(--t-label);
  /* Was 24px. This is the primary disclosure control on the queue, campaigns and
   * products pages, so it has to be a real touch target. Padding rather than
   * height so the text stays optically aligned in its table cell. */
  min-block-size: 44px;
  display: inline-flex;
  align-items: center;
  padding-block: var(--s2);
}
/* The marker, drawn rather than inherited.
 *
 * Changing `display` on a <summary> removes the browser's own disclosure
 * triangle, so making it a flex touch target silently turned every disclosure in
 * the OS into dim text with no affordance — "which ones" on the dashboard read as
 * a stray label rather than a control. */
details > summary::marker,
details > summary::-webkit-details-marker { content: ""; display: none; }
details > summary::before {
  content: "";
  inline-size: 0; block-size: 0;
  margin-inline-end: var(--s2);
  border-block: 4px solid transparent;
  border-inline-start: 6px solid currentColor;
  border-inline-end: 0;
  transition: transform var(--dur-fast) var(--ease);
}
details[open] > summary::before { transform: rotate(90deg); }
details > summary:hover { color: var(--accent); }
details[open] > summary { margin-block-end: var(--s2); }

/* ── the rail's account block: who is signed in, and the way out ────────── */
/*
 * The console had one user and no session, so the rail never had to say whose
 * approvals these are. With real accounts on a public hostname it does: a shared
 * screen with nobody's name on it is how the wrong person approves a budget.
 *
 * Sign-out is a POST, and the button is the only control that reaches it. A GET
 * logout is a URL any page on the internet can put in an <img> tag, which logs
 * you out mid-approval from across the web for free.
 *
 * DECLARED HERE, ABOVE THE MEDIA QUERIES, AND IT MATTERS.
 *
 * This block used to sit at the bottom of the file, after the responsive
 * section. Media queries add no specificity, so every one of the ≤1023px
 * overrides of .rail__account lost to this block on source order alone and had
 * been dead since it was written — including the one whose comment says "a top
 * border across a horizontal bar would draw a line through the middle of the
 * nav", which is exactly what it was doing. The strip was also carrying this
 * rule's 16px block padding and 8px gap it had explicitly cancelled.
 *
 * Nothing about the desktop rail changed in the move. The rules below it now
 * simply win, which is what they were always written to do.
 */
.rail__account {
  /* Pushed to the bottom of the rail — where every application puts the account
     control, and where it does not move when a nav item or the running-job pill
     is added above it. `auto` rather than a fixed margin so a short rail still
     separates it and a tall one does not push it off-screen. */
  margin-block-start: auto;
  margin-inline: var(--s3);
  padding-block: var(--s4);
  border-block-start: 1px solid var(--hairline);
  display: grid;
  justify-items: start;
  gap: var(--s2);
  /* Stuck to the foot of the rail rather than to the end of the nav. Nine links
     plus the group headings already fill a 720px-tall screen, so in flow the sign
     out control sat below the fold — reachable only by scrolling a navigation
     rail to find the one control that is in the same place in every other
     application. Opaque, because the links now scroll underneath it. */
  position: sticky;
  inset-block-end: 0;
  background: var(--ink);
}
.rail__account-name {
  padding-inline: var(--s4);
  font-size: var(--t-label);
  font-weight: 600;
  color: var(--text-2);
  /* A work email has no spaces to break on and the rail is 232px wide. */
  overflow-wrap: anywhere;
}
.rail__account-link {
  padding-inline: var(--s4);
  font-size: var(--t-micro);
  color: var(--text-faint);
  text-decoration-color: var(--text-faint);
}
.rail__account form { margin-inline-start: var(--s4); }

/* ── responsive: two breakpoints, not three ─────────────────────────────── */
/*
 * 767px matches reports/templates/_design.html exactly, so the OS and the
 * reports it produces break at the same width. The previous CSS had three
 * inconsistent breakpoints (720 shell / 800 rows / 767 reports).
 */

@media (max-width: 1023px) {
  .shell { flex-direction: column; }
  .rail {
    inline-size: 100%; flex: none; block-size: auto; position: static;
    border-inline-end: none; border-block-end: 1px solid var(--border);
    padding-block: var(--s4) 0;
    /* `row` is not redundant. The desktop rail is a flex COLUMN so the account
       block can sit at its foot; without this the strip inherits that and the nav
       becomes ten stacked rows filling the whole first screen. */
    display: flex; flex-direction: row;
    flex-wrap: wrap; align-items: center; gap: var(--s1);
  }
  /* In the strip the account is one more item at the end, not a footer. Nothing
     to push it against, nothing to stick to, and a top border across a horizontal
     bar would draw a line through the middle of the nav. */
  .rail__account {
    position: static;
    background: none;
    margin-block-start: 0;
    margin-inline-start: auto;
    padding-block: 0;
    border-block-start: none;
    grid-auto-flow: column;
    align-items: center;
  }
  /* .rail .brand, not .brand. These are rules about the navigation strip, and
     .gate__brand is the same class on the login, password, 2FA and recovery
     pages, which have no rail at all. Unscoped, the strip's layout leaked onto
     the gate: at 375px the login wordmark was picking up display:flex, a sticky
     --ink band and min-block-size, and losing its "Marketing agency" line to the
     strip's own `.brand small { display: none }`. */
  .rail .brand { padding: 0 var(--s4) var(--s3); inline-size: 100%; }
  /* Group headings become inline separators rather than display:none — hiding
   * them left seven undifferentiated links in a wrapped row. */
  .rail__group {
    padding: 0 var(--s2);
    align-self: center;
    color: var(--text-faint);
    border-inline-start: 1px solid var(--border);
  }
  .rail__link {
    /* Icon + label still, not icon-only: an icon-only rail is a memory test, and
     * this is used a few times a day, not a hundred. */
    gap: var(--s1);
    padding: var(--s2) var(--s3);
    min-block-size: 44px;
    margin-inline: 0;
    font-size: var(--t-label);
    border-inline-start: none;
    border-radius: var(--pill-radius);
  }
  .rail__link::before { display: none; }   /* the cursor needs a vertical rail */
  .rail__link .ic { opacity: 1; }
  .rail__link[aria-current="page"] { border-inline-start: none; }
  /* The current item's marker is the 2px spectrum bar from the base rule. In a
     column it runs down the leading edge; in a strip that edge is the wrong one,
     so it becomes an underline.

     What was here instead: two `border-block-end-color` declarations, one
     transparent and one --accent. Both were inert — .rail__link only ever sets a
     border on its inline-start, so a block-end colour had no width to paint —
     and the --accent one was reaching for hazard yellow on a navigation item,
     which .interface-design/system.md lists under "the forbidden list": yellow
     owns data, the spectrum owns chrome. */
  .rail__link[aria-current="page"]::after {
    inset-block: auto 0;
    inset-inline: 0;
    inline-size: auto;
    block-size: 2px;
  }
  .page-head, .wrap { padding-inline: var(--s4); }
}

@media (max-width: 767px) {
  /* One row that scrolls, not five that wrap.
   *
   * The tablet rail wraps, which is right at 768–1023px. At 375px the same rule
   * produced ten links and four headings over five rows — 490px of an 812px
   * viewport, so the entire first screen was navigation and the page title was
   * below the fold. The wordmark stays pinned so you always know where you are;
   * everything else scrolls under it. */
  .rail {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: none;
    padding-block: 0;
    gap: 0;
  }
  .rail::-webkit-scrollbar { display: none; }
  .rail .brand {
    flex: none;
    inline-size: auto;
    padding: 0 var(--s3) 0 var(--s4);
    font-size: var(--t-label);
    /* NOT sticky any more, and this is the important line in the block.
     *
     * The wordmark was pinned to the start of the strip and the account block to
     * its end. Measured at 375px: brand 0–123px, account 135–375px. Both are
     * opaque, so the nine navigation links were visible only through the 12px
     * gap between them — a 1308px strip read through a 12-pixel window. At 320px
     * the two pinned blocks measured 363px against a 320px viewport, overlapped,
     * and left zero pixels of visible navigation.
     *
     * A wordmark is not navigation. It cannot be tapped, it says nothing the
     * page's own <h1> and the browser title do not already say, and here it was
     * spending a third of the strip to say it. Sign-out stays pinned because
     * losing track of it on a shared phone has a real cost; the brand scrolls
     * away with the links, which costs nothing. */
    display: flex;
    align-items: center;
    /* Flex makes "NERITATION" and <em>OS</em> separate items, and the space
       between them collapses. Put it back explicitly. */
    gap: .35ch;
    min-block-size: 48px;
  }
  .rail .brand small { display: none; }
  /* The group headings earn their space at 768px and cost a third of the strip at
   * 375px. The order still groups the links; the labels are the affordable part. */
  .rail__group { display: none; }
  .rail__link { flex: none; white-space: nowrap; }
  .rail__running {
    margin: 0 var(--s3); align-self: center; flex: none;
    /* The label is a whole job description — "Fetch — Google Ads, Meta, TikTok
       (dry run)" — and nothing bounded it. Measured with a job running: the pill
       was 279px wide on a 375px strip and pushed it from 62px to 80px tall, so
       starting a fetch made the navigation worse until it finished. The label
       already sets text-overflow: ellipsis; it had no width to ellipsise
       against. */
    max-inline-size: 10rem;
  }
  .rail__running .running__rail { inline-size: 4rem; }
  /* "running — watch it" next to a bar that is visibly sweeping. The bar says it
     without costing the strip a third row. */
  .rail__running .hint { display: none; }
  /* Pinned to the end of the strip exactly as the wordmark is pinned to its
     start, and for the same reason: sign out is the one control that must not
     require finding out how far the nav scrolls. The name comes with it — a
     shared phone benefits from that more than a desktop does. Change-password
     does not fit and is one tap away on the name's own page. */
  .rail__account {
    position: sticky;
    inset-inline-end: 0;
    background: var(--ink);
    margin-inline-start: auto;
    padding-inline-start: var(--s3);
    gap: 0;
    min-block-size: 48px;
    /* Name ABOVE the button, not beside it. Side by side (the ≤1023 rule's
       grid-auto-flow: column) measured 240px wide — 64% of a 375px viewport,
       permanently parked over the navigation because this block is pinned.
       Stacked it is as wide as its widest child, which is the button: 121px
       measured, so the visible nav window goes 12px -> 254px.
       Both things the rail is asked to say on a phone survive: whose approvals
       these are, and the way out. */
    grid-auto-flow: row;
    justify-items: center;
    align-content: center;
  }
  .rail__account form { margin-inline-start: 0; }
  .rail__account-link { display: none; }
  .rail__account-name {
    padding-inline: 0;
    font-size: var(--t-micro);
    /* A real name or work email must not be allowed to widen the pinned block
       and undo the whole point of stacking it. */
    max-inline-size: 14ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .scope { padding-inline: var(--s3); }
  /* minmax(0, 1fr), not 1fr — see .row above. This single column is the one that
     was letting the campaigns and platform tables push the whole page sideways. */
  .grid.k2, .grid.k3, .grid.k4, .row, .row.k3 {
    grid-template-columns: minmax(0, 1fr); }
  .page-head { padding: var(--s4); }
  .page-head h1 { font-size: var(--t-h2); }
  .wrap { padding-inline: var(--s3); }
  .panel { padding: var(--s4); }
  .stat__value { font-size: var(--t-h1); }
  .result__verdict { font-size: var(--t-h1); }
  .result__stats { gap: var(--s4); }

  /* ── touch targets ──────────────────────────────────────────────────────
   *
   * Density is a desktop decision. A pointer is a pixel; a fingertip is about
   * nine millimetres, and none of these were built for one.
   *
   * .btn--sm is 32px, and the measured list of what that covers on a phone is:
   * Approve and Reject on the queue, the ✕ that deletes a campaign target
   * (35x32), the ✕ that deletes a tracking destination, and the product-view
   * toggles. Two of those spend or unspend a client's money and the first pair
   * sit 8px apart. 32px with an 8px gutter, on the control the whole product
   * exists to serve, is the single worst tap target in the console.
   *
   * They keep 32px at desktop widths, where the density is the point. */
  .btn--sm {
    min-block-size: 44px;
    min-inline-size: 44px;
    padding-inline: var(--s3);
  }
  /* .btn is 40px — WCAG's floor, not its target — and on a phone the extra four
     pixels cost nothing that matters. */
  .btn { min-block-size: 44px; }
  /* Two destructive-or-irreversible controls should not share a gutter sized for
     a mouse. --s3 puts a clear 12px between Approve and Reject. */
  .inline { gap: var(--s3); }

  /* The scope switcher is on every page and was 38px tall. */
  .scope__chip {
    display: inline-flex;
    align-items: center;
    min-block-size: 44px;
  }

  /* <summary> already carries min-block-size: 44px, but it is inline-flex, so
     "set" on the campaigns page rendered a 34px-wide target. */
  details > summary { min-inline-size: 44px; }
}

/* ── .table--stack: the one table that becomes a card list on a phone ───── */
/*
 * Applied to the approval queue, and deliberately nowhere else.
 *
 * The queue is the screen this product exists for — .interface-design/system.md
 * puts it in one word, "decide" — and it is the screen most likely to be opened
 * on a phone between client calls. As a table it measured 748px wide inside a
 * 317px scroll window, with Approve and Reject in the last column: deciding
 * meant swiping ~430px sideways *inside* a container, then hitting a 32px button
 * sitting 8px from its opposite. That is a bad way to spend someone's money.
 *
 * The other wide tables are left as scrolling tables on purpose. Campaigns
 * (1021px), products (921px) and tracking (799px) exist to be compared DOWN a
 * column — is this CPA worse than that one — and column alignment is the whole
 * value. Cards would give each row its own little layout and destroy the
 * comparison. Horizontal scroll is genuinely good at reading; it is only bad at
 * reaching a control. So: scroll for reading, cards for deciding.
 */
@media (max-width: 767px) {
  .table--stack thead { display: none; }
  .table--stack, .table--stack tbody, .table--stack tr, .table--stack td {
    display: block;
    inline-size: auto;
  }
  .table--stack tr {
    padding: var(--s3);
    border: 1px solid var(--hairline);
    background: var(--raised);
    border-radius: var(--panel-radius);
  }
  .table--stack tr + tr { margin-block-start: var(--s3); }
  /* The 1px row lift is a pointer affordance. There is no hover on a phone, and
     on a card it just makes the list twitch during a scroll. */
  .table--stack tbody tr:hover { transform: none; background: var(--raised); }
  .table--stack td {
    display: grid;
    grid-template-columns: 9ch minmax(0, 1fr);
    gap: var(--s3);
    align-items: baseline;
    padding: var(--s1) 0;
    border-block-end: none;
    /* th carried the alignment for .num columns; in a card the label is on the
       left and the value reads better against it. */
    text-align: start;
  }
  /* The label the hidden <th> used to carry, so a value is never orphaned from
     its name. Same type as .strip__label — it is the same idea. */
  .table--stack td::before {
    content: attr(data-label);
    font-size: var(--t-micro);
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-faint);
  }
  /* The decision is not a labelled field. It is the reason the card is on
     screen, so it gets the full width, a rule above it, and room to miss. */
  .table--stack td[data-label=""] {
    grid-template-columns: minmax(0, 1fr);
    margin-block-start: var(--s2);
    padding-block-start: var(--s3);
    border-block-start: 1px solid var(--hairline);
  }
  .table--stack td[data-label=""]::before { content: none; }
  .table--stack td[data-label=""] .inline {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3);
  }
  .table--stack td[data-label=""] .btn { inline-size: 100%; }
  /* A critical row marked its whole card rather than one cell's inset edge,
     which stops existing the moment the cells become blocks. */
  .table--stack tr.is-critical { border-inline-start: 3px solid var(--sev-critical); }
  .table--stack tr.is-critical td:first-child { box-shadow: none; }
  /* Nothing to scroll once the table is a column of cards, and a scroll
     container that cannot scroll still steals a swipe. */
  .table-wrap:has(.table--stack) { overflow-x: visible; border-block-start: none; }
}

/* ── effects: making it feel alive ──────────────────────────────────────── */
/*
 * Rules, from .interface-design/system.md and the skill's motion section:
 *  - Only transform and opacity animate. Never width/height/margin — those trigger
 *    layout and drop frames.
 *  - Entrances play ONCE on page load, never on a loop.
 *  - Actions repeated many times a day (approve, reject) get NO entrance
 *    animation: on a repeated action, motion reads as latency.
 *  - Everything here is switched off wholesale by the reduced-motion block below.
 */

/* Panels cascade in. 40ms stagger — the skill's 30-80ms range. Six steps is
 * enough; beyond that the last card feels late. */
@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.wrap > *, .grid > .panel {
  animation: rise var(--dur-slow) var(--ease) both;
}
.grid > .panel:nth-child(2) { animation-delay: 40ms; }
.grid > .panel:nth-child(3) { animation-delay: 80ms; }
.grid > .panel:nth-child(4) { animation-delay: 120ms; }
.wrap > *:nth-child(2) { animation-delay: 60ms; }
.wrap > *:nth-child(3) { animation-delay: 120ms; }
.wrap > *:nth-child(n+4) { animation-delay: 160ms; }

/* The headline stat gets a slower, larger entrance — it is the focal element. */
.stat__value--accent { animation: rise var(--dur-slow) var(--ease) 120ms both; }

/* Stake bars grow from zero to their data width. The width itself is set inline
 * from the data; this animates the reveal by scaling the fill, so no layout runs. */
@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
.stake__fill {
  transform-origin: left center;
  animation: grow var(--dur-slow) var(--ease) 160ms both;
}

/* A spectrum hairline under the page head. Chrome. */
.page-head::after {
  content: "";
  position: absolute;
  inset-block-end: -1px;
  inset-inline: 0;
  block-size: 1px;
  background: var(--spectrum);
  opacity: .5;
}

/* Rows lift on hover instead of only changing colour. 1px is enough to feel. */
tbody tr {
  transition: background var(--dur) var(--ease), transform var(--dur-fast) var(--ease);
}
tbody tr:hover { transform: translateY(-1px); }

/* Critical tags breathe, once, on arrival — then stop. A looping alert becomes
 * wallpaper within a minute and stops being an alert. */
@keyframes flare {
  0%   { opacity: .4; transform: scale(.96); }
  60%  { opacity: 1; transform: scale(1.04); }
  100% { opacity: 1; transform: none; }
}
.tag--critical { animation: flare var(--dur-slow) var(--ease) 200ms both; }



/* The rail marks the current section with a slow accent sweep on load. */
@keyframes sweep-in {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}
.rail__link[aria-current="page"] {
  position: relative;
}
.rail__link[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 2px;
  background: var(--spectrum);
  border-radius: var(--pill-radius);
  transform-origin: center;
  animation: sweep-in var(--dur-slow) var(--ease) both;
}

/* Count-up on stat values, driven by os.js. The element keeps its final width
 * because the font is tabular, so nothing reflows while the number climbs. */
.stat__value[data-countup] { font-variant-numeric: tabular-nums; }

/* ── icons ──────────────────────────────────────────────────────────────── */

.ic {
  flex: 0 0 auto;
  vertical-align: -0.14em;   /* optical baseline, not geometric */
  opacity: .85;
}
/* DESIGN.md §6 rule 7: icons that imply direction mirror in RTL; icons that do
   not (clock, warning) never do. Only [data-dir] icons carry the meaning. */
[dir="rtl"] .ic[data-dir] { transform: scaleX(-1); }

/* ── states: the fastest tell of an unfinished interface ────────────────── */

/* Disabled was opacity:.5 alone, which dims but does not *say* disabled. */
.btn[disabled], .btn[aria-disabled="true"],
input[disabled], select[disabled], textarea[disabled] {
  opacity: .45;
  cursor: not-allowed;
  filter: saturate(.4);
  box-shadow: none;
  transform: none;
}
.btn[disabled]:hover, .btn[aria-disabled="true"]:hover { filter: saturate(.4); }

/* Skeletons. A page that pops in fully formed or not at all has no loading
   state; these give the shape of the content while it arrives. */
@keyframes shimmer { to { background-position-x: -200%; } }
.skeleton {
  background: linear-gradient(90deg,
              var(--surface-1) 0%, var(--surface-2) 40%, var(--surface-1) 80%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
  border-radius: var(--control-radius);
  color: transparent !important;
  user-select: none;
  pointer-events: none;
}
.skeleton--line { block-size: 1em; margin-block: .25em; }
.skeleton--stat { block-size: var(--t-stat); inline-size: 60%; }

/* The one real async gap in a server-rendered app: Chart.js is fetched from a CDN
 * and initialises after paint, so .chart-box sits empty for a beat on a slow
 * connection. The skeleton is a ::before so it needs no extra markup, and it is
 * removed the moment a canvas has been drawn into.
 *
 * Deliberately NOT a generic page-level loader — there is no other async content
 * here, and a loader with nothing to load is the kind of dead feature this system
 * keeps catching elsewhere. */
.chart-box:not(.is-drawn)::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--control-radius);
  background: linear-gradient(90deg,
              var(--surface-1) 0%, var(--surface-2) 40%, var(--surface-1) 80%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
  opacity: .6;
}

/* ── empty states get a mark, not just a sentence ───────────────────────── */

.empty__mark {
  inline-size: 44px;
  block-size: 44px;
  margin-inline: auto;
  margin-block-end: var(--s4);
  display: grid;
  place-items: center;
  border-radius: var(--pill-radius);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--text-faint);
}
.empty__mark .ic { opacity: 1; }

/* ── motion hooks (motion.js) ────────────────────────────────────────────── */

/* Reveal. The pre-reveal state must be a *transition* start, not an animation,
 * so that if JS never runs the element is simply visible. .is-revealed is added
 * by the IntersectionObserver. */
[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-slow) var(--ease),
              transform var(--dur-slow) var(--ease);
}
[data-reveal].is-revealed { opacity: 1; transform: none; }

/* Tilt. will-change only while the pointer is inside — permanent will-change on
 * every panel keeps a compositor layer alive for nothing. */
[data-tilt] { transform-style: preserve-3d; }
[data-tilt]:hover { will-change: transform; }

[data-magnet] { will-change: auto; }
[data-magnet]:hover { will-change: transform; }

/* Word stagger. */
@keyframes word-in {
  from { opacity: 0; transform: translateY(.5em) rotate(1.5deg); }
  to   { opacity: 1; transform: none; }
}
[data-stagger] .word {
  display: inline-block;
  animation: word-in var(--dur-slow) var(--ease) both;
}

/* Two things deleted rather than tuned.
 *
 * `.spectrum-text` — gradient-filled display text. Referenced by no template; it
 * was written for a heading that never used it. Gradient text is the third
 * member of the generated-interface set, after the gradient button and the
 * glow, and it had no reason to survive its own disuse.
 *
 * The button sheen — a white shimmer crossing the primary button on hover, taken
 * from utm-vision-suite. It made sense on a gradient. On solid yellow it is a
 * flare across the one control that has to look like it means it, and a hover
 * effect whose only content is "look, an effect" is the definition of the thing
 * Anton is objecting to.
 */

/* ── reduced motion ─────────────────────────────────────────────────────── */
/*
 * Keep opacity and colour, drop movement. Chart.js is told separately in
 * charts.js — it animates by default and does not read this.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .running__rail::after { animation: none; inline-size: 100%; opacity: .5; }
  /* Entrances become instant rather than absent, so nothing is left invisible
   * by an animation that never ran. */
  .wrap > *, .grid > .panel, .stat__value--accent, .stake__fill,
  .tag--critical, .rail__link[aria-current="page"]::after {
    animation: none !important; opacity: 1 !important; transform: none !important; }
  tbody tr:hover { transform: none; }
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  [data-stagger] .word { animation: none !important; opacity: 1 !important; }
  body::before { animation: none !important; }
  .btn--primary:hover::after { transform: none !important; transition: none !important; }
  .panel:hover::before, .panel:hover::after { transform: none; }
  .btn:active { transform: none; }
  .btn--primary:active { transform: skewX(-8deg); }
}

/* ── print ──────────────────────────────────────────────────────────────── */

@media print {
  .rail, .actions, .btn, .skip { display: none !important; }
  body { background: var(--print-bg); color: var(--print-text); }
  .panel { break-inside: avoid; }
}

/* A panel nested inside a panel. Same padding rhythm, no border and no corner
   ornaments — a frame inside a frame reads as two competing objects, and the
   ornaments are a signature that loses its meaning when it repeats. */
.panel--flat { background: var(--raised); border: none; padding: var(--s4); }
.panel--flat::before, .panel--flat::after { display: none; }

/* The build stamp. Bottom of every page, quiet enough to ignore until the moment
   somebody needs to know whether they are looking at old code. */
.build {
  position: fixed;
  inset-block-end: 0;
  inset-inline-end: 0;
  padding: var(--s1) var(--s3);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  background: var(--ink);
  border-block-start: 1px solid var(--hairline);
  border-inline-start: 1px solid var(--hairline);
  pointer-events: none;
  z-index: 5;
}
@media (max-width: 767px) { .build { display: none; } }

/* The rail's account block used to be declared HERE, below the responsive
   section. See its new home above the media queries for why it moved. */

/* ── the gate: the two pages you can see logged out ─────────────────────── */
/*
 * login.html and password.html deliberately do NOT extend _layout.html. The rail
 * is a menu of places this visitor cannot go: drawing it around a login form
 * describes the console to whoever is guessing at the password, and offers ten
 * links that all bounce straight back here.
 *
 * Same tokens, same faces, same glass panel — so it is recognisably the same
 * product, without being the same page.
 */
.gate {
  min-block-size: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--s5);
  padding: var(--s6) var(--s5);
}
.gate__brand { padding: 0; text-align: center; }
.gate__card { inline-size: min(26rem, 100%); }
.gate__title {
  font-size: var(--t-h2);
  font-weight: 700;
  letter-spacing: -.01em;
}
/* The one line that says what this box is for, under the title rather than in a
   placeholder — placeholder text disappears exactly when it is being read. */
.gate__lede {
  margin: var(--s1) 0 0;
  color: var(--text-dim);
  font-size: var(--t-label);
}
.gate__foot {
  margin: 0;
  color: var(--text-faint);
  font-size: var(--t-micro);
  text-align: center;
  max-inline-size: 34ch;
}

/* The enrolment QR. Its black and white are baked into the SVG by segno rather
   than themed here — a QR that inherits a dark surface does not scan, and the
   quiet zone has to be light for the camera to find the corners. */
.gate__qr {
  display: block;
  margin-inline: auto;
  inline-size: min(15rem, 100%);
  block-size: auto;
  border-radius: var(--control-radius);
}

/* The same secret in text, for the phone that IS the machine, the desktop
   authenticator, and the screen reader. Selectable, so it can be copied. */
.gate__secret {
  display: block;
  margin-block-start: var(--s1);
  padding: var(--s2) var(--s3);
  background: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--control-radius);
  font-size: var(--t-label);
  letter-spacing: .08em;
  overflow-wrap: anywhere;
  user-select: all;
}

/* Recovery codes, shown exactly once. Two columns of monospace so a mis-read
   character is obvious while they are being copied down.

   The track minimum was 11ch and the codes are 13 characters. At 375px that
   resolved to three 84px columns and every single code wrapped onto two lines,
   breaking after its hyphen — measured, two line boxes per <li>. A code you are
   transcribing once, under pressure, split across two lines with a hyphen at the
   break is a code you cannot tell from a code with no hyphen in it. On the one
   page that can never be shown again.

   13ch is the real width of the content, min() lets it fall to one column if a
   viewport genuinely cannot hold two, and nowrap makes the guarantee absolute
   rather than a consequence of arithmetic that was already wrong once. */
.gate__codes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13ch), 1fr));
  gap: var(--s2);
  margin: 0;
  padding: var(--s3);
  list-style: none;
  background: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--control-radius);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--text);
  user-select: all;
}
.gate__codes li { white-space: nowrap; }
