/* ============================================================
   SORYVEX — design system
   Single token source + component library. See spec §2 / §3.
   ============================================================ */

/* ---------- Fonts (§2.2) ---------- */

@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/static/fonts/jbmono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/static/fonts/jbmono-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens (§2.1 / §2.2 / §2.3 / §2.4 / §2.5 / §2.7) ---------- */

:root {
  /* Surfaces */
  --bg:              #09090b;
  --bg-subtle:       #0c0c0f;
  --surface:         #111113;
  --surface-raised:  #18181b;
  /* Second surface level: interactive hover fills, one step above --surface-raised. */
  --surface-2:       #1f1f23;

  /* Borders */
  --border:          rgba(255,255,255,0.06);
  --border-strong:   rgba(255,255,255,0.10);
  --border-focus:    var(--accent);

  /* Text */
  --text:            #fafafa;
  --text-secondary:  #a1a1aa;
  --text-tertiary:   #89899a;

  /* Brand accent */
  --accent:          #38bdf8;
  --accent-hover:    #7dd3fc;
  --accent-muted:    rgba(56,189,248,0.12);
  --accent-ink:      #041016;

  /* Brand accent as RGB channels.
     Every tint and glow in the product is "the accent at some opacity". Spelling
     each one out as a literal rgba(56,189,248,…) is why the light theme could not
     re-point them: --accent became #1b6ea7 in the light palette, but thirty
     declarations kept the dark theme's blue, so a light page still carried
     dark-theme glows and one light-blue halo on near-white. Declaring the
     channels once and deriving every tint from them means a theme flip is one
     line here and nothing in the sheet can be left behind. */
  --accent-rgb:       56 189 248;

  /* The neutral overlay, the same way: white on the dark theme, ink on the light
     one. It is what hover fills, hairline borders and inset highlights are made
     of — a "just off the surface" wash, which has to invert with the surface or
     it disappears. */
  --neutral-rgb:      255 255 255;

  /* A deeper brand blue, for the gradient ends where --accent is too light to
     carry a shape on its own. */
  --accent-deep:      #2563eb;
  --accent-deep-rgb:  37 99 235;

  /* Password-strength meter: the "Good" tier needs a colour between the amber
     warning and green success, so it gets a token of its own. */
  --strength-good:  #84cc16;

  /* Ink for text set ON a saturated fill. `--accent-ink` covers the brand
     surface; these cover the other three, so a filled status button or a count
     badge is `color: var(--x-ink)` rather than a hand-picked near-black that
     drifts a shade every time a state colour is re-tuned. The dark theme's
     warning and success are light enough that near-white on them fails, which
     is why these are ink and not white; the light theme's are darker still
     (see the light block), so there they flip to white. */
  --success-ink:      #04140a;
  --warning-ink:      #1a1200;
  --danger-ink:       #ffffff;

  /* Semantic state (generic UI only) */
  --success:         #22c55e;
  --success-muted:   rgba(34,197,94,0.12);
  --success-text:    #86efac;
  --warning:         #eab308;
  --warning-muted:   rgba(234,179,8,0.12);
  --warning-text:    #fde047;
  --danger:          #ef4444;
  --danger-muted:    rgba(239,68,68,0.12);
  /* Filled danger surface that carries white text (count badges). --danger
     is tuned to be legible as *text* on the dark surface, where white on it
     is only 3.76:1; a badge needs the darker step to clear 4.5:1. */
  --danger-solid:   #dc2626;
  --danger-text:     #fca5a5;
  --info:            var(--accent);
  --info-muted:      var(--accent-muted);
  --info-text:       #bae6fd;

  /* Severity — findings/report context ONLY */
  --sev-critical:       #f87171;
  --sev-critical-bg:    rgba(248,113,113,0.12);
  --sev-high:           #fb923c;
  --sev-high-bg:        rgba(251,146,60,0.12);
  --sev-medium:         #fbbf24;
  --sev-medium-bg:      rgba(251,191,36,0.12);
  --sev-low:            #60a5fa;
  --sev-low-bg:         rgba(96,165,250,0.12);
  --sev-info:           #94a3b8;
  --sev-info-bg:        rgba(148,163,184,0.12);

  /* Type (§2.2) */
  /* Same stack as tokens.css, and it has to stay the same value: two sheets
     that declare --font differently are two answers to "what is the body
     font", and the one that loses is whichever page loads last. The named
     scripts beyond Latin are listed because the generic fallback synthesises
     bold for them, which is how a non-Latin heading ends up looking smeared
     next to a Latin one. Inter is still first, so no existing text moves. */
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Noto Sans Hebrew", "Noto Sans Arabic", "Noto Sans Devanagari",
          "Noto Sans", "Arial Hebrew", "Geeza Pro", "Arial Unicode MS",
          "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei",
          "Hiragino Sans", "Malgun Gothic", sans-serif;
  --mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace;

  /* Spacing (§2.3) — one 8px-based scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;

  /* Radius (§2.4) */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-full: 999px;

  /* Shadow / elevation (§2.5) — floating elements only */
  --shadow-sm: 0 4px 12px rgba(0,0,0,0.3);
  --shadow-md: 0 8px 24px rgba(0,0,0,0.4);
  --shadow-lg: 0 24px 48px rgba(0,0,0,0.5);
  --shadow-focus: 0 0 0 3px var(--accent-muted);

  /* Z-index (§2.7) */
  --z-sticky: 90;
  --z-mobile-nav: 99;
  --z-dropdown: 100;
  --z-modal-backdrop: 1000;
  --z-modal: 1001;
  --z-toast: 2000;

  /* Motion — a three-step duration scale plus one easing, so every transition
     in the product comes from the same set. */
  --speed:          0.12s;   /* hover, colour, border, opacity */
  --speed-md:       0.2s;    /* larger movement: slide, expand, drawer */
  --speed-lg:       0.32s;   /* reveals, modals, toasts */
  --ease:           cubic-bezier(0.4, 0, 0.2, 1);
  /* One deliberate exception to the three-step scale: the scroll reveal. It is
     the only motion on the site that starts on its own rather than on a user
     action, so it needs long enough to read as an entrance and a springier
     curve than an interaction should have. */
  --speed-reveal:   0.55s;
  --ease-reveal:    cubic-bezier(0.22, 1, 0.36, 1);
  /* Bar fills (plan usage, progress). Slower than --speed-md so the change is
     legible as a change, not as a flicker. */
  --speed-slow:     0.5s;

  --maxw: 1120px;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

/* Horizontal overflow safety for every page that is not the Agent Hub.
   The console has had this since it shipped its permission matrix; the public
   and account shells did not, so one wide element anywhere in them dragged the
   whole document sideways.

   It is not hypothetical. `/example-report` embeds a stored report whose
   findings and per-module coverage tables run to six columns. On the example
   page - which strips the report's shell, and with it the script that wraps
   those tables - nothing contained them, and at 375px the page measured a
   929px scrollWidth in a 339px column: the headline, the score and the
   navigation all slid off the left to read one row. On the public example, on a
   phone, which is most of its audience.

   `clip` and not `hidden`: `clip` contains the overflow without making the
   element a scroll container, so `position: sticky` on the account topbar and
   the app header keeps working, and it does not become a containing block for
   `position: fixed` the way `contain: paint` would. The Agent Hub made the same
   choice for the same reason - see `overflow-x: clip` in `agent.css`, which
   records the same reasoning.

   Deliberately not a substitute for wrapping the wide table in a scroll box.
   This stops a mistake from moving the whole page; it does not make a 970px
   table readable on a 375px screen. */
html, body { overflow-x: clip; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

::selection { background: rgb(var(--accent-rgb) / 0.2); }

a { color: var(--text-secondary); text-decoration: none; transition: color var(--speed) var(--ease); }
a:hover { color: var(--text); }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
  letter-spacing: -0.025em;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.125rem; }
h4 { font-size: 0.875rem; font-weight: 600; }

p { margin: 0 0 1rem; color: var(--text-secondary); }
p:last-child { margin-bottom: 0; }

code, .mono { font-family: var(--mono); font-size: 0.8125rem; }
.mono-500 { font-family: var(--mono); font-weight: 500; }

.container { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
.section { max-width: var(--maxw); margin: 0 auto; padding: var(--space-20) 24px; }

.muted { color: var(--text-tertiary); }
.small { font-size: 0.8125rem; }
/* These are text, so they use the text-safe tokens. `.color_warn` already did;
   `.color_pos` and `.color_neg` did not, which is why the light theme's
   "50/100" score reads at 3.30:1 and a negative figure at 4.23:1 on a tinted
   row. `--success-text` / `--danger-text` are the same hues taken dark enough
   for body text: 5.35:1 and 6.47:1 on a white card. The fill tokens
   (`--success`, `--danger`) stay where they belong - segment fills, status
   dots, borders, chart strokes. */
.color_pos { color: var(--success-text); }
.color_neg { color: var(--danger-text); }
.color_warn { color: var(--warning-text); }
.fw-600 { font-weight: 600; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
.d-inline { display: inline; }
.ml-auto { margin-left: auto; }
.maxw-480 { max-width: 480px; }
.maxw-460 { max-width: 460px; }
.maxw-520 { max-width: 520px; }
.maxw-560 { max-width: 560px; }
.maxw-600 { max-width: 600px; }
.maxw-640 { max-width: 640px; }
.maxw-720 { max-width: 720px; }
.maxw-800 { max-width: 800px; }
.stack-sm { display: flex; flex-direction: column; gap: 10px; }

.input-domain { font-family: var(--mono); flex: 1; }

.steps-list { margin: 0; padding-left: 20px; color: var(--text-secondary); font-size: 0.875rem; display: grid; gap: 10px; }
.steps-list b { color: var(--text); font-weight: 600; }

hr.sep { border: none; border-top: 1px solid var(--border); margin: 0; }

.eyebrow {
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}

/* ============================================================
   BUTTONS (§3.1)
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font: inherit;
  font-weight: 500;
  font-size: 0.875rem;
  border: none;
  border-radius: var(--radius-sm);
  padding: 9px 16px;
  cursor: pointer;
  transition: background var(--speed) var(--ease), color var(--speed) var(--ease), opacity var(--speed) var(--ease);
  white-space: nowrap;
  line-height: 1.2;
  min-width: 0;
}
.btn:active { opacity: 0.85; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* THE primary action per screen — solid accent fill, accent-ink text */
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
}
.btn-primary:hover { background: var(--accent-hover); color: var(--accent-ink); }

.btn-secondary {
  background: var(--surface);
  color: var(--text-secondary);
  border: 1px solid var(--border-strong);
}
.btn-secondary:hover { background: var(--surface-raised); color: var(--text); }

.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
}
.btn-ghost:hover { color: var(--text); background: rgb(var(--neutral-rgb) / 0.06); }

.btn-danger {
  background: var(--danger-muted);
  color: var(--danger-text);
}
.btn-danger:hover { background: rgba(239,68,68,0.2); }

.btn-sm { padding: 6px 10px; font-size: 0.8125rem; }
.btn-lg { padding: 12px 24px; font-size: 0.9375rem; }
.btn-block { width: 100%; }

.btn .spinner { width: 14px; height: 14px; }

/* ============================================================
   FORMS (§3.2)
   ============================================================ */

.field { margin-bottom: var(--space-4); }
.field > label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.field > label .hint { font-weight: 400; color: var(--text-tertiary); }

.input, .select, textarea.input {
  width: 100%;
  font: inherit;
  font-size: 0.875rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  transition: border-color var(--speed) var(--ease), box-shadow var(--speed) var(--ease);
  appearance: none;
}
textarea.input { resize: vertical; min-height: 80px; }
.input::placeholder { color: var(--text-tertiary); }
.input:focus, .select:focus, textarea.input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}
.input.is-error { border-color: var(--danger); box-shadow: 0 0 0 3px var(--danger-muted); }

.input:disabled {
  background: var(--bg);
  color: var(--text-tertiary);
  border-color: var(--border);
  cursor: not-allowed;
}
.input:disabled:focus { outline: none; box-shadow: none; }

.select {
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2371717a' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

.form-error { color: var(--danger-text); font-size: 0.8125rem; margin-top: 6px; min-height: 18px; }
.form-note { color: var(--text-tertiary); font-size: 0.8125rem; margin-top: 6px; }

/* The password toggle, strength meter and checkbox all have a single
   definition in the AUTH section further down this sheet. The copies that
   used to live here were not merely redundant — they disagreed:

     .input-password-wrap .input   44px of right padding here, 52px there
     .strength                    3px gap / 3px bars here, 4px / 4px there
     .strength .seg.s1/.s2/.s3    a three-tier contract nothing emits; the
                                  live meter is four `.seg[data-tier]` bars
                                  with a `.filled` state
     .check input                 a native checkbox with accent-color here, a
                                  fully custom appearance: none box there

   Equal specificity, so the later block won on every one of those and this
   half was dead. Removed rather than reconciled. */
.reg-strength-label, .reset-strength-label { font-size: 0.75rem; color: var(--text-tertiary); }

/* ============================================================
   CARDS / PANELS (§3.3)
   ============================================================ */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
}
.card.flush { padding: 0; }
.card-tinted {
  background: var(--accent-muted);
  border: 1px solid rgb(var(--accent-rgb) / 0.18);
}
.card-title { font-size: 1.125rem; font-weight: 600; margin-bottom: 4px; color: var(--text); }
.card-sub { color: var(--text-tertiary); font-size: 0.8125rem; margin-top: 4px; }

.panel-title {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: var(--space-4); padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.panel-title .card-title { margin-bottom: 0; }

/* ============================================================
   BADGES & STATUS (§3.5)
   ============================================================ */

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.75rem; font-weight: 500;
  padding: 3px 10px; border-radius: var(--radius-full);
  background: rgb(var(--neutral-rgb) / 0.06); color: var(--text-secondary);
  white-space: nowrap;
}
.badge-accent { background: var(--accent-muted); color: var(--accent); }
.badge-success { background: var(--success-muted); color: var(--success-text); }
.badge-warning { background: var(--warning-muted); color: var(--warning-text); }
.badge-danger { background: var(--danger-muted); color: var(--danger-text); }
.badge-neutral { background: rgb(var(--neutral-rgb) / 0.06); color: var(--text-secondary); }
.badge-outline { background: transparent; border: 1px solid var(--border-strong); color: var(--text-secondary); }

.status-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-tertiary); flex: none;
}
.status-dot.is-accent { background: var(--accent); }
.status-dot.is-success { background: var(--success); }
.status-dot.is-warning { background: var(--warning); }
.status-dot.is-danger { background: var(--danger); }
.status-dot.is-running { background: var(--accent); animation: pulse 1.8s ease-in-out infinite; }

@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.4); }
  50% { box-shadow: 0 0 0 5px rgb(var(--accent-rgb) / 0); }
}

/* Severity pill — findings/report context only, distinct from generic badges */
.sev {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.6875rem; font-weight: 500;
  letter-spacing: 0.05em; text-transform: uppercase;
  padding: 4px 10px; border-radius: var(--radius-full);
  white-space: nowrap;
}
.sev.critical { background: var(--sev-critical-bg); color: var(--sev-critical); }
.sev.high     { background: var(--sev-high-bg);     color: var(--sev-high); }
.sev.medium   { background: var(--sev-medium-bg);   color: var(--sev-medium); }
.sev.low      { background: var(--sev-low-bg);      color: var(--sev-low); }
.sev.info     { background: var(--sev-info-bg);     color: var(--sev-info); }

/* ============================================================
   TABLES (§3.4)
   ============================================================ */

/* `position: relative` is load-bearing, not decoration. A scroll container only
   clips a descendant if it is in that descendant's containing-block chain, and
   an absolutely-positioned element's containing block skips any `static`
   ancestor. `.visually-hidden` is `position: absolute`, so inside a static
   `.table-wrap` it escaped the overflow clip entirely: on /pricing at 320px the
   screen-reader-only "Included" spans sat at their static position 465px across
   a 320px page and widened the document to 374px, giving the page a horizontal
   scrollbar. Declaring the wrap `relative` makes it the containing block so
   `overflow-x: auto` clips them as intended. */
.table-wrap {
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
table.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table thead th {
  text-align: left;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  font-weight: 500;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
}
.table tbody td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  color: var(--text-secondary);
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background var(--speed) var(--ease); }
.table tbody tr:hover { background: rgb(var(--neutral-rgb) / 0.03); }
.table .cell-id { font-family: var(--mono); font-size: 0.75rem; color: var(--text-tertiary); }
.table .cell-domain { font-family: var(--mono); font-size: 0.84375rem; color: var(--text); }
/* Wide tables (e.g. assessment history): keep columns readable and let .table-wrap scroll */
.table.table-wide { min-width: 1000px; }

/* ============================================================
   ALERTS / TOASTS (§3.8)
   ============================================================ */

.alert {
  display: flex; gap: 10px; align-items: flex-start;
  border-radius: var(--radius-sm); padding: 12px 14px; font-size: 0.875rem;
  background: var(--surface);
}
.alert-error { background: var(--danger-muted); color: var(--danger-text); }
.alert-success { background: var(--success-muted); color: var(--success-text); }
.alert-warn, .alert-warning { background: var(--warning-muted); color: var(--warning-text); }
.alert-info { background: var(--accent-muted); color: var(--info-text); }

/* Two-tier owner-verification notice (app banners + report header line).
   New Hebrew-RTL widget; embedded LTR values use dir="ltr" spans. */
.verify-note {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  border: 1px solid var(--border-strong); background: var(--surface);
  border-radius: var(--radius-sm); padding: 12px 14px; margin: 18px 0;
  font-size: 0.875rem;
}
.verify-note .badge { white-space: nowrap; }

.toast-stack {
  position: fixed; top: 16px; right: 16px; z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: 8px;
  width: min(340px, calc(100vw - 32px));
}
.toast {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-size: 0.875rem;
  animation: toast-in .18s ease-out;
}
.toast.out { animation: toast-out .12s ease-in forwards; }
.toast-success { border-left: 2px solid var(--success); }
.toast-error { border-left: 2px solid var(--danger); }
.toast-info { border-left: 2px solid var(--accent); }
.toast-title { font-weight: 600; margin-bottom: 2px; font-size: 0.8125rem; }
.toast-body { color: var(--text-secondary); font-size: 0.8125rem; }

@keyframes toast-in { from { opacity: 0; transform: translateY(-6px); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(-4px); } }

/* ============================================================
   MODAL (§3.7)
   ============================================================ */

.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-modal-backdrop);
  display: none; align-items: center; justify-content: center; padding: 20px;
  backdrop-filter: blur(4px);
}
.modal-backdrop.open { display: flex; animation: modal-backdrop-in .18s ease-out; }
.modal {
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  width: 100%; max-width: 440px; padding: var(--space-6); box-shadow: var(--shadow-lg);
  z-index: var(--z-modal);
  animation: modal-in .18s ease-out;
}
.modal .modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.modal h3 { margin-bottom: 4px; }
.modal .modal-close {
  background: none; border: none; color: var(--text-tertiary); cursor: pointer;
  padding: 4px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
}
.modal .modal-close:hover { color: var(--text); }
.modal .modal-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.modal .modal-body { color: var(--text-secondary); font-size: 0.875rem; }
.modal .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: var(--space-5); }

@keyframes modal-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes modal-backdrop-in { from { opacity: 0; } }

/* Keyboard-shortcut help. Rendered by app.js from the modal component above.
   `kbd` is styled globally rather than scoped, because it is the one element
   whose meaning is entirely typographic and which should look the same
   wherever a product surface shows a key. */
/* Copy confirmation, set by app.js's single [data-copy]/[data-copy-value]
   handler and shared by both shells. agent.css carries the console's own
   .hub-copy[data-copied] rule; this is the product half, so a copy button
   outside the console still shows that it worked. */
[data-copied="1"] {
  color: var(--success-text);
  border-color: var(--success);
}

.shortcut-modal { max-width: 420px; }
.shortcut-list { display: grid; gap: 2px; margin: 0 0 var(--space-4); }
.shortcut-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: 7px 10px; border-radius: var(--radius-sm);
}
.shortcut-row:nth-child(odd) { background: var(--surface-2); }
.shortcut-row dt, .shortcut-row dd { margin: 0; }
.shortcut-row dd { color: var(--text-secondary); font-size: 0.875rem; }
kbd {
  display: inline-block;
  min-width: 22px;
  padding: 2px 7px;
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.4;
  text-align: center;
}

/* ============================================================
   SPINNER / PROGRESS (§3.10)
   ============================================================ */

.spinner {
  width: 16px; height: 16px; border-radius: 50%; flex: none;
  border: 2px solid var(--border-strong); border-top-color: var(--accent);
  animation: spin .6s linear infinite;
}
.spinner.large { width: 28px; height: 28px; border-width: 2.5px; }
@keyframes spin { to { transform: rotate(360deg); } }

.progress {
  width: 100%; height: 6px; background: rgb(var(--neutral-rgb) / 0.06);
  border-radius: var(--radius-full); overflow: hidden;
}
.progress-bar {
  height: 100%; background: var(--accent); border-radius: var(--radius-full);
  transition: width var(--speed-slow) var(--ease);
}

/* Compact progress used inside table rows */
.progress-sm { width: 90px; height: 5px; }

/* The live-scan signature gradient — reserved for scan page only */
.progress-bar.gradient {
  background: linear-gradient(90deg, var(--accent-deep), var(--accent));
}

/* ============================================================
   EMPTY / LOADING / ERROR STATES (§3.9)
   ============================================================ */

.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: var(--space-3); padding: var(--space-12) var(--space-6);
  color: var(--text-tertiary);
}
.empty .empty-icon {
  width: 44px; height: 44px; border-radius: var(--radius-md); display: grid; place-items: center;
  background: var(--surface-raised); color: var(--text-tertiary);
}
.empty .empty-icon svg { width: 20px; height: 20px; }
/* The empty state's heading level depends on where it is placed — a direct
   child of the page (h2) or inside a card that already has a heading (h3) —
   so all three levels are styled together. Styling only `h4` meant the
   ui.empty() macro, which emits an `h3`, rendered its title at the browser's
   default h3 size: visibly larger than every hand-rolled empty state in the
   product. */
.empty h2,
.empty h3,
.empty h4 { color: var(--text-secondary); font-weight: 600; }
.empty p { font-size: 0.8125rem; max-width: 320px; margin: 0 auto; }

.skeleton {
  position: relative; overflow: hidden;
  background: rgb(var(--neutral-rgb) / 0.04); border-radius: var(--radius-sm);
}
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgb(var(--neutral-rgb) / 0.04), transparent);
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer { 100% { transform: translateX(100%); } }

/* ============================================================
   PUBLIC: NAV (§3.6)
   ============================================================ */

.site-nav {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: rgba(9,9,11,0.85);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
.site-nav-inner {
  max-width: var(--maxw); margin: 0 auto; padding: 0 24px;
  height: 56px; display: flex; align-items: center; gap: 24px;
}
.brand {
  display: inline-flex; align-items: center; gap: 9px;
  font-weight: 600; font-size: 0.9375rem; color: var(--text); letter-spacing: -0.01em;
}
.brand:hover { color: var(--text); }
.brand svg { display: block; }
.brand .brand-logo { display: block; border-radius: 6px; object-fit: cover; flex-shrink: 0; }
.brand-sub {
  font-family: var(--mono); font-size: 0.625rem; color: var(--text-tertiary);
  letter-spacing: 0.1em; text-transform: uppercase; font-weight: 400;
}
.nav-links { display: flex; align-items: center; gap: 20px; margin-left: auto; font-size: 0.8125rem; }
.nav-links a { color: var(--text-tertiary); font-weight: 500; }
.nav-links a:hover { color: var(--text); }
.nav-cta { display: flex; gap: 8px; align-items: center; }

.nav-burger {
  display: none; background: none; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); color: var(--text-secondary); width: 36px; height: 36px;
  cursor: pointer; font-size: 16px;
  align-items: center; justify-content: center; padding: 0;
}
.nav-burger:hover { color: var(--text); }
.nav-burger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.mobile-nav {
  position: fixed; inset: 0; z-index: var(--z-mobile-nav); background: var(--bg);
  padding: 72px 24px 30px; display: none; flex-direction: column; gap: 0;
}
.mobile-nav.open { display: flex; }
.mobile-nav a {
  font-size: 1.125rem; font-weight: 500; padding: 14px 0;
  color: var(--text-secondary); border-bottom: 1px solid var(--border);
}
.mobile-nav a:hover { color: var(--text); }

/* PUBLIC: FOOTER

   The live rules are further down this sheet, in the SITE FOOTER section,
   which the markup in _footer.html was rebuilt against: it styles
   .footer-brand-col / .footer-tagline / .footer-heading / .footer-row /
   .footer-links / .site-footer-compact. This block used to sit here as well,
   styling a .footer-company column and an <h5> heading that the template no
   longer emits, and it fought the real rules on .site-footer's background and
   .site-footer-inner's padding with equal specificity — resolved only by
   source order, so which of the two footers a page actually got depended on
   which came last in the file. Removed rather than merged. */

.pricing-grid {
  position: relative; display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 18px; align-items: stretch; background: transparent; border-radius: var(--radius-md);
}
/* Billing plan tiers — 4 equal columns in one row; 2x2 mid-size; single column on mobile */
.pricing-grid.billing-tiers { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1024px) {
  .pricing-grid.billing-tiers { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .pricing-grid.billing-tiers { grid-template-columns: 1fr; }
}
.plan {
  display: flex; flex-direction: column; position: relative;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-8) 26px 28px;
  transition: transform var(--speed-md) var(--ease), border-color var(--speed) var(--ease), box-shadow var(--speed) var(--ease);
}
.plan:hover { transform: translateY(-4px); border-color: var(--border-strong); }
.plan.featured {
  background: linear-gradient(180deg, rgb(var(--accent-rgb) / 0.08), var(--surface) 45%);
  border-color: rgb(var(--accent-rgb) / 0.4);
  box-shadow: 0 0 0 1px rgb(var(--accent-rgb) / 0.06), 0 34px 70px -30px rgb(var(--accent-rgb) / 0.3);
  transform: translateY(-8px);
  padding: 48px 26px 28px;
}
.plan.featured:hover { transform: translateY(-10px); border-color: rgb(var(--accent-rgb) / 0.6); }
.plan-flag {
  position: absolute; top: 0; left: 50%; transform: translate(-50%, -50%);
  z-index: 1; padding: 5px 12px; border-radius: var(--radius-full);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--mono); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; white-space: nowrap;
  box-shadow: 0 8px 22px -8px rgb(var(--accent-rgb) / 0.7);
}
.plan h3 { font-size: 0.875rem; color: var(--text-secondary); font-weight: 500; margin-bottom: var(--space-2); }
.plan .price {
  font-size: 2.5rem; font-weight: 700; letter-spacing: -0.04em;
  color: var(--text); margin-bottom: 4px; line-height: 1;
}
.plan .price .curency { font-size: 1.25rem; vertical-align: top; color: var(--text-tertiary); font-weight: 500; }
.plan .price-period { font-size: 0.8125rem; color: var(--text-tertiary); margin-bottom: var(--space-6); }
/* The free plan has no money in its price line, so it needs the size and
   weight a number would have had, or the card looks like it is hiding one. */
.plan .price-free { font-size: 2.25rem; font-weight: 700; letter-spacing: -0.02em; }
/* The three cards are stretched to a common height, and the feature list was
   the element absorbing the slack (`flex: 1`). Because the list is a grid, the
   extra height was shared out between its rows — so a four-feature plan got
   58px rows and the six-feature plan got 33px, and nothing lined up across the
   cards. That is the one thing a pricing table exists to make possible.
   The list now keeps its own height and the call to action takes the slack
   instead, so features start at the same height on every card and the buttons
   still share a baseline. */
.plan ul { list-style: none; margin: 0 0 var(--space-6); padding: 0; flex: 0 0 auto; }
.plan .btn-block { margin-top: auto; }
.plan li {
  padding: 6px 0; color: var(--text-secondary); font-size: 0.8125rem;
  display: flex; gap: 8px; align-items: baseline;
}
.plan li svg { color: var(--text-tertiary); flex: none; margin-top: 2px; width: 14px; height: 14px; }
.plan.featured li svg { color: var(--accent); }


/* ---- accordion ---------------------------------------------------------
   Moved here from the landing page when that page was rebuilt. The class
   names still say `l-` because /pricing's own markup (pricing.html) uses
   them, and renaming a selector the pricing page depends on is a change to
   a page outside the scope of the landing rebuild. The rules are unchanged,
   so /pricing renders exactly as it did. */
/* -------------------------------------------------------------------- FAQ */
.l-faq-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 24px; align-items: start;
}
.l-faq-grid details {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.l-faq-grid summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 16px;
  font-size: 0.875rem; font-weight: 500; color: var(--text-secondary);
  cursor: pointer; list-style: none;
}
.l-faq-grid summary::-webkit-details-marker { display: none; }
.l-faq-grid summary:hover { color: var(--text); }
.l-faq-icon { flex: 0 0 auto; color: var(--text-tertiary); transition: transform var(--speed) var(--ease); }
.l-faq-grid details[open] .l-faq-icon { transform: rotate(45deg); }
.l-faq-body {
  padding: 0 16px 15px;
  font-size: 0.8125rem; line-height: 1.6; color: var(--text-tertiary);
}
/* ============================================================
   PRICING PAGE (§5.3)
   The cards above are shared with the landing page through
   ui.plan_card. Everything below is this page alone: the comparison
   table, the "which plan" cards and the billing FAQ.

   The table is built from the same `limits` and `entitlements` the
   billing system charges against, so a row cannot claim something
   the catalogue does not sell.
   ============================================================ */

.pricing-page { max-width: var(--maxw); margin: 0 auto; padding: var(--space-16) 24px var(--space-20); }
.pricing-head { max-width: 640px; margin: 0 auto var(--space-12); text-align: center; }
.pricing-head .eyebrow { display: block; margin-bottom: var(--space-3); }
.pricing-head h1 {
  font-size: clamp(2rem, 4vw, 2.75rem);
  letter-spacing: -0.03em;
  line-height: 1.15;
  margin-bottom: var(--space-4);
}
.pricing-head .lede { font-size: 1.0625rem; line-height: 1.65; margin: 0; }
.pricing-note {
  max-width: 640px;
  margin: var(--space-8) auto 0;
  text-align: center;
  font-size: 0.875rem;
  color: var(--text-secondary);
}

/* ---------- Comparison table ---------- */

.pricing-compare { margin-top: var(--space-20); }
.pricing-compare > h2,
.pricing-choose > h2,
.pricing-faq > h2,
.pricing-cta > h2 {
  font-size: clamp(1.375rem, 2.2vw, 1.75rem);
  letter-spacing: -0.02em;
  text-align: center;
  margin-bottom: var(--space-3);
}
.pricing-compare > p { text-align: center; margin-bottom: var(--space-8); }

.pricing-table { width: 100%; border-collapse: collapse; }
.pricing-table thead th {
  vertical-align: bottom;
  text-align: left;
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-strong);
}
/* The first column is the feature name and has no header of its own; it is
   the row-header column, so it is left-aligned and quiet. */
.pricing-table thead th:first-child { width: 40%; }
.pricing-th-name { display: block; font-size: 0.9375rem; font-weight: 600; color: var(--text); }
.pricing-th-price { display: block; font-size: 1.25rem; font-weight: 600; color: var(--text); margin-top: 2px; }
.pricing-th-period { display: block; font-size: 0.75rem; font-weight: 400; color: var(--text-tertiary); }

.pricing-table tbody th[scope="row"] {
  text-align: left;
  font-weight: 500;
  font-size: 0.875rem;
  color: var(--text-secondary);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.pricing-table tbody td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  font-size: 0.875rem;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* The recommended plan's column carries a tint down its whole height, so the
   eye lands on one plan rather than scanning three equal ones. */
.pricing-table .is-popular { background: var(--accent-muted); }
.pricing-table thead th.is-popular { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }

.pricing-group th {
  text-align: left;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
  padding: var(--space-5) var(--space-4) var(--space-2);
  border-bottom: 1px solid var(--border);
}
.pricing-yes { color: var(--success-text); display: inline-flex; }
.pricing-no { color: var(--text-tertiary); opacity: 0.5; }
.pricing-unlimited { color: var(--text-secondary); font-size: 0.8125rem; }

.pricing-table tfoot td { border-bottom: none; padding-top: var(--space-5); }
.pricing-table tfoot .btn { white-space: nowrap; }

/* ---------- Which plan ---------- */

.pricing-choose { margin-top: var(--space-20); }
.pricing-choose-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-8);
  align-items: start;
}
.pricing-choose-grid .card { position: relative; }
.pricing-choose-grid .card p { font-size: 0.875rem; color: var(--text-secondary); line-height: 1.7; }
.pricing-choose-note { font-size: 0.8125rem !important; color: var(--text-tertiary) !important; }
.pricing-choose-grid .is-recommended { border-color: var(--accent); }
.pricing-choose-flag {
  position: absolute;
  top: -11px;
  left: var(--space-5);
  padding: 2px 10px;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ---------- Billing FAQ + closing CTA ---------- */

.pricing-faq { margin-top: var(--space-20); }
.pricing-faq .l-faq-grid { margin-top: var(--space-8); }

.pricing-cta {
  margin-top: var(--space-20);
  padding: var(--space-12) var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: center;
}
.pricing-cta > p {
  max-width: 520px;
  margin: 0 auto var(--space-6);
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.7;
}

@media (max-width: 900px) {
  .pricing-choose-grid { grid-template-columns: 1fr; }
  /* The table keeps its shape on a phone but stops trying to be readable:
     feature names get the width they need and the plan columns narrow. */
  .pricing-table thead th:first-child { width: auto; }
  .pricing-table tbody th[scope="row"],
  .pricing-table tbody td,
  .pricing-table thead th,
  .pricing-table tfoot td { padding: var(--space-3) var(--space-2); font-size: 0.8125rem; }
  .pricing-th-price { font-size: 1rem; }
  .pricing-table tfoot .btn { padding: 0 var(--space-3); font-size: 0.8125rem; }
}


/* ============================================================
   CONTACT ROUTING (§4.6) — /contact public page
   ============================================================ */

.contact-group { margin-bottom: var(--space-16); }
.contact-group h2, .contact-fallback h2 {
  font-size: 1.125rem; font-weight: 600; color: var(--text);
  margin: 0 0 6px;
}
.contact-group > .muted { margin-bottom: var(--space-5); }
.contact-list { list-style: none; margin: 0; padding: 0; }
.contact-item {
  display: flex; align-items: baseline; justify-content: space-between; gap: 24px;
  padding: 13px 0; border-bottom: 1px solid var(--border);
}
.contact-item:last-child { border-bottom: none; }
.contact-email-wrap { display: inline-flex; align-items: center; gap: 10px; flex: none; }
.contact-email {
  font-family: var(--mono); font-size: 0.9375rem; font-weight: 500;
  color: var(--accent); white-space: nowrap;
}
.contact-email:hover { color: var(--accent-hover); }
.contact-desc { color: var(--text-secondary); font-size: 0.875rem; text-align: right; }
.contact-fallback {
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  background: var(--surface); padding: var(--space-6) var(--space-6) var(--space-5);
  margin-top: var(--space-12);
}
.alert a {
  color: inherit; text-decoration: underline; text-underline-offset: 2px;
}
.alert a:hover { color: var(--text); }
@media (max-width: 640px) {
  .contact-item { flex-direction: column; align-items: flex-start; gap: 4px; }
  .contact-desc { text-align: left; }
}


/* ============================================================
   AUTH (§4.3) — redesigned two-pane layout
   ============================================================ */

.auth-body { min-height: 100vh; background: var(--bg); }

/* Two-pane shell for ≥1024px */
.auth-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr;
  padding: 0;
}

@media (min-width: 1024px) {
  .auth-shell {
    grid-template-columns: 55% 45%;
  }
}

/* Left pane — brand/trust/context */
.auth-left-pane {
  background: var(--bg-subtle);
  position: relative;
  display: none;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

@media (min-width: 1024px) {
  .auth-left-pane { display: flex; }
}

/* Subtle geometric pattern — radar/sweep motif */
.auth-left-pattern {
  position: absolute;
  inset: 0;
  opacity: 0.04;
  background-image:
    radial-gradient(ellipse 80% 60% at 20% 80%, var(--accent) 0%, transparent 50%),
    linear-gradient(90deg, rgb(var(--neutral-rgb) / 0.03) 1px, transparent 1px),
    linear-gradient(rgb(var(--neutral-rgb) / 0.03) 1px, transparent 1px);
  background-size: 100% 100%, 40px 40px, 40px 40px;
  pointer-events: none;
}

.auth-left-content {
  position: relative;
  z-index: 1;
  max-width: 440px;
  padding: var(--space-16) var(--space-10) var(--space-16) var(--space-12);
}

.auth-brand-large {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--text);
  text-decoration: none;
  margin-bottom: var(--space-10);
}

.auth-brand-large svg { flex-shrink: 0; }

.auth-left-headline {
  font-size: clamp(1.5rem, 3vw, 2rem);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.025em;
  color: var(--text);
  margin: 0 0 var(--space-8);
}

.auth-left-proof {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.auth-left-proof li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-secondary);
}

.auth-left-icon {
  flex: none;
  width: 22px;
  height: 22px;
  margin-top: 2px;
  color: var(--accent);
  opacity: 0.85;
}

/* Right pane — form */
.auth-right-pane {
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-12) var(--space-6);
  min-height: 100vh;
}

@media (min-width: 1024px) {
  .auth-right-pane {
    padding: var(--space-16) var(--space-10);
    /* Centred on both axes, like .auth-left-pane. This was flex-start, which
       pinned the form to the top of its pane and hard against the divider —
       so the marketing column was optically centred at 500px while the form
       sat 184px above it, and 280px of the pane's width was empty to the
       right of a 400px card. The two halves read as unrelated panels.
       `safe` keeps a form taller than the viewport (register, password reset
       with an error) reachable instead of clipping its top; the plain value
       is declared first as the fallback for engines without it. */
    align-items: center;
    justify-content: center;
    align-items: safe center;
    justify-content: safe center;
  }
}

.auth-card-wrap {
  width: 100%;
  max-width: 400px;
}

.auth-alerts {
  margin-bottom: var(--space-6);
}

.auth-head {
  margin-bottom: var(--space-8);
}

.auth-head .eyebrow {
  display: block;
  margin-bottom: var(--space-2);
}

.auth-head h1 {
  font-size: clamp(1.75rem, 2.5vw, 2rem);
  margin-bottom: var(--space-3);
  font-weight: 600;
}

.auth-head p.muted {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  margin: 0;
}

/* Form field adjustments for auth pages */
.auth-card-wrap .field { margin-bottom: var(--space-5); }
.auth-card-wrap .field > label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.auth-card-wrap .field > label:focus-within { color: var(--text); }
.auth-card-wrap .input,
.auth-card-wrap .select,
.auth-card-wrap textarea.input {
  height: 44px;
  padding: 12px 14px;
  font-size: 0.9375rem;
}
.auth-card-wrap .input:focus + label,
.auth-card-wrap .field > label:has(+ .input:focus),
.auth-card-wrap .field:focus-within > label { color: var(--text); }

.auth-card-wrap .field-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}
.auth-card-wrap .field-header label { margin-bottom: 0; }
.auth-card-wrap .field-header .small { font-size: 0.8125rem; color: var(--text-tertiary); }
.auth-card-wrap .field-header .small:hover { color: var(--accent); }

/* Password toggle — icon based */
.input-password-wrap { position: relative; }
.input-password-wrap .input { padding-right: 52px; }
.password-toggle {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  padding: 4px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
}
.password-toggle:hover { color: var(--text-secondary); }
.password-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.password-toggle .icon-eye { display: block; }
.password-toggle .icon-eye-slash { display: none; }
.password-toggle[aria-pressed="true"] .icon-eye { display: none; }
.password-toggle[aria-pressed="true"] .icon-eye-slash { display: block; }

/* Password strength meter — redesigned */
.strength {
  display: flex;
  gap: 4px;
  margin: 10px 0 8px;
}
.strength .seg {
  height: 4px;
  flex: 1;
  background: var(--border-strong);
  border-radius: 2px;
  transition: background var(--speed) var(--ease);
}
.strength .seg[data-tier="1"].filled { background: var(--danger); }
.strength .seg[data-tier="2"].filled { background: var(--warning); }
.strength .seg[data-tier="3"].filled { background: var(--strength-good); } /* lime-500 — intermediate */
.strength .seg[data-tier="4"].filled { background: var(--success); }

.strength-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 500;
  text-align: right;
  min-height: 18px;
  transition: color var(--speed) var(--ease);
}
/* The strength label is TEXT, so it uses the text-safe tokens, not the fill
   tokens. `--success` and `--warning` were re-pointed for the light theme to
   stay legible as a *fill* and a *border* (progress segments, status dots,
   chart series), and this one rule kept using them as text: measured on the
   light theme, `--warning` on a white card is 2.94:1 and `--success` is
   3.30:1, both under the 4.5:1 floor for body text. `--success-text` and
   `--warning-text` are the same hues taken dark enough to pass, at 5.35:1 on
   white and 5.03:1 on the page background. The bar beside this label is
   `aria-hidden`, so the label is the only signal a screen reader gets. */
.strength-label[data-level="1"] { color: var(--danger); }
.strength-label[data-level="2"] { color: var(--warning-text); }
.strength-label[data-level="3"] { color: var(--strength-good); }
.strength-label[data-level="4"] { color: var(--success-text); }
.strength-label:empty { min-height: 0; }

/* Custom checkbox — 18px with custom checkmark */
.check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-size: 0.875rem;
  color: var(--text-secondary);
  cursor: pointer;
}
.check input {
  appearance: none;
  margin-top: 2px;
  width: 18px;
  height: 18px;
  flex: none;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  background: var(--bg);
  position: relative;
  cursor: pointer;
  transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}
.check input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.check input:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.check input:checked::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 2px;
  width: 5px;
  height: 9px;
  border: solid var(--accent-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.check input:hover:not(:checked) { border-color: var(--border-strong); }
.check span { line-height: 1.5; }
.check a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.check a:hover { color: var(--accent-hover); }

.check.is-error input {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-muted);
}
.check.is-error span { color: var(--danger-text); }

/* Report sharing (/assessments/<id>/share). The create form is a three-up grid
   that stacks, and the revoke control is an inline form so the button keeps the
   table row's appearance without adding a cell of its own — the same
   display:contents trick the settings rail uses. */
.two-col-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
  align-items: start;
}
.inline-form { display: contents; }
@media (max-width: 560px) {
  .two-col-fields { grid-template-columns: minmax(0, 1fr); }
}

/* Team conversation (/teams/chat). A fixed-height log that scrolls, over a
   composer that does not scroll away with it. */
.chat-wrap { display: flex; flex-direction: column; max-height: 68vh; }
.chat-log {
  list-style: none;
  margin: 0;
  padding: var(--space-4);
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.chat-msg { padding: var(--space-3) 0; border-bottom: 1px solid var(--border); }
.chat-msg:last-child { border-bottom: 0; }
.chat-msg.is-mine .chat-meta b { color: var(--accent); }
.chat-meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: 2px;
}
.chat-body {
  margin: 0;
  white-space: pre-wrap;   /* the server preserved the line breaks it was sent */
  overflow-wrap: anywhere;  /* a long URL must not widen the page */
}
.chat-body.is-muted { color: var(--text-tertiary); font-style: italic; }
.chat-compose {
  display: flex;
  gap: var(--space-3);
  align-items: flex-end;
  padding: var(--space-4);
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.chat-compose .input { flex: 1 1 auto; resize: vertical; }
.chat-status { padding: 0 var(--space-4) var(--space-3); min-height: 1.25rem; }
.rail-list { list-style: none; margin: 0; padding: 0; }
.rail-list li { padding: 3px 0; }

/* Shared report (/r/<token>) -- a read-only public rendering of a report. */
.shared-report { max-width: 900px; margin: 0 auto; }
.shared-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.shared-report .card { margin-bottom: var(--space-4); }
.shared-findings { margin: 0; padding: 0; list-style: none; }
.shared-findings li {
  padding: var(--space-4) 0;
  border-top: 1px solid var(--border);
}
.shared-findings li:first-child { border-top: 0; padding-top: 0; }
.shared-findings p { margin: var(--space-2) 0 0; }

/* Score trend (/domains/<id>/trend). The figure is inline SVG so it survives
   into the PDF; these are the few styles it needs, all from the same tokens as
   the rest of the sheet. */
.trend-figure { margin: 0; }
.trend-figure svg { display: block; overflow: visible; }
.trend-grid line { stroke: var(--border); stroke-width: 1; }
.trend-grid text { fill: var(--text-tertiary); font-size: 10px; }
.trend-area { fill: var(--accent); opacity: 0.10; }
.trend-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.trend-dot { fill: var(--accent); stroke: var(--surface); stroke-width: 2; }
.table tr.is-muted td { opacity: 0.62; }


   channel checkboxes per event. The checkboxes are the same `.check input`
   control used by the auth forms; only the layout is new, so there is no
   second checkbox appearance to drift. */
/* Notification preferences (/settings/notifications) — a description and two
   channel checkboxes per event. The checkboxes are the same `.check input`
   control used by the auth forms; only the layout is new, so there is no
   second checkbox appearance to drift. */
.pref-head,
.pref-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 84px 84px;
  gap: var(--space-4);
  align-items: start;
}
.pref-head {
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.pref-col { text-align: center; }
.pref-row {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border);
}
.pref-row:last-of-type { border-bottom: 0; }
.pref-label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  cursor: pointer;
  min-width: 0;
}
.pref-label b { font-size: 0.9375rem; font-weight: 600; color: var(--text); }
.pref-default { align-self: flex-start; margin-top: 2px; }
.pref-cell {
  display: flex;
  justify-content: center;
  padding-top: 2px;
}
.pref-cell input {
  appearance: none;
  width: 18px;
  height: 18px;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  background: var(--bg);
  position: relative;
  cursor: pointer;
  transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}
.pref-cell input:hover:not(:checked) { border-color: var(--border-strong); }
.pref-cell input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pref-cell input:checked { background: var(--accent); border-color: var(--accent); }
.pref-cell input:checked::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 2px;
  width: 5px;
  height: 9px;
  border: solid var(--accent-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

@media (max-width: 640px) {
  /* Below this width the two 84px columns squeeze the description into a
     two-word-per-line ribbon, so the channel names move above their boxes
     instead of beside them. */
  .pref-head { display: none; }
  .pref-row {
    grid-template-columns: minmax(0, 1fr) auto auto;
    gap: var(--space-3);
  }
  .pref-cell { flex-direction: column; align-items: center; gap: 4px; }
  .pref-cell::before {
    content: attr(data-channel);
    font-size: 0.6875rem;
    color: var(--text-tertiary);
  }
}

/* Primary button — auth context */
.auth-card-wrap .btn-primary.btn-lg {
  height: 46px;
  font-size: 0.9375rem;
  font-weight: 600;
}
.auth-card-wrap .btn-loading { display: inline-flex; align-items: center; gap: 8px; }
.auth-card-wrap .btn-loading .spinner { width: 16px; height: 16px; border-width: 2px; }

.auth-alt {
  text-align: center;
  margin-top: var(--space-5);
  font-size: 0.875rem;
  color: var(--text-tertiary);
}
.auth-alt a { color: var(--accent); font-weight: 500; }
.auth-alt a:hover { color: var(--accent-hover); }

/* Confirmation that a typed address survived the hand-off. Reassurance, not
   instruction, so it is quieter than the link above it and monospaced for the
   value — a domain is something to check, not something to read. */
.auth-alt-note {
  text-align: center;
  margin: var(--space-3) 0 0;
  font-size: 0.8125rem;
  color: var(--text-tertiary);
}
.auth-alt-note strong { font-family: var(--mono); font-weight: 500; color: var(--text-secondary); }

/* Footer wrap — distinct floor */
.auth-foot-wrap {
  margin-top: var(--space-8);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}
.auth-foot {
  text-align: center;
  font-size: 0.75rem;
  color: var(--text-tertiary);
  margin: 0;
}
.auth-foot a { color: var(--text-secondary); text-decoration: none; }
.auth-foot a:hover { color: var(--text); text-decoration: underline; }

/* OTP inputs — larger on desktop */
.otp-row {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin: var(--space-6) 0 var(--space-3);
}
.otp-input {
  width: 52px;
  height: 60px;
  text-align: center;
  font-size: 1.5rem;
  font-family: var(--mono);
  font-weight: 600;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
}
.otp-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--shadow-focus); }
.otp-input.is-error { border-color: var(--danger); }

/* Alert with icon */
.alert-icon {
  flex: none;
  color: currentColor;
  margin-top: 2px;
}

/* Responsive: <1024px left pane hidden, right pane centered */
@media (max-width: 1023px) {
  .auth-left-pane { display: none; }
  .auth-right-pane {
    align-items: center;
    justify-content: center;
    padding: var(--space-10) var(--space-5);
  }
}

@media (max-width: 767px) {
  .auth-right-pane { padding: var(--space-6) var(--space-4); }
  .auth-left-content { padding: var(--space-10) var(--space-6); }
  .auth-brand-large { font-size: 1.1rem; gap: 10px; margin-bottom: var(--space-8); }
  .auth-left-headline { font-size: 1.375rem; }
  .auth-left-proof li { font-size: 0.875rem; gap: var(--space-3); }
  .auth-left-icon { width: 20px; height: 20px; }
  .auth-head h1 { font-size: 1.5rem; }
  .auth-head p.muted { font-size: 0.875rem; }
  .auth-card-wrap .input,
  .auth-card-wrap .select,
  .auth-card-wrap textarea.input { height: 42px; }
  .otp-input { width: 44px; height: 52px; font-size: 1.25rem; }
  .check input { width: 17px; height: 17px; }
  .auth-card-wrap .btn-primary.btn-lg { height: 44px; }
}

/* ============================================================
   APP SHELL (§3.6)
   ============================================================ */

.app-shell { display: flex; flex-direction: column; min-height: 100vh; }

.sidebar {
  background: var(--bg); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
}
.sidebar-brand {
  display: flex; align-items: center; gap: 9px;
  padding: 18px 20px; border-bottom: 1px solid var(--border);
}

.side-nav { padding: var(--space-3) 10px; display: flex; flex-direction: column; gap: 1px; flex: 1; overflow-y: auto; }
.side-nav .nav-group {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-tertiary); padding: var(--space-4) 10px 6px; font-weight: 500;
}
.side-nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--radius-sm);
  color: var(--text-tertiary); font-size: 0.8125rem; font-weight: 500;
}
.side-nav a svg { flex: none; opacity: 0.6; }
.side-nav a:hover { background: rgb(var(--neutral-rgb) / 0.04); color: var(--text-secondary); }
.side-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.side-nav a.active { background: var(--accent-muted); color: var(--accent); }
.side-nav a.active svg { opacity: 1; }

.sidebar-foot {
  padding: 14px 16px; border-top: 1px solid var(--border);
  font-size: 0.75rem; color: var(--text-tertiary);
}
.sidebar-foot .credit-num { font-family: var(--mono); color: var(--accent); font-size: 0.875rem; }

.topbar {
  position: sticky; top: 0; z-index: var(--z-sticky);
  height: 56px; display: flex; align-items: center; gap: 14px;
  padding: 0 24px; background: rgba(9,9,11,0.85); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
.topbar .page-title { font-size: 0.875rem; font-weight: 600; color: var(--text); }
.topbar .spacer { flex: 1; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.topbar .user-chip { display: flex; align-items: center; gap: 8px; font-size: 0.8125rem; color: var(--text-secondary); }

/* Zones shell: top bar (new global nav) */
.app-topbar { background: var(--surface); backdrop-filter: none; gap: 16px; }
.app-brand { min-width: 40px; min-height: 40px; padding: 0 4px; justify-content: center; }
.app-tabs {
  display: flex; align-items: center; gap: 4px;
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}
.app-tabs::-webkit-scrollbar { display: none; width: 0; height: 0; }
.app-tabs a {
  display: inline-flex; align-items: center; justify-content: center;
  height: 40px; padding: 0 12px; min-width: 40px; white-space: nowrap;
  border-radius: var(--radius-sm);
  color: var(--text-tertiary); font-size: 0.8125rem; font-weight: 500;
}
.app-tabs a:hover { color: var(--text); background: rgb(var(--neutral-rgb) / 0.04); }
.app-tabs a.active { background: var(--accent-muted); color: var(--accent); }
.app-tabs a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.app-topbar .icon-btn { width: 40px; height: 40px; }
.avatar-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: var(--radius-sm); color: var(--text);
}
.avatar-link:hover { background: rgb(var(--neutral-rgb) / 0.04); }

.avatar {
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--accent-muted); border: 1px solid var(--border-strong);
  display: grid; place-items: center;
  color: var(--accent); font-weight: 600; font-size: 0.75rem;
  /* A fixed-size circle cannot hold a name. `place-items: center` lets the
     text item size to its content and spill out of the box, so the initials
     are clipped here as a backstop even though the markup now passes at most
     two characters. */
  overflow: hidden;
  flex: 0 0 auto;
  line-height: 1;
  text-transform: uppercase;
}
.avatar.sm { width: 24px; height: 24px; font-size: 0.625rem; }

.icon-btn {
  background: none; border: none; color: var(--text-tertiary); cursor: pointer;
  padding: 6px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center; position: relative;
}
.icon-btn:hover { color: var(--text); background: rgb(var(--neutral-rgb) / 0.04); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The unread count on the bell.
   This was a bare 6px dot sized for an empty state, and app.js now puts a
   number in it: the dot carried no count, so "one new message" and "nine new
   messages" looked identical, and a customer deciding whether to open the
   notifications had nothing to go on. Sized to hold two digits, and the text is
   --danger-ink on --danger-solid because that pairing is the one measured to
   clear 4.5:1 (white on --danger is 3.76:1). */
.icon-btn .unread-dot {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--danger-solid);
  color: var(--danger-ink);
  font-size: 0.625rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  /* A badge must not sit on top of the bell glyph and make it unreadable. */
  box-shadow: 0 0 0 2px var(--surface);
}

.chip-name { color: var(--text-secondary); font-size: 0.8125rem; }

.app-content {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  flex: 1 0 auto;
  padding: 28px 28px 60px;
}
.app-main { min-width: 0; }
.app-rail { display: none; }
.app-content.has-rail .app-rail { display: flex; }

/* The rail is a sidebar only above the drawer breakpoint (1024px). It used to
   start at 980px while the drawer rules above ran to `max-width: 1024px`, so
   between the two both matched and the drawer rules won: the rail was pushed
   off-canvas and made `visibility: hidden` on every has-rail page in that band,
   with no control able to bring it back. One breakpoint, no overlap. */
@media (min-width: 1025px) {
  .app-content.has-rail {
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 0;
    padding: 0;
  }
  .app-content.has-rail .app-main { padding: 28px 28px 60px 28px; }
  .app-content.has-rail .app-rail {
    position: sticky; top: 56px; align-self: start;
    max-height: calc(100vh - 56px); overflow-y: auto;
    flex-direction: column; gap: var(--space-5);
    background: var(--surface); border-left: 1px solid var(--border);
    padding: var(--space-6) var(--space-5);
  }
}

/* Right rail blocks: stacked with hairline separators on desktop */
.rail-block { min-width: 0; overflow-wrap: anywhere; }
.rail-block + .rail-block { border-top: 1px solid var(--border); padding-top: var(--space-5); }
.rail-label {
  display: block;
  font-size: 0.6875rem; color: var(--text-tertiary);
  text-transform: uppercase; letter-spacing: 0.06em;
  margin-bottom: 8px; font-weight: 500;
}
.rail-title { font-size: 0.9375rem; font-weight: 600; color: var(--text); margin-bottom: 10px; }
.rail-note { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 6px; line-height: 1.5; }
.rail-stat {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-bottom: 8px; font-size: 0.8125rem; color: var(--text-secondary);
}
.rail-stat .r-value { font-family: var(--mono); color: var(--text); flex: none; }
.rail-block .r-value { font-family: var(--mono); color: var(--text); margin-bottom: 6px; }
.rail-block .m-value { font-size: 1.5rem; }
.rail-links { display: flex; flex-direction: column; }
.rail-link {
  display: flex; align-items: center; min-height: 40px; padding: 8px 0;
  color: var(--text-secondary); font-size: 0.875rem; font-weight: 500;
  border-radius: var(--radius-sm); border-bottom: 1px solid var(--border);
}
.rail-link:last-child { border-bottom: none; }
.rail-link:hover { color: var(--accent); }
/* Sign-out is a <button> in a POST form (see settings.html) so it carries a
   CSRF token, but it has to occupy the same row as the links above it.
   display:contents takes the form out of layout, and the button is reset to
   inherit the link's metrics and colours. */
.rail-form { display: contents; }
.rail-link-btn {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.rail-link-btn:last-child { border-bottom: none; }
.rail-link-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.app-rail .btn { min-height: 40px; min-width: 40px; }
.rail-block .badge { white-space: normal; overflow-wrap: anywhere; }
.rail-block.rail-tinted {
  background: var(--accent-muted);
  border: 1px solid rgb(var(--accent-rgb) / 0.18);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

/* Dashboard: horizontally-scrolling strip of compact domain chips */
/* Was `overflow-x: auto` with the scrollbar hidden and no affordance, so a
   fourth domain was sliced in half at the card edge and there was no way to
   tell the user it could scroll — the information was simply invisible. The
   strip shows at most four chips, so wrapping shows all of them; `d-domain`
   gets a max-width and an ellipsis so one very long host cannot blow the row
   out. Both live in the responsive block further down as well. */
.domain-strip {
  display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap;
}
.domain-strip .domain-chip { flex: 0 1 auto; min-width: 0; max-width: 100%; }
/* .d-domain is styled once, further down, with the ellipsis guard. */

/* Mid-size: rail below main, its blocks wrap into a horizontal row */
@media (min-width: 640px) and (max-width: 979px) {
  .app-content.has-rail .app-rail {
    flex-direction: row; flex-wrap: wrap;
    gap: var(--space-4);
    border-top: 1px solid var(--border); padding-top: var(--space-5);
  }
  .app-content.has-rail .rail-block {
    flex: 1 1 240px;
    border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--space-4); background: var(--surface);
  }
  .app-content.has-rail .rail-block + .rail-block { padding-top: var(--space-4); }
}

/* Narrow: rail blocks stack one per row, full width */
@media (max-width: 639px) {
  .app-content.has-rail .app-rail {
    flex-direction: column; gap: var(--space-4);
    border-top: 1px solid var(--border); padding-top: var(--space-5);
  }
  .app-content.has-rail .rail-block {
    width: 100%;
    border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--space-4); background: var(--surface);
  }
  .app-content.has-rail .rail-block + .rail-block { padding-top: var(--space-4); }
}

.page-header {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-6); flex-wrap: wrap;
}
.page-header h1 { font-size: 2rem; margin-bottom: 2px; }
.page-header .sub { color: var(--text-tertiary); font-size: 0.8125rem; margin-top: 4px; }

/* Metrics */

.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); margin-bottom: var(--space-6); }
.metric {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: var(--space-4) 18px;
}
.metric .m-label {
  font-size: 0.6875rem; color: var(--text-tertiary); text-transform: uppercase;
  letter-spacing: 0.06em; margin-bottom: 6px; font-weight: 500;
}
.metric .m-value { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; }
.metric .m-value.mono { font-family: var(--mono); font-size: 1.25rem; }
.metric .m-note { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 4px; }

.two-col { display: grid; grid-template-columns: 1.6fr 1fr; gap: var(--space-4); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.stack { display: flex; flex-direction: column; gap: var(--space-4); }

/* Assessment progress */

.phase-list {
  display: flex; flex-direction: column; gap: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.phase-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--border);
}
.phase-row:last-child { border-bottom: none; }
.phase-row .phase-ico {
  width: 30px; height: 30px; border-radius: var(--radius-sm); display: grid; place-items: center;
  background: rgb(var(--neutral-rgb) / 0.04); color: var(--text-tertiary); flex: none;
}
.phase-row.done .phase-ico { background: var(--success-muted); color: var(--success-text); }
.phase-row.current .phase-ico { background: var(--accent-muted); color: var(--accent); }
.phase-row.failed .phase-ico { background: var(--danger-muted); color: var(--danger-text); }
.phase-row .phase-meta { flex: 1; min-width: 0; }
.phase-row .phase-name { font-weight: 500; font-size: 0.875rem; color: var(--text); }
.phase-row .phase-desc { font-size: 0.75rem; color: var(--text-tertiary); }
.phase-row .phase-time { font-family: var(--mono); font-size: 0.75rem; color: var(--text-tertiary); }

.assessment-hero {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-8); text-align: center;
}
.assessment-hero .a-domain { font-family: var(--mono); font-size: 1.5rem; letter-spacing: -0.01em; color: var(--text); }
.assessment-hero .a-sub { color: var(--text-secondary); font-size: 0.875rem; margin-top: 6px; }
.assessment-hero .a-progress-wrap { max-width: 420px; margin: var(--space-5) auto 0; }
.assessment-hero .a-pct { font-family: var(--mono); font-size: 0.75rem; color: var(--accent); margin-top: 8px; }

.check-result { width: 18px; height: 18px; flex: none; }

/* Terminal states (§3.9) */

.completed, .failed, .cancelled {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-8);
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-3); margin-bottom: var(--space-6);
}
.completed h1, .failed h1, .cancelled h1 { font-size: 1.5rem; }
.completed p, .failed p, .cancelled p { max-width: 460px; margin: 0; }
.error-details {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: var(--space-4);
  font-family: var(--mono); font-size: 0.8125rem; color: var(--text-secondary);
  text-align: left; overflow-wrap: anywhere; width: 100%; max-width: 460px;
  display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-2);
}
.error-details strong { color: var(--text); font-family: var(--font); font-weight: 600; }
.process-stages {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: var(--space-4) var(--space-5);
  color: var(--text-secondary); font-size: 0.875rem;
}

/* Notifications (§5 D14) */

.notif-list { display: flex; flex-direction: column; gap: 0; }
.notif-list .notif-item:last-child { border-bottom: none; }
.notif-item {
  display: flex; gap: var(--space-4); align-items: flex-start;
  padding: var(--space-4); border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
}
.notif-item:hover { background: rgb(var(--neutral-rgb) / 0.02); }
.notif-item.unread { border-left-color: var(--accent); }
.notif-item .flex-1 { flex: 1; min-width: 0; }
.notif-item b { color: var(--text); font-weight: 600; font-size: 0.875rem; }
.notif-item p { font-size: 0.8125rem; color: var(--text-tertiary); margin: 2px 0 0; }
.notif-badge { flex-shrink: 0; padding-top: 2px; }
.notif-link { text-decoration: none; color: inherit; flex: 1; min-width: 0; }
.notif-read-btn {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 36px; height: 36px; margin-top: 4px; border: none; border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-tertiary); cursor: pointer;
  padding: 0; transition: color var(--speed), background var(--speed);
}
.notif-read-btn:hover { color: var(--text); }
.notif-item .dots {
  background: none; border: none; color: var(--text-tertiary); cursor: pointer;
  font-size: 1rem; padding: 4px; border-radius: var(--radius-sm);
}
.notif-item .dots:hover { color: var(--text); }

/* Domain chips */

.domain-chip {
  display: flex; align-items: center; gap: 10px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 10px 14px;
}
/* `min-width: 0` + ellipsis is what lets a long host shrink instead of forcing
   the wrapping strip above to overflow its card again. */
.domain-chip .d-domain {
  font-family: var(--mono); font-size: 0.8125rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}

/* Billing / usage (§5 D9/D10) */

.plan-badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: var(--space-2); }
.plan-badge .muted { font-size: 0.8125rem; }

.usage-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5) 48px; margin-top: var(--space-5); }
.usage-item label { font-size: 0.8125rem; font-weight: 500; color: var(--text-secondary); display: block; margin-bottom: 8px; }
.usage-bar { height: 6px; background: rgb(var(--neutral-rgb) / 0.06); border-radius: var(--radius-full); overflow: hidden; }
.usage-fill { height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width var(--speed-slow) var(--ease); }
.usage-text { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 8px; display: block; font-family: var(--mono); }

.checkout-details { margin: 0; }
.checkout-details > div {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4);
  padding: var(--space-3) 0; border-bottom: 1px solid var(--border);
}
.checkout-details > div:first-child { padding-top: 0; }
.checkout-details > div:last-child { border-bottom: none; padding-bottom: 0; }
.checkout-details dt { color: var(--text-secondary); font-size: 0.875rem; }
.checkout-details dd { color: var(--text); font-size: 0.875rem; margin: 0; }
.checkout-details dd.mono { font-family: var(--mono); }

/* Admin */

.stat-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }

/* Utility */

/* Every spacing utility applies a step of the --space scale, and a utility
   named after a step applies that step. Two blocks used to define these with
   raw pixels at 20px for the "4" rungs and tokens at 16px, and the token copy
   won on source order — so `.mt-4` resolved to two different values depending
   on which block a reader happened to look at first.

   .mx-auto is longhand on purpose. Written as `margin: 0 auto` it also zeroes
   the top and bottom, and the templates combine it with vertical utilities
   (`card mt-6 maxw-720 mx-auto`, `maxw-600 mx-auto mb-12`), so which margin
   survived came down to which line of the stylesheet came later. */
.mt-0 { margin-top: 0; } .mt-1 { margin-top: var(--space-1); } .mt-2 { margin-top: var(--space-2); } .mt-3 { margin-top: var(--space-3); } .mt-4 { margin-top: var(--space-4); }
.mb-1 { margin-bottom: var(--space-1); } .mb-2 { margin-bottom: var(--space-2); } .mb-3 { margin-bottom: var(--space-3); } .mb-4 { margin-bottom: var(--space-4); } .mb-5 { margin-bottom: var(--space-5); } .mb-12 { margin-bottom: var(--space-12); }
.mx-auto { margin-left: auto; margin-right: auto; }
.p-0 { padding: 0; }
.flex { display: flex; } .items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: var(--space-2); } .gap-3 { gap: var(--space-3); }
/* min-width: 0 on both: a flex item's default min-width is auto, which lets a
   long unbroken string (a DNS value, a share link) push the row wider than its
   container instead of shrinking. */
.flex-1 { flex: 1; min-width: 0; }
.grow { flex: 1; min-width: 0; }
.field-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 7px; }
.field-header label { margin-bottom: 0; }
.hidden { display: none !important; }

/* Back to top.

   The old rule had BOTH `opacity: 0; pointer-events: none` and
   `display: none`, and no media query ever restored the display — so
   `.back-to-top.show { opacity: 1 }` could never take effect and the button
   was permanently invisible while still being a focusable element in the tab
   order. One mechanism now: the button is always laid out and positioned off
   the bottom edge, and `.show` slides it in. That keeps the `show` class the
   single source of truth and leaves nothing a media query has to remember. */
.back-to-top {
  position: fixed; right: 20px; bottom: 20px; z-index: var(--z-sticky);
  width: 40px; height: 40px; border-radius: var(--radius-sm); border: 1px solid var(--border-strong);
  background: var(--surface-raised); color: var(--text-tertiary);
  cursor: pointer; opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity var(--speed) var(--ease), transform var(--speed) var(--ease),
              color var(--speed) var(--ease), border-color var(--speed) var(--ease);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-md);
}
.back-to-top:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }
.back-to-top.show { opacity: 1; pointer-events: auto; transform: none; }

/* ============================================================
   STATUTORY / PAGE-LEVEL
   ============================================================ */

/* Legal prose (terms, privacy) */
.legal h1 { font-size: 2rem; margin-bottom: var(--space-2); }
.legal .updated { color: var(--text-tertiary); font-size: 0.8125rem; margin-bottom: var(--space-8); }
/* The revision date, from the page registry rather than typed into the
   template. Struck through nothing and boxed nothing: it is one quiet line
   under the title, because it is metadata and it should read as metadata. */
.legal .legal-updated {
  color: var(--text-tertiary);
  font-size: 0.8125rem;
  margin: calc(var(--space-2) * -1) 0 var(--space-8);
}
.legal h2 { font-size: 1.125rem; margin: var(--space-8) 0 var(--space-3); }
.legal h3 { font-size: 1rem; margin: var(--space-6) 0 var(--space-2); }
.legal p, .legal li { color: var(--text-secondary); font-size: 0.875rem; line-height: 1.7; }
.legal ul, .legal ol { padding-left: 20px; }
.legal li { margin-bottom: 6px; }

/* The scan console's styles (scan-page / scan-card / steps / target-dot and
   friends) used to live here. No template in this product renders them —
   scan.html is a standalone document that loads tokens.css + scan.css only, so
   none of this was ever applied to the one page named after it. It was also the
   sheet's largest concentration of raw colour literals (var(--accent), var(--accent-deep) and 18
   hand-written rgba() tints), all of which pinned the dark palette into rules no
   light theme could reach. Removed rather than re-themed: re-pointing rules that
   nothing renders is the same as carrying them. */

/* ============================================================
   RESPONSIVE (§6.1)
   ============================================================ */

@media (max-width: 1024px) {
  .metric-grid, .stat-tiles { grid-template-columns: repeat(2, 1fr); }
  .pricing-grid { grid-template-columns: 1fr; }
  .two-col, .grid-2 { grid-template-columns: 1fr; }
  /* Five footer nav columns: three is the widest that still fits without a
     label wrapping to three lines. See .footer-grid. */
  .footer-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .split-section { grid-template-columns: 1fr; gap: 40px; }
  .hero-inner { grid-template-columns: 1fr; gap: 40px; }
  .hero-visual { display: none; }
  .plan.featured { transform: none; padding: var(--space-8) 26px 28px; }
  .plan.featured:hover { transform: none; }
}

/* ---------- Public nav: two steps ----------
   The desktop link row is ~850px wide with six entries. Rather than let it
   overflow, it tightens once and then collapses into the drawer. The 900px
   step matches the `window.innerWidth > 900` check in app.js that closes the
   drawer on resize, so the two never disagree about which layout is live. */
@media (max-width: 1000px) {
  .nav-links { gap: 13px; font-size: 0.78125rem; }
  .site-nav-inner { gap: 16px; }
}
@media (max-width: 900px) {
  .nav-links { display: none; }
  .nav-burger { display: inline-flex; align-items: center; justify-content: center; }
}

/* 320px: brand + "Sign in" + "Get started" + burger overflow the nav row by
   ~13px, which put a horizontal scrollbar on every public page. Drop the
   secondary link — it is still the first entry in the drawer. */
@media (max-width: 340px) {
  .nav-signin { display: none; }
}

@media (max-width: 768px) {
  .app-shell { display: block; }
  #sidebar-toggle { display: inline-flex !important; align-items: center; justify-content: center; }
  .app-shell.sidebar-open .sidebar {
    display: flex; position: fixed; z-index: var(--z-mobile-nav); width: 240px;
    box-shadow: 0 16px 40px rgba(0,0,0,0.5);
  }
  .app-shell.sidebar-open .sidebar-foot { display: none; }
  .site-nav-inner { height: 56px; gap: 12px; }
  .nav-links { display: none; }
  .nav-burger { display: inline-flex; align-items: center; justify-content: center; }
  .app-content { padding: 20px 16px 48px; }
  .topbar { padding: 0 16px; }
  .domain-bar { flex-direction: column; }
  .domain-bar .btn { border-radius: var(--radius-sm); }
  .metric-grid { grid-template-columns: 1fr 1fr; }
  .footer-grid { grid-template-columns: 1fr; gap: 24px; }
  .otp-input { width: 40px; height: 48px; font-size: 1.125rem; }
  .hero { padding: 64px 20px 48px; }
  .hero h1 { font-size: 2rem; }
  .usage-grid { grid-template-columns: 1fr; }
  /* landing */
  .l-faq-grid { grid-template-columns: 1fr; }
}

@media (max-width: 520px) {
  .metric-grid { grid-template-columns: 1fr; }
  .topbar .chip-name { display: none; }
  .topbar-actions { gap: 6px; }
  .scan-page { padding: 20px 12px; }
  .scan-card { padding: 28px 20px; }
  .scan-header h1 { font-size: 24px; }
  /* landing */
}

/* ============================================================
   REDUCED MOTION (§2.6) — must reach every page
   ============================================================ */

/* Cookie notice (Section 10) */
.cookie-notice {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  max-width: 420px;
  width: calc(100vw - 32px);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  box-shadow: var(--shadow-md);
  z-index: var(--z-toast);
  animation: slideIn 0.3s var(--ease);
}
.cookie-notice-text {
  font-size: 0.875rem;
  color: var(--text-secondary);
  margin: 0 0 var(--space-3);
  line-height: 1.5;
}
.cookie-notice-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.cookie-notice-link {
  color: var(--accent);
  font-size: 0.875rem;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cookie-notice-link:hover { color: var(--accent-hover); }
@keyframes slideIn {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes slideOut {
  from { opacity: 1; transform: translateX(-50%) translateY(0); }
  to { opacity: 0; transform: translateX(-50%) translateY(10px); }
}

@media (max-width: 768px) {
  /* `bottom: 80px` used to be here, commented "above bottom bar". There is no
     bottom bar on this site, so on a phone the notice floated 80px clear of
     the bottom edge and sat in the middle of the landing page's first screen —
     measured at 360x640 it covered y=440..560, which is exactly where the
     domain field and its button are.

     It is now genuinely bottom-anchored, with the home-indicator inset, and
     trimmed: 120px of card became about 93px. Nothing else about the notice
     changes — same text, same two actions, same dismissal behaviour — it just
     stops being in the way of the one control the page exists to get used.

     Measured across every public page at 320/360/375/390px: this uncovered
     the primary control on six of them, which the old offset was covering —
     the landing page's domain field and button, /login's Sign in,
     /register's Create account, and both CTAs on /example-report. */
  .cookie-notice {
    left: 16px;
    right: 16px;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    max-width: none;
    transform: none;
    padding: 10px 12px;
    border-radius: var(--radius-sm);
  }
  .cookie-notice-text {
    font-size: 0.75rem;
    line-height: 1.45;
    margin-bottom: var(--space-2);
  }
  .cookie-notice-actions { gap: var(--space-2); }
  .cookie-notice-actions .btn { padding-inline: 10px; }

  /* …and the one control this created a collision with.

     `.back-to-top` lives in base-app.html only, fixed at bottom-right
     (`bottom: 20px`), and the notice on a phone spans nearly the whole width
     and paints over it (`--z-toast: 2000` against `--z-sticky: 90`). Under
     the old 80px offset the two did not intersect; anchored to the bottom
     they do, and once the button slides in after a scroll it is unreachable.

     Hidden rather than moved. Displacing it would mean restating the
     notice's height here, which is a second place to keep in step with a
     number that is not ours, and it would be wrong the moment the notice's
     text wrapped to a different number of lines. A convenience control that
     is not reachable until the reader has taken one action on a cookie
     notice is not a control anyone is missing: `:has()` drops this the
     instant consent.js removes the notice, and the button is back. */
  body:has(> .cookie-notice) .back-to-top { display: none; }
}

@media print {
  .cookie-notice {
    display: none !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    transition-duration: 0.001s !important;
  }
  /* ensure reveal elements are visible even if JS hasn't run yet */
  .js .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
}
/* ============================================================
   RESTORED + EXTENDED LAYER
   ------------------------------------------------------------
   Everything below this banner was lost in an accidental
   `git checkout` of this file and has been rebuilt from the class
   names the templates reference, the design tokens above, and
   tests/test_frontend_integrity.py (which fails on any class with
   no rule in any stylesheet). Kept as one appended layer rather
   than interleaved so the loss is obvious in review and easy to
   diff against a recovered copy.

   Read this before editing: if a recovered stylesheet turns up,
   diff it against this block rather than editing both.
   ============================================================ */

/* ============================================================
   THEME — light palette layered over the dark-first token set
   The dark values in :root remain the default so nothing changes for
   anyone without a preference; [data-theme="light"] only re-points the
   tokens. _theme_init.html sets the attribute before first paint from
   localStorage or prefers-color-scheme, so there is no flash.
   ============================================================ */

:root[data-theme="light"] {
  color-scheme: light;

  --bg:              #f7f8fa;
  --bg-subtle:       #eef0f4;
  --surface:         #ffffff;
  --surface-raised:  #ffffff;
  --surface-2:       #eef0f4;

  --border:          rgba(15, 23, 42, 0.10);
  --border-strong:   rgba(15, 23, 42, 0.18);

  --text:            #0f172a;
  --text-secondary:  #475569;
  /* #64748b measured 4.48:1 on --bg — just under the 4.5:1 AA floor for body
     text. Darkened to clear it with headroom. */
  --text-tertiary:   #556070;

  /* #0284c7 measured 3.85:1 on --bg and 4.10:1 as white-on-fill, so accent
     links and the primary button both failed AA in the light theme. This blue
     clears 4.5:1 against --bg, against --surface, and under white text.

     It was then darkened one more step to #1b6ea7: #1d77b5 cleared 4.5:1 on a
     plain surface but only 3.99:1 on an accent-tinted one, which is what a
     badge (.hub-badge-accent) and a chip actually sit on. The audit found
     those at 4.21-4.23:1. #1b6ea7 measures 4.79:1 on the tinted card and
     4.53:1 on the tinted page background, and improves white-on-fill from
     4.82 to 5.48:1. */
  --accent:          #1b6ea7;
  --accent-hover:    #175f92;
  --accent-muted:    rgba(27, 110, 167, 0.10);
  --accent-ink:      #ffffff;

  /* The channels the whole sheet derives its tints from. Flipping the theme means
     re-pointing these two lines and every glow, wash, hairline and focus ring in
     the product follows — which is why the light palette is 40 lines rather than
     the several hundred it would take to override each usage. */
  --accent-rgb:       27 110 167;
  --neutral-rgb:      15 23 42;
  --accent-deep:      #1e40af;
  --accent-deep-rgb:  30 64 175;

  --success:         #16a34a;
  --success-muted:   rgba(22, 163, 74, 0.10);
  /* Measured against the 10% tint it is used on, not just against --surface:
     #15803d was 4.25:1 there. #147b3b is 4.80:1 on a tinted card and 4.53:1
     on a tinted page. */
  --success-text:    #147b3b;
  --warning:         #ca8a04;
  --warning-muted:   rgba(202, 138, 4, 0.12);
  /* Same correction: #a16207 was 4.15:1 on the warning tint. */
  --warning-text:    #995d07;
  --danger:          #dc2626;
  --danger-muted:    rgba(220, 38, 38, 0.10);
  --danger-text:     #b91c1c;
  /* A filled surface that carries white text (count badges, hover fills).
     Declared here as well as in the base :root: without it, the light theme
     inherited the dark theme's value by accident rather than by decision. */
  --danger-solid:    #dc2626;
  --info:            var(--accent);
  --info-muted:      var(--accent-muted);
  --info-text:       #175f92;

  /* The strength meter's "Good" tier. The dark theme's lime #84cc16 measures
     1.9:1 on white — it is a bar fill, not text, and it simply vanishes as
     anything else. #4d7c0f clears 5.0:1 on --surface and 4.8:1 on the tinted
     row, so the bar and the label beside it both survive. */
  --strength-good:   #4d7c0f;

  /* Same tokens, light theme. --success is a MID-tone green in both themes,
     so its ink stays near-black in both: white on the light theme's #16a34a
     measured 3.30:1, and #04140a measures 5.74:1. --warning is the same
     story (white would be 2.94:1). Only the dark-theme accents are light
     enough for white to work. */
  --success-ink:      #04140a;
  --warning-ink:      #1a1200;
  --danger-ink:       #ffffff;

  /* Severity — the dark theme's pastels are unusable as text on a light
     surface (1.67-2.77:1), so the light theme uses the darker step of each
     ramp. All clear 4.5:1 on --surface. */
  --sev-critical:       #b91c1c;
  --sev-critical-bg:    rgba(185, 28, 28, 0.10);
  /* #c2410c measured 4.48:1 on its own 10% tint over --surface — 0.02 short of
     the 4.5:1 floor, which a rounding-tolerant audit passes and a strict one
     does not. #b23a09 measures 5.13:1 on the same tint. */
  --sev-high:           #b23a09;
  --sev-high-bg:        rgba(178, 58, 9, 0.10);
  /* #a16207 measured 4.32:1 on its tint. #8a5a06 measures 5.13:1. */
  --sev-medium:         #8a5a06;
  --sev-medium-bg:      rgba(138, 90, 6, 0.10);
  --sev-low:            #1d4ed8;
  --sev-low-bg:         rgba(29, 78, 216, 0.10);
  --sev-info:           #475569;
  --sev-info-bg:        rgba(71, 85, 105, 0.10);

  /* Shadows need far less weight on a light background. */
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
  --shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
  --shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.12);
}

/* Smooth theme swap, but never animate the initial paint.
   The list names the surfaces that visibly change colour, in the shells whose
   theme can actually change — the Agent Hub's own sheet transitions its sidebar
   and topbar, and it keeps the `.hub-*` namespace, so nothing from the console
   is named here. The old list had `.agent-body`, `.agent-sidebar` and
   `.agent-topbar`, none of which exist: the console's namespace is `.hub-*`, so
   those three matched nothing and the console swapped palette instantly while
   everything else faded. */
body, .card, .site-nav, .topbar, .app-topbar, .app-shell, .app-drawer,
.modal, .filter-bar, .table-wrap, .app-drawer-head,
.auth-left-pane, .legal, .alert, .badge, .input, .btn, .app-rail {
  transition: background-color var(--speed) var(--ease),
              border-color var(--speed) var(--ease),
              color var(--speed) var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  body, .card, .site-nav, .topbar, .app-topbar, .app-shell, .app-drawer,
  .modal, .filter-bar, .table-wrap, .app-drawer-head,
  .auth-left-pane, .legal, .alert, .badge, .input, .btn, .app-rail { transition: none !important; }
}

/* ============================================================
   UTILITIES (§2.3) — the small set templates compose with
   ============================================================ */

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Not a hiding utility: "present in the DOM, not shown right now". Kept
   separate from .hidden, which is display:none, because a <select> or
   <option> that must stay focusable/measurable cannot be display:none. */
.invisible { visibility: hidden; }

/* Only the utilities the block above does not already define. Everything
   shared with it used to be declared twice at the same cascade level. */
.flex-wrap { flex-wrap: wrap; }
.justify-center { justify-content: center; }
.mt-6 { margin-top: var(--space-6); }
.mb-0 { margin-bottom: 0; }
.mb-6 { margin-bottom: var(--space-6); }
/* NB: there is deliberately no `.w-80 { width: 80px }` here. `.w-80` belongs
   to the percentage scale further down (`.w-5` … `.w-100`), which is how the
   landing hero's score bar uses it. A pixel definition of the same name sat
   here and only lost because the percentage rule came later in the file — the
   hero bar silently became 80px wide the day anyone inserted a rule between the
   two. A utility whose value depends on source order is a trap, so the name is
   defined once, in the scale it belongs to. */
/* Long tokens (a DNS value, a record name, a share link) must break rather
   than widen the page. overflow-wrap alone is not enough for a run with no
   spaces, so break-word is the fallback. */
.break-all { word-break: break-all; overflow-wrap: anywhere; }

/* Prose list: a bulleted list with the browser's default indentation
   removed and the marker styled to match the rest of the UI. */
.prose-list {
  margin: 0 0 var(--space-4); padding-left: 20px;
  color: var(--text-secondary); font-size: 0.875rem;
  display: grid; gap: 8px;
}
.prose-list li::marker { color: var(--text-tertiary); }

/* Split label: "Label ......... Value" for a two-column definition row
   where the value should sit hard right. */
.split-label { display: flex; align-items: baseline; gap: var(--space-3); }
.split-label > :first-child { color: var(--text-tertiary); }
.split-label > :last-child { margin-left: auto; text-align: right; }

/* Scroll-triggered reveal. Opt-in via .stagger so a page that has no
   IntersectionObserver still shows everything. */
.stagger { transition: opacity var(--speed-lg) var(--ease), transform var(--speed-lg) var(--ease); }

/* Short inline note above a block of content. */
.note {
  color: var(--text-tertiary); font-size: 0.8125rem;
  line-height: 1.55;
}
.note + .note { margin-top: var(--space-2); }

/* ============================================================
   ACCESSIBILITY — skip link (§2.5)
   Off-screen until focused. Shared by every shell (public, auth, app,
   console) so a keyboard user can always reach #main.
   ============================================================ */

.skip-link {
  position: absolute;
  left: var(--space-3);
  top: -64px;
  z-index: 10000;
  padding: 10px 16px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  font-size: 0.875rem;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  transition: top var(--speed) var(--ease);
}
.skip-link:focus { top: var(--space-3); color: var(--accent-ink); }

/* Visible focus ring for the whole product, on top of the default. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.agent-body :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ============================================================
   BUTTON EXTENSIONS (§3.1)
   ============================================================ */

.btn-xs {
  font-size: 0.75rem; padding: 3px 8px; gap: 4px; border-radius: var(--radius-sm);
  line-height: 1.4;
}
/* Submit-button label. Swapped for a spinner by the pending-state script in
   app.js, so it must be an inline-block that keeps its metrics when the
   spinner is prepended - otherwise the button jumps on submit. */
.btn-text {
  display: inline-block;
  font: inherit;
  letter-spacing: inherit;
  vertical-align: middle;
}
.btn-warning {
  background: var(--warning-muted);
  color: var(--warning-text);
  border-color: var(--warning);
}
.btn-warning:hover { background: var(--warning); color: var(--warning-ink); }
.btn-success {
  background: var(--success-muted);
  color: var(--success-text);
  border-color: var(--success);
}
.btn-success:hover { background: var(--success); color: var(--success-ink); }
/* A control that is present but not actionable. Kept in the flow (so a row of
   buttons does not reflow) and dimmed, with aria-disabled carrying the state
   for assistive tech. */
.is-disabled,
.btn.is-disabled {
  opacity: 0.45;
  pointer-events: none;
  cursor: not-allowed;
}

/* ============================================================
   BADGE EXTENSIONS (§3.5)
   ============================================================ */

.badge-info { background: var(--accent-muted); color: var(--info-text); border-color: var(--accent); }
/* Bare status colours for the places a badge is too heavy - a table cell that
   should read as a word, not a chip. */
.success { color: var(--success-text); }
.warning { color: var(--warning-text); }
.danger   { color: var(--danger-text); }
.col-actions { text-align: right; white-space: nowrap; }

/* ============================================================
   PUBLIC NAV (§3.6) — burger state
   Two glyphs driven by one attribute swap, so the transition is CSS
   rather than a JS class toggle that can desync from the aria state.
   ============================================================ */

.nav-burger-open, .nav-burger-close { display: none; }
.nav-burger.is-open .nav-burger-open { display: block; }
.nav-burger.is-open .nav-burger-close { display: none; }
.nav-burger:not(.is-open) .nav-burger-close { display: block; }

/* ============================================================
   SITE FOOTER (§4.7)
   Two layouts from one element: a four-column grid on the landing page,
   and a single compact row everywhere else. A centred column stranded in
   an otherwise empty footer is the reason .site-footer-compact exists.
   ============================================================ */

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-subtle);
  padding: var(--space-16) 0 var(--space-8);
  margin-top: var(--space-16);
}
.site-footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}
.footer-grid {
  display: grid;
  /* Five nav columns (Product, Use Cases, Resources, Company, Legal) plus the
     brand, which takes its own full-width row above them.

     This was `1.6fr repeat(3, 1fr)` and the site had three nav columns. When
     the footer grew to five, the extra two wrapped onto an implicit second row
     and left a wide empty gap beside them — a layout bug that renders fine and
     looks wrong, and that nothing in the test suite could see because the
     column count lives in a stylesheet and the footer is generated from
     `seo.FOOTER_COLUMNS`.

     So the columns are pinned by `tests/test_seo_metadata.py`, which parses this
     rule and compares it to the registry. Brand on its own row also means the
     five nav columns get equal width, which they cannot have beside a 320px
     brand column inside a 1120px frame. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-8);
  align-items: start;
}
.footer-brand-col { grid-column: 1 / -1; max-width: 320px; }
.footer-tagline {
  margin: var(--space-4) 0 0;
  color: var(--text-tertiary);
  font-size: 0.8125rem;
  line-height: 1.6;
}
.footer-heading {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  margin: 0 0 var(--space-3);
}
.footer-grid ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.footer-grid a { color: var(--text-secondary); font-size: 0.875rem; }
.footer-grid a:hover { color: var(--text); }

.site-footer-compact { padding: var(--space-8) 0 var(--space-6); margin-top: var(--space-12); }
.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.footer-links {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.footer-links a { color: var(--text-secondary); font-size: 0.8125rem; }

.footer-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-5);
  color: var(--text-tertiary);
  font-size: 0.75rem;
}
.site-footer:not(.site-footer-compact) .footer-bottom { margin-top: var(--space-10); }

@media (max-width: 900px) {
  /* Same as the 1024 step above, and deliberately so rather than drifting to a
     second value: five items in two columns leaves an orphan. */
  .footer-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footer-brand-col { grid-column: 1 / -1; max-width: none; }
}
@media (max-width: 640px) {
  .footer-grid { grid-template-columns: 1fr; }
  .footer-bottom { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
}

/* ============================================================
   APP SHELL (§3.6) — mobile drawer
   The scrim is a sibling of the drawer rather than a ::after, so it can
   carry its own z-index and stay above page content without stacking
   above the drawer itself.

   Breakpoint contract: 1024px, shared with the `matchMedia` query in
   app.js that moves the rail into the drawer. The old sheet had a sticky
   rail from `min-width: 980px` AND a hidden off-canvas rail from
   `max-width: 1024px`; the 44px overlap meant both matched at 980-1024px
   and the later rule won, so the rail silently vanished there on every
   has-rail page. The rail is now a sidebar only ABOVE 1024px, and below
   that the drawer owns it — there is no width where it is styled both ways.
   ============================================================ */

.app-body { min-height: 100vh; }

/* One scroll lock for both open drawers. The class names are set by app.js
   (`nav-locked` for the public site, `app-nav-locked` for the app shell);
   neither was defined, so the page scrolled behind an open drawer. */
body.nav-locked, body.app-nav-locked { overflow: hidden; }

.app-nav-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: calc(var(--z-mobile-nav) - 1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--speed-md) var(--ease);
}
.app-nav-scrim.is-visible { opacity: 1; pointer-events: auto; }

.app-mobile-menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
}
.app-mobile-menu-toggle:hover { background: var(--surface-2); }
.app-menu-icon-open, .app-menu-icon-close { display: block; }
.app-mobile-menu-toggle[aria-expanded="true"] .app-menu-icon-open { display: none; }
.app-mobile-menu-toggle[aria-expanded="true"] .app-menu-icon-close { display: block; }

/* The drawer itself. `hidden` in the markup plus `.open` for the transition:
   the attribute removes it from the tab order and the a11y tree, the class
   slides it in. Both are needed — neither alone does both jobs. */
.app-drawer {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  width: min(300px, 84vw);
  z-index: var(--z-mobile-nav);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  transform: translateX(-100%);
  transition: transform var(--speed-md) var(--ease);
  overscroll-behavior: contain;
}
.app-drawer.open { transform: translateX(0); }
.app-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  height: 56px;
  flex: none;
  padding: 0 var(--space-3) 0 var(--space-5);
  border-bottom: 1px solid var(--border);
}
.app-drawer-title {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
}
.app-drawer-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
}
.app-drawer-close:hover { background: var(--surface-2); color: var(--text); }
.app-drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-4) var(--space-3) var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* Nav inside the drawer: a stacked list, not the top bar's tab strip. */
.app-drawer .app-tabs { display: flex; flex-direction: column; align-items: stretch; gap: 2px; }
.app-drawer .app-tabs a {
  height: auto;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  justify-content: flex-start;
  font-size: 0.9375rem;
  border-radius: var(--radius-sm);
}
.app-drawer .app-tabs a:hover { background: var(--surface-2); color: var(--text); }
.app-drawer .app-tabs a.active { background: var(--accent-muted); color: var(--accent); }

/* The rail, once relocated into the drawer. It is the same markup the desktop
   sidebar uses, so its blocks need re-stacking rather than a second design. */
.app-drawer-rail:empty { display: none; }
.app-drawer-rail { display: flex; flex-direction: column; gap: var(--space-5); }
.app-drawer-rail .rail-block + .rail-block { padding-top: var(--space-5); }
.app-rail.in-drawer {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: 0;
  background: none;
  border-left: 0;
  max-height: none;
  overflow: visible;
  position: static;
}
.app-rail.in-drawer .rail-label,
.app-rail.in-drawer .rail-title {
  border-top: 1px solid var(--border);
  padding-top: var(--space-5);
}
.app-rail.in-drawer > :first-child .rail-label,
.app-rail.in-drawer > :first-child .rail-title { border-top: 0; padding-top: 0; }

@media (max-width: 1024px) {
  .app-mobile-menu-toggle { display: inline-flex; }
  /* The top bar's tab strip has no room for seven labels here; the drawer's
     copy of the same macro (ui.app_nav) takes over. */
  .app-topbar .app-tabs { display: none; }
  .app-content { padding: var(--space-6) var(--space-5) var(--space-16); }
  .app-content.has-rail .app-main { padding: 0; }
}

/* ============================================================
   PLAN CARD (§5) — internals
   The card markup lives in _macros.html and is shared by the landing
   page, /pricing and the billing tier table.
   ============================================================ */

.plan-name { font-size: 1rem; margin: 0 0 var(--space-2); }
.plan-features {
  list-style: none;
  margin: var(--space-5) 0;
  padding: 0;
  display: grid;
  gap: 10px;
  font-size: 0.875rem;
  color: var(--text-secondary);
}
.plan-features li { display: flex; align-items: flex-start; gap: 10px; }
.plan-features svg { flex-shrink: 0; margin-top: 3px; color: var(--success); }

/* ============================================================
   DOMAIN VERIFICATION WIZARD (§4.8)
   Four states in one page: choose a method, add the record, wait,
   verified. The step rail is a horizontal <ol> of completed/current/
   upcoming so it reads correctly with the list semantics removed.
   ============================================================ */

.wizard-steps {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.wizard-step {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 140px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.8125rem;
  color: var(--text-tertiary);
}
.wizard-dot {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-tertiary);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.wizard-dot svg { width: 13px; height: 13px; }
.wizard-step-label { line-height: 1.3; }
.wizard-step.is-current {
  border-color: var(--accent);
  background: var(--accent-muted);
  color: var(--text);
}
.wizard-step.is-current .wizard-dot {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
}
.wizard-step.is-done { color: var(--text-secondary); }
.wizard-step.is-done .wizard-dot {
  border-color: var(--success);
  background: var(--success-muted);
  color: var(--success-text);
}

@media (max-width: 640px) {
  .wizard-step { flex: 1 1 100%; }
}

/* --- verified state --- */

.verified-panel { border-color: var(--success); background: var(--success-muted); }
.verified-head { display: flex; align-items: center; gap: var(--space-4); }
.verified-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  background: var(--success-muted);
  color: var(--success-text);
}
.verified-icon svg { width: 20px; height: 20px; }
.verified-panel .card-title { margin-bottom: 2px; }
.verified-panel p { margin: 0; }

/* --- method chooser --- */

.method-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin-top: var(--space-5);
}
@media (max-width: 768px) { .method-grid { grid-template-columns: 1fr; } }
.method {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
}
.method h3 {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.9375rem;
}
.method p { margin: 0; }
.method form { margin-top: auto; }
.method code { word-break: break-all; }
.method-panel > p:first-of-type { margin-top: 0; }

/* --- the record to publish --- */

.record-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
  margin-top: var(--space-4);
}
.record-table th,
.record-table td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.record-table th {
  width: 140px;
  color: var(--text-tertiary);
  font-weight: 500;
  white-space: nowrap;
}
.record-table tr:last-child th,
.record-table tr:last-child td { border-bottom: none; }
.record-table td { color: var(--text-secondary); }
.record-value {
  display: inline-block;
  word-break: break-all;
  margin-right: var(--space-2);
}

.code-block {
  margin: var(--space-3) 0 0;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--text-secondary);
  overflow-x: auto;
  white-space: pre;
}

/* --- provider hints --- */

.provider-list {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.provider {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
}
.provider-head { font-weight: 600; font-size: 0.875rem; }
.provider-head a { color: var(--text); }
.provider p { margin: 2px 0 0; }
.provider .code-block { margin-top: var(--space-2); }

/* --- actions + the "shown once" note --- */

.wizard-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}
.wizard-restart {
  flex: 1 1 260px;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--warning);
  background: var(--warning-muted);
  border-radius: var(--radius-sm);
  color: var(--warning-text);
}

/* --- verification activity timeline --- */

.timeline {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0 0 0 var(--space-5);
  border-left: 1px solid var(--border);
  display: grid;
  gap: var(--space-4);
}
.timeline-item {
  position: relative;
  display: grid;
  gap: 2px;
}
.timeline-item::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-5) - 3px);
  top: 7px;
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  background: var(--border-strong);
}
.timeline-kind { font-weight: 600; font-size: 0.875rem; }
.timeline-detail { color: var(--text-secondary); font-size: 0.8125rem; }
.timeline-time { font-size: 0.75rem; }

/* ============================================================
   ASSESSMENT / REPORT LISTS (§4.5)
   ============================================================ */

/* The risk column: a numeric score on a coloured chip, so a row can be
   triaged from the score alone without reading the verdict word. */
.cell-risk { white-space: nowrap; }
.risk-score {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 0.8125rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}
.risk-score.risk-critical { background: var(--sev-critical-bg); border-color: var(--sev-critical); color: var(--sev-critical); }
.risk-score.risk-high     { background: var(--sev-high-bg);     border-color: var(--sev-high);     color: var(--sev-high); }
.risk-score.risk-medium   { background: var(--sev-medium-bg);   border-color: var(--sev-medium);   color: var(--sev-medium); }
.risk-score.risk-low      { background: var(--sev-low-bg);      border-color: var(--sev-low);      color: var(--sev-low); }
.risk-score.risk-info     { background: var(--sev-info-bg);     border-color: var(--sev-info);     color: var(--sev-info); }

/* The assessment/reports table gets a hover-revealed row action column and
   tabular figures so scores line up down the column. */
.as-cards tbody tr { transition: background var(--speed) var(--ease); }
.as-cards tbody tr:hover { background: var(--surface-2); }
.as-cards td { font-variant-numeric: tabular-nums; }
.as-cards .col-actions { opacity: 0.55; transition: opacity var(--speed) var(--ease); }
.as-cards tr:hover .col-actions,
.as-cards .col-actions:focus-within { opacity: 1; }

/* A table this wide cannot be read on a phone: the subject and the status get a
   few dozen pixels each and every row becomes a horizontal scroll. The `data-label`
   each cell already carries becomes the card's field name instead, so the same
   markup serves both layouts and the labels are never a second thing to keep in
   step with the header row. */
@media (max-width: 720px) {
  .as-cards thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }
  .as-cards, .as-cards tbody, .as-cards tr, .as-cards td { display: block; width: auto; }
  .as-cards tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 10px);
    margin-bottom: var(--space-3);
    padding: var(--space-3);
  }
  .as-cards td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    border-bottom: 0;
    padding: var(--space-1) 0;
  }
  .as-cards td::before {
    content: attr(data-label);
    flex: none;
    color: var(--text-tertiary);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  /* The subject is the row: it is the link, and it gets the whole card. */
  .as-cards td[data-label="Request"] {
    display: block;
    padding-bottom: var(--space-2);
    margin-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
  }
  .as-cards td[data-label="Request"]::before { display: none; }
  .as-cards tr:hover { background: none; }
}

/* ============================================================
   BULK SELECTION (§3.4)
   The bar is a sticky strip that only takes space when something is
   selected, so an unselected table keeps its full height.
   ============================================================ */

.bulk-bar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  margin: var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent-muted);
  backdrop-filter: blur(6px);
}
.bulk-bar[hidden] { display: none; }
.bulk-count { font-size: 0.875rem; color: var(--text); }
.bulk-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.cell-select { width: 40px; text-align: center; }
.cell-select input[type="checkbox"] {
  width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer;
}
@media (max-width: 640px) {
  .bulk-bar { position: static; }
  .cell-select { width: auto; }
}

/* The confirmation step lists every domain the batch will touch, so an
   accidental "select all" is visible before anything is issued. */
.bulk-target-list {
  list-style: none;
  margin: var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  display: grid;
  gap: 6px;
  font-size: 0.8125rem;
}
.bulk-target-list code { word-break: break-all; }

/* ============================================================
   VERIFICATION ACTIVITY (§4.8, app-area instance)
   Same idea as the wizard timeline, but colour-coded by outcome: a
   failed check should be findable without reading it.
   ============================================================ */

.verify-timeline {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.verify-event {
  display: grid;
  gap: 2px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-left: 2px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  font-size: 0.8125rem;
}
.verify-event-kind {
  font-weight: 600;
  color: var(--text);
  text-transform: capitalize;
}
.verify-event-detail { color: var(--text-secondary); }
.verify-event time { font-size: 0.75rem; }
.verify-event-verified { border-left-color: var(--success); }
.verify-event-failed,
.verify-event-error { border-left-color: var(--danger); }
.verify-event-expired,
.verify-event-revoked { border-left-color: var(--warning); }
.verify-event-issued,
.verify-event-created { border-left-color: var(--accent); }

/* ============================================================
   OWNER SHARE PAGE (§4.8, public)
   A single centred card: this URL is pasted into a chat window, so it
   has to read with no context.
   ============================================================ */

.verify-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  padding: var(--space-8) 24px;
  text-align: center;
}
.verify-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--text);
}
.verify-domain {
  font-family: var(--mono);
  font-size: 0.9375rem;
  color: var(--text-secondary);
  word-break: break-all;
}

.verify-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-10) 24px;
}
.verify-card {
  width: 100%;
  max-width: 560px;
  text-align: center;
  padding: var(--space-8);
}
.verify-title {
  font-size: 1.375rem;
  margin: var(--space-3) 0 var(--space-3);
  word-break: break-all;
}
.verify-table {
  width: 100%;
  margin: var(--space-5) 0 0;
  border-collapse: collapse;
  font-size: 0.875rem;
  text-align: left;
}
.verify-table th,
.verify-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.verify-table th {
  width: 40%;
  color: var(--text-tertiary);
  font-weight: 500;
}
.verify-table td { color: var(--text-secondary); }
.verify-table tr:last-child th,
.verify-table tr:last-child td { border-bottom: none; }
.verify-foot {
  margin: var(--space-6) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  color: var(--text-tertiary);
  font-size: 0.75rem;
}

/* ============================================================
   BILLING RAIL (§4.9)
   ============================================================ */

.rail-usage { display: grid; gap: var(--space-4); }
.rail-usage .rail-stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.rail-usage .r-value {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   MONITORING TOGGLE (§4.10)
   A <button> styled as a badge, so it is reachable by keyboard and
   announces as a button. A checkbox would have needed a label and a
   hit target bigger than a chip.
   ============================================================ */

.badge-toggle {
  cursor: pointer;
  font: inherit;
  border-style: solid;
  transition: filter var(--speed) var(--ease);
}
.badge-toggle:hover { filter: brightness(1.15); }

/* ============================================================
   REPORT EXTRAS (§7)
   ============================================================ */

/* The owner-authorization line at the top of a report. A text variant so
   the two states (verified / not) can differ in weight without differing in
   box - the block is already bordered by .verify-note. */
.verify-note-text { margin: 0 0 var(--space-4); }
.verify-note-text[data-ownership-verified="1"] { border-color: var(--success); }
.verify-note-text:last-child { margin-bottom: 0; }

.simple-explanation-section { margin-top: var(--space-6); }
.simple-explanation-section h4 { margin-bottom: var(--space-3); }

/* ---------- Progress-bar width ladder (restored) ----------
   `bar_width()` (web/templating.py) maps a percentage to one of these
   classes. They exist because inline style="width:N%" attributes were
   the last thing forcing `style-src 'unsafe-inline'` into the CSP, and
   a nonce cannot cover a style *attribute* - only <style> elements and
   stylesheet links. Snapping to 5% steps keeps the ladder short and the
   rendering indistinguishable at bar sizes. */
/* Percentage width scale. Deliberately complete even though only `.w-80` is
   used today: a page that needs 65% should not have to add a rung because the
   one it wanted was missing, and a gap in a scale reads as an oversight. Same
   reasoning as the unused --space-24 rung in tokens.css. */
.w-0   { width: 0%; }     .w-5   { width: 5%; }     .w-10  { width: 10%; }
.w-15  { width: 15%; }    .w-20  { width: 20%; }    .w-25  { width: 25%; }
.w-30  { width: 30%; }    .w-35  { width: 35%; }    .w-40  { width: 40%; }
.w-45  { width: 45%; }    .w-50  { width: 50%; }    .w-55  { width: 55%; }
.w-60  { width: 60%; }    .w-65  { width: 65%; }    .w-70  { width: 70%; }
.w-75  { width: 75%; }    .w-80  { width: 80%; }    .w-85  { width: 85%; }
.w-90  { width: 90%; }    .w-95  { width: 95%; }    .w-100 { width: 100%; }

/* ============================================================
   FILTER BAR (§4.5)
   ------------------------------------------------------------
   Shared by the assessment and report history tables. Still a plain
   GET form, so filters stay bookmarkable, shareable and working
   without JavaScript.

   The layout is a grid of independent items rather than nested flex
   groups. Two earlier attempts used flex and both broke: a row that
   does not fit either squeezes its children to their min-content or
   wraps them unpredictably, and since a <select> carries an intrinsic
   width from its widest option, the squeeze produced controls that
   overflowed their group and painted over each other. A grid track
   cannot overlap, so the only thing that can happen to a control that
   does not fit is that it takes its own line.

   Three templates, widest first, each declaring how many tracks the
   eight controls share. The optional facets (status on /reports,
   verdicts on the history tables) drop out of the flow rather than
   dragging the rest of the row sideways with them.
   ============================================================ */
.filter-bar {
  padding: var(--space-5);
  margin-bottom: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.filter-form {
  display: grid;
  gap: var(--space-3) var(--space-4);
  align-items: end;
  grid-template-columns:
    minmax(180px, 1.6fr)   /* search   */
    minmax(130px, 1fr)     /* status   */
    minmax(130px, 1fr)     /* verdict  */
    minmax(140px, 1.1fr)   /* domain   */
    minmax(250px, auto)    /* range    */
    minmax(120px, 1fr)     /* sort     */
    minmax(110px, 0.9fr)   /* order    */
    auto;                   /* actions  */
}

.filter-field { display: grid; gap: 6px; min-width: 0; }
.filter-field > label {
  color: var(--text-secondary);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.filter-form .input,
.filter-form .select { width: 100%; min-width: 0; height: 38px; }

/* The date range is one control made of two boxes, so it is bounded as a unit
   and separated by a dash. Without the bound each date renders as its own
   narrow "dd/mm/yyyy" field with a separate label, which is what made the pair
   unreadable. */
.filter-range {
  margin: 0;
  /* No bottom padding: the fieldset's own border sits on the grid's baseline,
     and any padding inside it lifted the two date inputs 5px above the
     selects beside them, so the row never looked like one row. */
  padding: 0 var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  display: flex;
  align-items: end;
  gap: var(--space-2);
  min-width: 0;
}
.filter-range legend {
  padding: 0 6px;
  color: var(--text-secondary);
  font-size: 0.75rem;
  font-weight: 600;
}
.filter-range input[type="date"] {
  height: 36px;
  flex: 1 1 0;
  min-width: 0;
  font-size: 0.8125rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  color-scheme: dark;
}
:root[data-theme="light"] .filter-range input[type="date"] { color-scheme: light; }
.filter-range-sep { color: var(--text-tertiary); }

.filter-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.filter-actions .btn { height: 38px; }

.filter-summary { margin: var(--space-4) 0 0; color: var(--text-tertiary); }
.filter-summary a { color: var(--accent); }

/* ---- the three layouts ------------------------------------------------- */

/* Two rows once eight controls stop fitting beside each other. Row one is
   "what matches", row two is "how it is ordered, and commit". The rule between
   them makes the split deliberate instead of looking like a wrap. */
@media (max-width: 1240px) {
  .filter-form {
    grid-template-columns:
      minmax(160px, 1.4fr) minmax(120px, 1fr) minmax(130px, 1.1fr) auto;
    row-gap: var(--space-4);
  }
  .filter-form > .filter-range,
  .filter-form > .filter-actions {
    grid-column: 1 / -1;
  }
  .filter-actions { justify-content: flex-end; }
  .filter-form > .filter-range {
    justify-self: start;
    margin-top: var(--space-1);
  }
}

/* One column. Every control full width, the actions pinned to the start so
   the primary action is where the thumb is. */
@media (max-width: 720px) {
  .filter-form { grid-template-columns: minmax(0, 1fr); }
  .filter-actions { justify-content: flex-start; }
  .filter-bar { padding: var(--space-4); }
  /* Two date boxes side by side stop fitting well before the phone does. */
  .filter-range { flex-wrap: wrap; }
  .filter-range input[type="date"] { flex: 1 1 110px; }
}

/* ---------- PAGINATION (restored) ---------- */
.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.pagination-status { font-variant-numeric: tabular-nums; }

/* A row of buttons that ends in a "cancel"-shaped action. Grouped so the
   spacing is not restated at each call site. */
.flex-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-5);
}

/* ---------- the customer's request list: find, filter, sort ---------- */

.support-controls {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.support-search { flex: 1 1 240px; min-width: 0; }
.support-control { display: flex; flex-direction: column; gap: 5px; }
.support-control label {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--text-secondary);
}
.support-control .input { width: auto; min-width: 140px; height: 40px; }
.support-count {
  margin: 0 0 var(--space-4);
  color: var(--text-tertiary);
  font-size: 0.8125rem;
}

@media (max-width: 640px) {
  .support-controls { gap: var(--space-2); }
  .support-search { flex: 1 1 100%; }
  .support-control { flex: 1 1 calc(50% - var(--space-2)); }
  .support-control .input { min-width: 0; width: 100%; }
  .support-controls .btn { flex: 1 1 100%; }
}

/* ==========================================================================
   SUPPORT THREAD (§5.11)
   --------------------------------------------------------------------------
   /support/<reference> is a page, and the conversation is the page.

   The previous design put the thread in a sticky, viewport-height panel docked
   to the right of a two-column layout, with the request's own facts beside it.
   That was wrong twice over. The panel existed only on that one URL, so from
   anywhere else in the app the same conversation was an ordinary row in a list
   that had to be clicked into — it read as a floating overlay rather than as
   the thing you were on. And it gave the page two scroll regions: the document,
   plus a fixed-height thread box inside it, so the oldest messages were only
   reachable by scrolling inside a box on a page that already scrolls.

   So: no panel, no internal scroller, no sticky, no close control. The thread
   grows with its content and the page scrolls once. What survives from the
   panel design is the part that was right — the newest message first in the
   DOM, and each side distinguishable without relying on colour.
   ========================================================================== */

/* No width cap. The page is nothing but the conversation now, so constraining
   it to a reading column left roughly half of every wide screen empty — the
   page read as a panel again, which is the thing that was removed. `.app-content`
   supplies the gutter; a message is bounded by `.thread-msg`'s own max-width
   instead, so long lines are still avoided. */
.thread { min-width: 0; }

/* ---------- /support/new ----------
   The two buttons on the new-request form. A bare `.btn` has no background and
   no border, so without this the secondary action rendered as a stray word
   beside the primary one. */
.support-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

/* ---------- status ---------- */

.thread-status {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: 0 0 var(--space-5);
  color: var(--text-secondary);
  font-size: 0.875rem;
}

/* ---------- the conversation ---------- */

.thread-list {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* A message is a bordered block with its author and time above it. Customer
   and support are told apart by ACCENT and ALIGNMENT as well as colour: a
   conversation you cannot follow is a conversation you cannot trust. System
   notices are centred and unboxed, because they are not from either party. */
/* Both a share and an absolute cap. The page is full width, so 82% alone would
   be 1400px of line on a large monitor; 720px alone would be too narrow inside
   a narrow one. `min()` takes whichever is smaller, so the measure stays
   readable at every width. */
.thread-msg {
  max-width: min(82%, 720px);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.thread-msg-you {
  align-self: flex-end;
  border-color: var(--accent);
  background: var(--accent-muted);
}
.thread-msg-agent { align-self: flex-start; }
.thread-msg-system {
  align-self: center;
  max-width: 100%;
  background: none;
  border-style: dashed;
  color: var(--text-tertiary);
  font-size: 0.8125rem;
  text-align: center;
}

.thread-msg-meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  font-size: 0.75rem;
  color: var(--text-tertiary);
}
.thread-msg-who { font-weight: 600; color: var(--text-secondary); }
.thread-msg-you .thread-msg-who { color: var(--accent); }

/* A message that arrived while the page was open. It fades out rather than
   blinking, and it is skipped entirely under reduced-motion — a message that
   appears by itself is announced by the toast and the aria-live region, so the
   colour flash is a bonus, not the signal. */
.thread-msg[data-fresh] {
  animation: thread-fresh 2.4s var(--ease) forwards;
}
@keyframes thread-fresh {
  from { border-color: var(--accent); background: var(--accent-muted); }
  to   { border-color: var(--border); background: var(--surface); }
}
.thread-msg-you[data-fresh] {
  animation-name: thread-fresh-you;
}
@keyframes thread-fresh-you {
  from { border-color: var(--accent); }
  to   { border-color: var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
  .thread-msg[data-fresh] { animation: none; }
}
.thread-msg-meta time { font-family: var(--mono); font-size: 0.6875rem; }

.thread-msg-body {
  font-size: 0.9375rem;
  line-height: 1.65;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.thread-msg-body p { margin: 0 0 var(--space-3); }
.thread-msg-body p:last-child { margin-bottom: 0; }
.thread-msg-body a { color: var(--accent); }

/* ---------- the reply box ---------- */

.thread-composer {
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.thread-composer > label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.thread-composer textarea { min-height: 96px; }
.thread-composer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

/* ---------- close the request ---------- */

.thread-close { margin-top: var(--space-5); }
.thread-close > summary {
  cursor: pointer;
  font-size: 0.875rem;
  padding: 6px 0;
}
.thread-close[open] > summary { margin-bottom: var(--space-3); }
.thread-close form p { margin-bottom: var(--space-3); }

/* ---------- this request's own facts ---------- */

.thread-details { margin: var(--space-4) 0 0; }
.thread-details > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}
.thread-details > div:first-child { padding-top: 0; }
.thread-details > div:last-child { border-bottom: none; padding-bottom: 0; }
.thread-details dt { color: var(--text-secondary); font-size: 0.875rem; }
.thread-details dd { color: var(--text); font-size: 0.875rem; margin: 0; text-align: right; }

/* ---------- the other requests ---------- */

/* Every request on the account, the one being read marked rather than hidden.
   A chooser that silently drops the selected item is a list you have to read
   twice to work out where you are, and a customer with one request gets an
   empty box that looks broken. */
.thread-list-nav { list-style: none; margin: var(--space-4) 0 0; padding: 0; }
.thread-list-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: inherit;
}
.thread-list-item:hover { background: var(--surface-2); }
.thread-list-item.is-current { background: var(--accent-muted); }
.thread-list-subject {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.875rem;
  color: var(--text);
}
.thread-list-meta { font-size: 0.75rem; color: var(--text-tertiary); }

/* The unread marker. A dot alone would be a colour-only signal, so it also
   carries a title for anyone who cannot see it. */
.sc-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--warning);
  flex: none;
}

/* The request's own facts and the list of other requests are the two blocks
   under the conversation. Stacked, they are two full-width cards with a line of
   text in each; side by side on a wide screen they fill the page without
   bringing back the two-column layout the conversation just shed. */
.thread-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  margin-top: var(--space-6);
}
@media (min-width: 1100px) {
  .thread-foot { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); }
}
.thread-foot .card { margin: 0 !important; }

/* The message the reply redirect points at. `scroll-margin-top` keeps the
   sticky top bar from covering the message it just scrolled to, which is the
   whole reason the fragment exists. */
.thread-msg:target {
  border-color: var(--accent);
}
.thread-msg:target .thread-msg-body { background: var(--accent-muted); border-radius: var(--radius-sm); }
.thread-list .thread-msg { scroll-margin-top: 80px; }

/* ---------- the history: what happened, between the messages ---------- */

/* A quiet divider, not a bubble. It belongs to nobody: an event is a change of
   state that the service recorded, and dressing it like a message would put it
   in somebody's mouth. Centred, unboxed, and the same width as the thread. */
.thread-event {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin: var(--space-2) 0;
  padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
  color: var(--text-tertiary);
  font-size: 0.8125rem;
}
.thread-event-mark {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--border-strong);
  transform: translateY(-2px);
}
.thread-event-text { flex: 1 1 auto; min-width: 0; }
.thread-event time { font-family: var(--mono); font-size: 0.6875rem; flex: none; }
/* The two events that change what the reader should do next, and so the two
   that get a colour. Everything else stays quiet. */
.thread-event[data-event-kind="resolved"] .thread-event-mark { background: var(--success); }
.thread-event[data-event-kind="reopened"] .thread-event-mark { background: var(--warning); }

/* ---------- a finished request: reopen, and rate ---------- */

.thread-reopened,
.thread-rated {
  margin: 0 0 var(--space-4);
  color: var(--text-tertiary);
  font-size: 0.8125rem;
}

.thread-reopen { margin-bottom: var(--space-5); }
.thread-reopen > summary { list-style: none; }
.thread-reopen > summary::-webkit-details-marker { display: none; }
.thread-reopen[open] > summary { margin-bottom: var(--space-4); }
.thread-reopen form p { margin-bottom: var(--space-3); }

.thread-rate {
  padding: var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  margin-bottom: var(--space-5);
}
.thread-rate h2 { font-size: 1.0625rem; margin-bottom: var(--space-2); }
.thread-rate > p { margin-bottom: var(--space-5); font-size: 0.875rem; }

.thread-rate-scale {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-2);
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-4);
}
.thread-rate-face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: var(--space-3) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  cursor: pointer;
  min-width: 0;
  transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}
.thread-rate-face:hover { border-color: var(--border-strong); }
/* The radio stays in the DOM and stays focusable — it is the control — and is
   drawn as the highlight on its own label rather than replaced by one. That
   keeps keyboard support, the form post and the announcement free. */
.thread-rate-face input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.thread-rate-num {
  font-family: var(--mono);
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--text-secondary);
  line-height: 1.1;
}
.thread-rate-word {
  font-size: 0.6875rem;
  color: var(--text-tertiary);
  text-align: center;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.thread-rate-face:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-muted);
}
.thread-rate-face:has(input:checked) .thread-rate-num { color: var(--accent); }
.thread-rate-face:has(input:checked) .thread-rate-word { color: var(--text-secondary); }
.thread-rate-face:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.thread-rate-form { display: flex; flex-direction: column; gap: var(--space-4); }
.thread-rate-form .btn { align-self: flex-start; }

@media (max-width: 640px) {
  /* Five columns of "Not great" does not fit a phone. The scale wraps to a
     tighter grid and the words go, because the number is the value and the word
     is the help. */
  .thread-rate-scale { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-1); }
  .thread-rate-face { padding: var(--space-2) var(--space-1); }
  .thread-rate-word { display: none; }
  .thread-msg, .thread-msg-system { max-width: 100%; }
  .thread-composer-row { flex-direction: column; align-items: stretch; }
  .thread-composer-row .btn { width: 100%; }
}


/* ============================================================
   TOUCH TARGETS
   ------------------------------------------------------------
   Every navigational link in the footer, the cookie notice and the
   compact rows rendered at 16-21px tall — their own line-height and
   nothing more. That is a legible text link and a poor tap target: on a
   phone the platform recommends 44px and WCAG 2.2 AA asks for 24px in
   either dimension, and the audit found undersized controls on all
   40 audited pages.

   The size is added as vertical padding on the link itself, which keeps
   the type and the surrounding rhythm untouched. Only standalone
   navigational links are listed: a link inside a sentence is left alone,
   because giving it 24px of height would break the line box it sits in.
   ============================================================ */
@media (max-width: 640px) {
  .footer-grid a,
  .footer-links a,
  .footer-bottom a,
  .cookie-notice a,
  .l-contact-link,
  .footer-row .brand,
  .site-footer .brand,
  .field-header .small {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
  }
  /* Two footer links side by side in the compact row: keep them from
     overlapping once each has padding. */
  .footer-links { gap: var(--space-2); }
  .footer-grid li { margin-bottom: 0; }
}

/* Two standalone action links the tap-target audit still flagged on a phone:
   "Verify ownership" in the report's ownership note (22px tall) and the
   "Request again" resend link on the password-reset page (17px). Both are the
   only interactive thing in their block, so neither qualifies for the
   inline-in-a-sentence exemption WCAG 2.2 grants to prose links. */
@media (max-width: 640px) {
  .verify-note-text a,
  .auth-alt a {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
  }
}

/* ============================================================
   LEGAL READING LAYOUT (§4.9)
   /terms and /privacy run to several thousand pixels of prose. The
   contents list is built by static/js/toc.js from the h2s the template
   already renders; these rules give it somewhere to live.

   Two things were wrong with the page as it stood. It had no way to move
   around 18 numbered sections, and the text ran the full 800px column of
   14px type — about 95 characters a line, well past the 45-75 that stays
   readable. The measure is now capped and the space beside it carries the
   contents, so the extra width is used rather than spent on line length.

   The two-column layout is a flex row, not a grid: as a grid, the
   explicitly-placed sticky rail pushed later sections into the rail's own
   column and the prose ran underneath it.
   ============================================================ */

.legal-toc {
  margin: var(--space-6) 0;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.legal-toc-title {
  font-size: 0.6875rem !important;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
  margin: 0 0 var(--space-3) !important;
}
.legal-toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  /* Two columns once there is room: 18 sections in one column is a wall. */
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  column-gap: var(--space-6);
}
.legal-toc-list li { margin: 0; }
.legal-toc-list a {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 5px 8px;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.4;
  border-left: 2px solid transparent;
}
.legal-toc-list a:hover { background: var(--surface-2); color: var(--text); }
.legal-toc-list a:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.legal-toc-list a.is-current {
  color: var(--accent);
  border-left-color: var(--accent);
  background: var(--accent-muted);
}
.legal-toc-num {
  flex: 0 0 auto;
  min-width: 1.4em;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--text-tertiary);
}

@media (min-width: 1200px) {
  /* .legal-body and .legal-has-toc are created by toc.js, which also moves the
     contents nav in here from inside the body. Both items are placed
     explicitly: with auto-placement the prose that followed the rail landed in
     the rail's own column and ran underneath it. Without JavaScript none of
     this applies and the page stays a single readable column, which is the
     correct fallback for a legal document. */
  .legal { max-width: 1000px; }
  .legal.legal-has-toc {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    column-gap: var(--space-10);
    align-items: start;
  }
  .legal.legal-has-toc .legal-body { grid-column: 1; grid-row: 1; min-width: 0; }
  .legal.legal-has-toc .legal-toc {
    grid-column: 2; grid-row: 1;
    position: sticky;
    top: 84px;
    max-height: calc(100vh - 108px);
    overflow-y: auto;
    margin: 0;
  }
  .legal.legal-has-toc .legal-toc-list { grid-template-columns: 1fr; }
  .legal p, .legal li, .legal h2, .legal h3 { max-width: 68ch; }
}
