/*
 * Phase 6a baseline styles for the Synesis P_success frontend.
 *
 * Card-based layout with progressive disclosure. Sections start hidden
 * (via the `hidden` HTML attribute) and main.js reveals them as the user
 * advances through the flow: setup → analysis → parameters / scenarios /
 * benchmarks / Monte Carlo. The full theme port from
 * `../SynesisLabs Platform/psuccess-platform.html` is still deferred —
 * this pass is the structural rework, with the visual upgrade staying
 * intentionally light so the markup is what we're committing to, not
 * the colours.
 */

/* === Tokens =============================================================== */
/*  Single source of truth for the colour palette. Tweak here, not at        */
/*  per-component sites. The hex values stay close to the legacy brand       */
/*  so a future dark-theme port stays a colour swap, not a layout fight.    */

:root,
[data-theme="light"] {
  --bg: #f5f7fa;
  --surface: #ffffff;
  --surface-muted: #f7fafc;
  --border: #e2e8f0;
  --border-strong: #cbd5e0;
  --text: #1a202c;
  --text-muted: #4a5568;
  --text-subtle: #5d6a7d;
  --accent: #2b6cb0;
  --accent-strong: #1e3a5f;
  --accent-soft: rgba(43, 108, 176, 0.08);
  /* Foreground for text sitting ON an accent or danger FILL. A separate token
     because the right answer differs per theme: light's accent is dark enough
     for white text (5.42:1), dark's is a bright cyan where white measures
     2.14:1 — a WCAG AA failure that shipped on the primary button, RUN
     PROJECT, and the active tab. Dark ink on the bright fill scores 8.79:1.
     Never hard-code #fff on a themed fill again. */
  --accent-on: #ffffff;
  --bad-on: #ffffff;
  --good: #2f855a;
  --good-soft: #d1fae5;
  --warn: #b7791f;
  --warn-soft: #fef3c7;
  --bad: #c53030;
  --bad-soft: #fee2e2;
  /* Foreground for text sitting ON a *soft* semantic fill, as distinct from
     --accent-on / --bad-on above, which cover text on a SOLID fill. The split
     is needed because this theme's soft fills are pale enough that the base
     semantic tone misses WCAG AA on them: measured 4.00:1 (good), 3.27:1
     (warn) and 4.48:1 (bad) against the 4.5:1 that normal-size text requires.
     Each value below holds its hue and drops lightness only as far as 4.6:1,
     so the palette darkens as little as accessibility allows. The dark theme
     needs no such split -- its semantic tones already clear 4.7:1 on the same
     fills -- but defines the tokens anyway so no component ever branches on
     theme to pick a foreground. */
  --good-on-soft: #2b7952;
  --warn-on-soft: #946219;
  --bad-on-soft: #c12f2f;
  --orange: #f97316;
  --modal-backdrop: rgba(15, 23, 42, 0.28);
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 3px rgba(15, 23, 42, 0.06);
  --shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
  --shadow-dialog: 0 8px 16px rgba(15, 23, 42, 0.10), 0 28px 64px rgba(15, 23, 42, 0.26);
}

/*  Dark-theme token overrides. Same variable names, different values —
 *  every component reads via `var(--*)` so swapping the attribute on
 *  `<html>` swaps the whole UI without touching any per-component CSS.
 *  Soft variants use rgba alpha so coloured backgrounds blend onto the
 *  dark surfaces correctly; light mode's pastel solids would look
 *  garish on a dark card.
 */
[data-theme="dark"] {
  --bg: #0b1120;
  --surface: #131c30;
  --surface-muted: #1c2740;
  --border: #2a3656;
  --border-strong: #475569;
  --text: #e7ecf3;
  --text-muted: #cbd5e0;
  --text-subtle: #8c9ab2;
  --accent: #38bdf8;
  --accent-strong: #7dd3fc;
  --accent-soft: rgba(56, 189, 248, 0.14);
  /* Dark ink on the bright fills — see the light block. 8.79:1 on --accent,
     11.29:1 on --accent-strong, 6.81:1 on --bad (#f87171, where white was
     2.77:1). */
  --accent-on: #0b1120;
  --bad-on: #0b1120;
  --good: #34d399;
  --good-soft: rgba(52, 211, 153, 0.18);
  --warn: #fbbf24;
  --warn-soft: rgba(251, 191, 36, 0.18);
  --bad: #f87171;
  --bad-soft: rgba(248, 113, 113, 0.18);
  /* See the light block. These already clear AA on their own soft fills
     (6.10:1, 6.90:1 and 4.74:1 measured over --surface), so they are the base
     tones unchanged. The tokens exist so a component can reference one name in
     both themes. */
  --good-on-soft: #34d399;
  --warn-on-soft: #fbbf24;
  --bad-on-soft: #f87171;
  --orange: #fb923c;
  --modal-backdrop: rgba(0, 0, 0, 0.42);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.5);
  --shadow-dialog: 0 8px 16px rgba(0, 0, 0, 0.45), 0 28px 64px rgba(0, 0, 0, 0.70);
}

/*  Smooth-but-not-distracting transition when the user toggles. Picks
 *  the properties that actually change so the page doesn't animate
 *  unrelated layout shifts.
 */
html {
  transition: background-color 0.15s linear;
}

body {
  transition: background-color 0.15s linear, color 0.15s linear;
}

/* === Page-level ========================================================== */
/*  Constrained reading column, neutral background, consistent type ramp.    */

* {
  box-sizing: border-box;
}

/* The `hidden` attribute must always win over a component's own `display`
   rule. Several shell elements set `display` (`.login-wrap` → flex, `.app-body`
   → grid, `.app-menu` / `.menu-item` → flex) which would otherwise override the
   UA `[hidden] { display: none }` and leave "hidden" elements rendered. This
   guard (the HTML5-boilerplate idiom) keeps `el.hidden = true` authoritative
   everywhere it's used. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
}

main {
  max-width: 880px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

/* === App header ========================================================== */
/*  Sticks to the top of the page (no scroll detach). Title on the left,    */
/*  account chip on the right; the chip is hidden when no session exists.   */

.app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.85rem 1.5rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.app-title {
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}

.app-title .title-accent {
  color: var(--accent);
}

.app-header-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* NOTE: `.app-header`, `.app-title`, `.app-header-actions` and `.title-accent`
 * above are retained because the admin SPA (`admin.html`) still uses that
 * header layout. The main app's header was replaced by the shell top bar in
 * UI-overhaul phase U0 (see the "Shell" sections near the end of this file). */

.theme-icon {
  display: block;
  pointer-events: none;
}

/*  Default (light theme): show moon glyph, hide sun. Dark theme flips. */
.theme-icon-sun { display: none; }
[data-theme="dark"] .theme-icon-moon { display: none; }
[data-theme="dark"] .theme-icon-sun  { display: block; }

/* === Card system ========================================================= */
/*  Every major section sits inside a .card. The visible heading is in
    .card-header > .card-title, with optional .card-subtitle copy. Actions
    live in .card-footer at the bottom. Cards stack vertically with
    consistent spacing so the page reads as a top-down checklist. */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-sm);
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.25rem;
}

.card-header {
  margin-bottom: 1rem;
}

.card-title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--accent-strong);
}

.card-subtitle {
  margin: 0.35rem 0 0;
  font-size: 0.9rem;
  color: var(--text-muted);
  line-height: 1.45;
}

.card-footer {
  margin-top: 1rem;
}

/* Section headings — "SUMMARY", "EXECUTIVE SUMMARY", and the Parameters group
   headings below, which share the colour. Record Ink rather than Marginalia
   Grey: these name the section, and a heading painted in the caption colour
   reads as a caption. The uppercase micro-label voice is unchanged, so the
   weight comes from ink, not from size. */
.section-subtitle {
  margin: 1rem 0 0.5rem;
  font-size: 0.95rem;
  color: var(--text);
  font-weight: 700;
}
/* In a .tab-head the heading shares the row with RE-RUN, so it drops the
   stacked-heading margins and lets the row own the spacing. */
.tab-head .section-subtitle {
  margin: 0;
}

.prose {
  line-height: 1.55;
  color: var(--text);
}

.prose strong {
  color: var(--accent-strong);
}

/* === Form controls ======================================================= */
/*  Inputs span the card width by default. Labels stack above inputs.       */

label {
  display: block;
  margin: 0.85rem 0 0.3rem;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--text);
}

label .label-hint {
  font-weight: 400;
  color: var(--text-subtle);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
select,
textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  font-size: 0.95rem;
  font-family: inherit;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  box-sizing: border-box;
}

input:focus,
select:focus,
textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}

textarea {
  resize: vertical;
  min-height: 5rem;
  font-family: inherit;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
  font-weight: 500;
  font-size: 0.9rem;
}

.checkbox-label input {
  width: auto;
  margin: 0;
}

/* === Buttons ============================================================= */
/*  Four visual roles: primary (the headline action — RUN MODEL, Log in),   */
/*  secondary (RUN scenarios / RUN benchmarks / Run MC), ghost (Log out,    */
/*  modal close, anything that shouldn't compete for attention), and danger */
/*  (the confirm button of a destructive dialog, and nothing else).         */

button {
  font-family: inherit;
  cursor: pointer;
  border: 0;
  border-radius: 6px;
  font-weight: 600;
  padding: 0.5rem 1rem;
  font-size: 0.95rem;
  transition: filter 0.12s, background-color 0.12s;
}

button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-on);
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-strong);
}

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

/* Hover was `#edf2f7`, a hard-coded light grey that painted a near-white fill
   under light text in dark mode. --accent-soft is the themed equivalent and
   agrees with the accent border/text this rule already sets. */
.btn-secondary:hover:not(:disabled) {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

/* Destructive confirmations: privacy's Clear History and the scenario /
   benchmark delete dialog (issue #56). It sat beside the privacy panel's rules
   while that was its only caller; a second caller elsewhere in the app moved it
   up here to the shared button block. */
.btn-danger {
  background: var(--bad);
  color: var(--bad-on);
}

.btn-danger:hover:not(:disabled) {
  filter: brightness(0.92);
}

.btn-ghost {
  background: transparent;
  color: var(--text-muted);
  padding: 0.35rem 0.65rem;
  font-weight: 500;
}

.btn-ghost:hover:not(:disabled) {
  color: var(--text);
  background: var(--surface-muted);
}

.btn-large {
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
}

.btn-small {
  padding: 0.3rem 0.55rem;
  font-size: 0.85rem;
}

/* Horizontal group of buttons inside a card-footer (e.g. Export + Generate
   report). Wraps on narrow screens; the gap keeps them from touching. */
.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Visually hidden, still announced. The API chip shows its verdict as a
   colour, which is not readable by everyone; #apiStatusText carries the same
   verdict as text inside the chip's `role="status"` region, where a screen
   reader announces it on change. Clip-based rather than `display:none` or
   `visibility:hidden` — both of those remove the node from the accessibility
   tree entirely, which would defeat the purpose. */
.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;
}

/* === Status + error lines ================================================ */
/*  Monospace so a status string's width doesn't jitter as it updates. Used
    by #reportStatus and the admin page; the two tab loading fields moved to
    .snake-field (issue #47).                                                */

.status-line {
  margin: 0.6rem 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92rem;
  color: var(--accent);
}

.error {
  color: var(--bad);
  margin: 0.5rem 0 0;
}

.meta-status {
  text-align: center;
  color: var(--text-muted);
  font-style: italic;
  margin: 1rem 0;
}

/* === Sign-in (Clerk) ===================================================== */

/*
 * The sign-in screen is just a centering wrapper around Clerk's own card — we
 * intentionally don't wrap it in our .card (that produced a redundant outer box
 * the Clerk card overflowed). Clerk is themed to our palette via clerkAppearance()
 * in app/clerk.js.
 */
.login-wrap {
  display: flex;
  /* Column so the admin-only notice (when revealed) stacks above the Clerk
     card rather than sitting beside it; align-items keeps the card centred
     exactly as the old row layout did. */
  flex-direction: column;
  align-items: center;
  margin: 3rem auto;
  padding: 0 1rem;
}

/* Warning banner shown on admin-only deployments (branch trial stacks) after
   a non-admin's sign-in is refused server-side — see auth:admin-only in
   app/main.js. Uses the theme's "bad" colour at low opacity so it reads as a
   warning in both light and dark. */
.login-notice {
  max-width: 25rem;
  margin: 0 0 1.25rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--bad);
  border-radius: 6px;
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 8%, transparent);
  font-size: 0.92rem;
}

.card-title {
  margin: 0;
}

/* === Upload zone ========================================================= */

.upload-zone {
  border: 1.5px dashed var(--border-strong);
  border-radius: 6px;
  padding: 1rem 0.75rem;
  text-align: center;
  font-size: 0.9rem;
  color: var(--text-muted);
  cursor: pointer;
  background: var(--surface-muted);
  transition: border-color 0.12s, color 0.12s, background-color 0.12s;
}

.upload-zone:hover,
.upload-zone:focus {
  border-color: var(--accent);
  color: var(--accent);
  outline: none;
}

.upload-zone.dragging {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

.upload-hint {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  color: var(--text-subtle);
}

.file-list {
  margin-top: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.file-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.55rem;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 0.85rem;
}

.file-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-badge {
  font-size: 0.75rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--border);
  color: var(--text-muted);
}

.file-badge-parsing { background: var(--border); color: var(--text-muted); }
.file-badge-ok      { background: var(--good-soft); color: var(--good); }
.file-badge-unsupported { background: var(--warn-soft); color: var(--warn); }
.file-badge-error   { background: var(--bad-soft); color: var(--bad); }

.file-remove {
  background: transparent;
  color: var(--text-subtle);
  border: 0;
  padding: 0 0.4rem;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  font-weight: 700;
}

.file-remove:hover {
  color: var(--bad);
}

/* The old six-cell diagnostic result strip (.result-strip / .result-cell /
   .result-label / .result-value) was replaced by the headline metrics strip in
   UI-overhaul phase U0 — see the "Shell: headline strip" section below. */

/* ========================================================================= *
 *  Parameters tab: the Ink Ledger
 *
 *  The fourteen parameters as one ruled ledger, read left to right: name and
 *  evidence state | figure over a two-mark scale | the case, then its sources.
 *  Built on Vercel's Geist system: ink on near-white, one grey ramp, hairline
 *  rules, Geist for words and Geist Mono for figures, sources and state.
 *
 *  PILOT SCOPE. This is the first surface in the world the whole app will move
 *  to. Its tokens are scoped to the Parameters tab panel
 *  (.tab-panel[data-tab="parameters"]) so the other tabs keep their current
 *  look until they migrate; lifting these tokens to :root is that migration's
 *  first step.
 *
 *  COLOUR RULE. Colour marks evidence state only -- an uncited claim, in amber.
 *  The value itself is never coloured: a green or red figure reads as a
 *  verdict, and the scoring model is unvalidated.
 *
 *  FORMULA RULE. Every scale has the same geometry and the rows stay in their
 *  taxonomy order. Nothing here sizes, orders or weights a parameter by its
 *  influence on P_success.
 *
 *  FONT COST. Two variable woff2 files, ~70 KB each, self-hosted under
 *  vendor/geist (see vendor/README.md). `font-display: swap`, so text renders
 *  in the system stack first and never waits on the font.
 * ========================================================================= */

@font-face {
  font-family: "Geist";
  src: url("vendor/geist/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("vendor/geist/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Tokens and ground. The world owns the whole Parameters tab panel: its tokens
   are scoped to the panel, and the panel is painted as one sheet on the
   ledger's own ground -- #fafafa, or a neutral near-black in dark. The ledger
   used to sit straight on the app's page ground (#f5f7fa, and navy in dark),
   whose blue tint pulled against the neutral inks. The sheet is padded and
   hairline-edged so that ground edge reads as deliberate: it is the pilot seam
   between this world and the tabs that have not migrated yet.

   Every text colour clears 4.5:1 on the ground in both themes, and the scale
   line, a UI boundary, clears 3:1. Lines are translucent ink rather than fixed
   greys, so the ratios hold on whatever surface this sheet later moves onto. */
.tab-panel[data-tab="parameters"] {
  --il-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --il-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --il-ground: #fafafa;
  /* Floating surfaces (the definition popover) sit one step above the ground. */
  --il-raised: #ffffff;
  --il-ink: #171717;
  --il-body: #4d4d4d;
  --il-mute: #666666;
  --il-rule: rgba(23, 23, 23, 0.10);
  --il-rule-strong: rgba(23, 23, 23, 0.50);
  --il-link: #0761d1;
  --il-warn: #a1510a;
  --il-focus: #0761d1;
  --il-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.10);
  /* The native range thumb's width. Marks are placed along the thumb's travel,
     which starts and ends half a thumb in from each edge of the input. */
  --il-thumb: 10px;
  /* The ledger's cell geometry, shared by the rows, the column head and the
     group headings. They are tokens because the wide layouts further down
     place copies of the column labels and range words by adding these widths
     up: if the name column changed width and a copy's offset did not, the
     range words would drift off their scales. */
  --il-name-col: 11rem;
  --il-scale-col: 14rem;
  --il-cell-gap: 1.5rem;
  /* Between two ledger columns on a wide screen: twice the gap between cells,
     so a row's own cells never read as belonging to its neighbour. */
  --il-column-gap: 3rem;

  padding: 1.25rem 1.5rem;
  border: 1px solid var(--il-rule);
  border-radius: 8px;
  background: var(--il-ground);
  font-family: var(--il-sans);
  color: var(--il-ink);
}
[data-theme="dark"] .tab-panel[data-tab="parameters"] {
  --il-ground: #0a0a0a;
  --il-raised: #161616;
  --il-ink: #ededed;
  --il-body: #c7c7c7;
  --il-mute: #a3a3a3;
  --il-rule: rgba(237, 237, 237, 0.12);
  --il-rule-strong: rgba(237, 237, 237, 0.50);
  --il-link: #6cb4ff;
  --il-warn: #f5a623;
  --il-focus: #6cb4ff;
  --il-shadow: 0 1px 2px rgba(0, 0, 0, 0.40), 0 10px 28px rgba(0, 0, 0, 0.55);
}
/* The ledger lays itself out against its own width, not the viewport: the
   sidebar takes a variable share of the screen, so the tab's width is the
   number that decides how many columns fit. */
#slidersSection {
  container: ledger / inline-size;
}
#slidersSection ::selection {
  background: var(--il-ink);
  color: var(--il-ground);
}

/* Visually hidden, still read by screen readers. */
.ledger-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Coverage line: the tab's lead ------------------------------------- */
.ledger-coverage {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 1rem;
  padding: 0.25rem 0 1.25rem;
  border-bottom: 1px solid var(--il-rule);
}
.ledger-coverage-figure {
  font-family: var(--il-sans);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--il-ink);
}
/* Only the counts are figures; the words around them are prose. */
.ledger-num {
  font-family: var(--il-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}
.ledger-coverage-detail {
  font-family: var(--il-mono);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  color: var(--il-body);
}
.ledger-coverage-detail .is-unsourced { color: var(--il-warn); }
.ledger-legend {
  display: flex;
  gap: 1.25rem;
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--il-mute);
}
.ledger-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

/* --- One grid, shared by the column head, group headings and rows ------- */
/* The middle column is a FIXED width, so every scale in the ledger has the
   same geometry: any two parameters compare by eye, and a range shared by a
   whole group can be set once over the column instead of on every row.
   The column head holds one set of labels per ledger column (see
   sliders.js#buildColumnHead); each set is the row grid, so its labels sit
   over the cells they name. */
.ledger-head-set,
.ledger-group,
.param-row {
  display: grid;
  grid-template-columns: var(--il-name-col) var(--il-scale-col) minmax(0, 1fr);
  column-gap: var(--il-cell-gap);
}
.ledger-head {
  padding: 0.75rem 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--il-mute);
}
/* Copies of the column labels and of a group's range words, for a second and
   third ledger column. Hidden until the tab is wide enough to lay those
   columns out; the "Wide tabs" container queries reveal them. */
.ledger .ledger-twin { display: none; }

/* Group headings stick to the top of the scroller while their rows pass
   beneath. The elasticities' range words ("low risk / high risk") live only on
   their heading, so without this they scrolled out of sight after the first
   few rows -- and range words must stay visible. */
#slidersSection h2.ledger-group {
  position: sticky;
  /* `top: 0`, not an offset. Which element scrolls depends on width: the
     inner `.content` scroller (20px padding-top) on desktop, but the whole
     `main.shell-main` (no padding) once the shell stacks on phones. A
     negative offset tuned for the first clipped the heading by 20px on the
     second (measured at 390px). The ::before band below handles the strip
     `.content`'s padding leaves above a heading pinned on desktop. */
  top: 0;
  z-index: 4;
  align-items: end;
  row-gap: 0.3rem;
  margin: 2.25rem 0 0;
  padding: 0.6rem 0;
  background: var(--il-ground);
  border-bottom: 1px solid var(--il-ink);
  font-family: var(--il-sans);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--il-ink);
}
/* A band of ground above each heading, as tall as `.content`'s padding-top.
   On desktop a pinned heading sits at the scroller's padding edge, 20px below
   its top edge, and rows passing through that strip showed above the heading;
   the band covers them. Unpinned, it lies inside the heading's own top margin,
   over nothing but ground. On phones, where nothing pads the scroller, it sits
   above the screen edge. It also takes the pointer there, so a click on the
   strip can never reach a row hidden beneath it. */
#slidersSection h2.ledger-group::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 1.25rem;
  background: var(--il-ground);
}
/* 1.5rem, not 1.25rem: the band above must clear the coverage line's rule. */
#slidersSection .ledger-coverage + h2.ledger-group { margin-top: 1.5rem; }
/* The title takes the whole row: inside the 11rem name column it wrapped as
   "Internal Factors / (Project Strengths)". It is a button, because the
   heading is also the group's disclosure: clicking the title folds the group
   away and back (sliders.js#groupHeading). It keeps the heading's type, and
   carries the sidebar accordions' chevron -- pointing up while the group is
   open, down once it is folded -- so both kinds of fold read the same. */
.ledger-group-toggle {
  grid-column: 1 / -1;
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.ledger-group-toggle .chev { border-color: var(--il-mute); }
.ledger-group-toggle:hover .chev { border-color: var(--il-ink); }
.ledger-group-toggle[aria-expanded="true"] .chev { transform: rotate(-135deg); }
.ledger-group-toggle:focus-visible {
  outline: 2px solid var(--il-focus);
  outline-offset: 4px;
  border-radius: 4px;
}
/* A folded group's shared range words describe scales that are no longer on
   screen, so they fold with it. */
#slidersSection h2.ledger-group[data-collapsed] .param-group-caption { display: none; }
.param-group-caption {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  justify-content: space-between;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--il-mute);
}

/* --- A row -------------------------------------------------------------- */
.param-row {
  grid-template-areas:
    "name     setting case"
    "evidence setting case";
  grid-template-rows: auto 1fr;
  row-gap: 0.35rem;
  align-items: start;
  padding: 1rem 0;
  border-bottom: 1px solid var(--il-rule);
}
.param-row:last-child { border-bottom: none; }

/* Name + definition trigger. The ⓘ is the label's SIBLING -- see
   sliders.js#buildLabelCell. */
.param-label {
  grid-area: name;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  /* The name leads the row, so it is never smaller than the figure. */
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--il-ink);
}
.param-label-name {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
}

/* Evidence state, read as a column down the left edge. */
.param-evidence {
  grid-area: evidence;
  font-family: var(--il-mono);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--il-body);
}
.param-row[data-evidence="unsourced"] .param-evidence { color: var(--il-warn); }
.param-row[data-evidence="pending"] .param-evidence { color: var(--il-mute); }
/* An unassessed row's figure is the range midpoint, not anything the model
   concluded. It is set quiet and its mark hollow, so a default never reads
   with the weight of an evidenced setting. This is evidence state, not a
   judgement of the value, so it keeps to the colour rule. */
.param-row[data-evidence="pending"] .param-figure {
  font-weight: 400;
  color: var(--il-mute);
}
.param-row[data-evidence="pending"] .param-mark-setting {
  box-sizing: border-box;
  border: 1.5px solid var(--il-mute);
  background: var(--il-ground);
}

/* Setting: the figure, the scale, the range words, the adjustment note. */
.param-value {
  grid-area: setting;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}
.param-figure {
  font-family: var(--il-mono);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--il-ink);
}

/* The scale. The drawn parts are presentational; the real range input lies
   over the whole box at opacity 0, so drag, keyboard and the accessible name
   all come from the native control. */
.param-scale {
  position: relative;
  height: 1.25rem;
  border-radius: 4px;
}
.param-scale-line {
  position: absolute;
  top: 50%;
  /* Inset by half a thumb, so the end ticks sit exactly on min and max. */
  left: calc(var(--il-thumb) / 2);
  right: calc(var(--il-thumb) / 2);
  height: 1px;
  background: var(--il-rule-strong);
}
/* End ticks: a measured range has marked ends. */
.param-scale-line::before,
.param-scale-line::after {
  content: "";
  position: absolute;
  top: -4px;
  width: 1px;
  height: 9px;
  background: var(--il-rule-strong);
}
.param-scale-line::before { left: 0; }
.param-scale-line::after { right: 0; }

/* The gap between the model's proposal and the current setting. Drawn only
   once the two differ -- the ledger's one authored motion. */
.param-gap {
  position: absolute;
  top: 50%;
  left: calc(var(--il-thumb) / 2 + (100% - var(--il-thumb)) * var(--p, 0));
  width: calc((100% - var(--il-thumb)) * var(--dp, 0));
  /* 3px, a clear step up from the 1px line: the departure should read as a
     departure, not as a slightly thicker stretch of line. */
  height: 3px;
  transform: translateY(-50%);
  background: var(--il-ink);
  opacity: 0;
  transition: opacity 0.16s cubic-bezier(0.22, 1, 0.36, 1);
}
.param-row[data-adjusted] .param-gap { opacity: 1; }

.param-mark {
  position: absolute;
  top: 50%;
  /* `--p` (0-1) is set by sliders.js#placeMark. Mapping it onto the thumb's
     travel keeps the dot under the pointer at the ends of the scale, where a
     plain 0-100% placement drifted by half the thumb's width. */
  left: calc(var(--il-thumb) / 2 + (100% - var(--il-thumb)) * var(--p, 0));
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.param-mark-setting {
  z-index: 2;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--il-ink);
}
.param-mark-proposal {
  z-index: 1;
  width: 8px;
  height: 8px;
  border: 1.5px solid var(--il-body);
  background: var(--il-ground);
  transform: translate(-50%, -50%) rotate(45deg);
}
/* In the key, the marks sit in the text flow instead of on a scale. */
.ledger-legend .param-mark {
  position: relative;
  top: auto;
  left: auto;
  transform: none;
}
.ledger-legend .param-mark-proposal { transform: rotate(45deg); }

.param-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 3;
  /* A known thumb width, so the marks can be mapped onto the same travel.
     The browser default (16px in Chromium) is not something CSS can read. */
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
.param-input::-webkit-slider-runnable-track { height: 100%; background: transparent; }
.param-input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: var(--il-thumb);
  height: 100%;
  border: 0;
  background: transparent;
}
.param-input::-moz-range-track { height: 100%; background: transparent; }
.param-input::-moz-range-thumb {
  width: var(--il-thumb);
  height: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
}
/* The input is invisible, so keyboard focus is drawn around the whole scale. */
.param-scale:focus-within {
  outline: 2px solid var(--il-focus);
  outline-offset: 4px;
}

/* Range words, held level at each end of the scale. */
.param-range {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.75rem;
  color: var(--il-mute);
}
.param-adjusted {
  margin: 0;
  font-family: var(--il-mono);
  font-size: 0.75rem;
  color: var(--il-body);
  /* One line, always. "Adjusted from the estimated 0.62" is 230px in Geist
     Mono against the 224px (14rem) setting column, so it wrapped -- and since
     the note holds its space even while hidden, every assessed internal
     factor grew a line taller. Unwrapped, it runs at most ~6px into the
     1.5rem gap before the case column, which it never reaches. */
  white-space: nowrap;
  /* Always holds its line, so revealing it never moves the rows below.
     Hidden with visibility, which also takes it out of the accessibility
     tree while the setting still matches the proposal. It fades in with the
     gap as one authored moment. */
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.16s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 0.16s;
}
.param-row[data-adjusted] .param-adjusted {
  visibility: visible;
  opacity: 1;
  transition: opacity 0.16s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s;
}

/* Case: the model's reasoning, then the sources behind it. */
.param-rationale {
  grid-area: case;
  min-width: 0;
}
.param-rationale-text {
  margin: 0;
  /* Capped measure: prose unbounded ran to 131 characters on a wide screen. */
  max-width: 68ch;
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--il-body);
  text-wrap: pretty;
  /* The model writes this text and sometimes pastes a raw URL into it. A URL
     with no hyphen gives the browser no break point: one measured 219-441px
     past its cell -- into the neighbouring column on a wide screen, off the
     edge on a phone. Break inside such a token only when it cannot otherwise
     fit; ordinary words still wrap between words. The source line below does
     the same. */
  overflow-wrap: anywhere;
}
.param-sources {
  margin: 0.3rem 0 0;
  max-width: 68ch;
  font-family: var(--il-mono);
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--il-mute);
  overflow-wrap: anywhere;
}
.param-sources a {
  color: var(--il-link);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 3px;
}
.param-sources a:hover { text-decoration-color: currentColor; }
.param-rationale-nosrc {
  margin: 0.3rem 0 0;
  font-family: var(--il-mono);
  font-size: 0.75rem;
  color: var(--il-warn);
}

/* The definition trigger and popover, in the ledger's hand. The popover is
   anchored to the trigger's bottom edge rather than a fixed `top: 1.5em`, so
   the shared 0.5em hover bridge closes the gap at ANY trigger size -- which
   matters because touch screens enlarge the trigger (see the coarse-pointer
   rule below). */
.ledger .slider-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  height: 1em;
  margin-left: 0;
  font-size: 0.875rem;
  color: var(--il-mute);
}
.ledger .slider-info svg {
  flex: none;
  width: 1em;
  height: 1em;
}
.ledger .slider-info:hover,
.ledger .slider-info:focus { color: var(--il-ink); }
.ledger .slider-info:focus {
  outline: 2px solid var(--il-focus);
  outline-offset: 2px;
  border-radius: 999px;
}
.ledger .rationale-tip {
  border: 1px solid var(--il-rule-strong);
  border-radius: 8px;
  background: var(--il-raised);
  top: calc(100% + 0.25em);
  box-shadow: var(--il-shadow);
  font-family: var(--il-sans);
  font-size: 0.875rem;
  color: var(--il-ink);
}
.ledger .rationale-tip > strong {
  color: var(--il-ink);
  font-weight: 600;
}
.ledger .rationale-tip .rt-info { color: var(--il-body); }

@media (prefers-reduced-motion: reduce) {
  .param-gap,
  .param-adjusted,
  .param-row[data-adjusted] .param-adjusted,
  .ledger-group-toggle .chev { transition: none; }
}

/* Touch: the drawn scale stays slim, but the box the range input fills grows
   to 44px and the definition trigger to about 24px. Measured at phone width,
   they were 20px and 14px -- below what a finger can reliably hit. */
@media (pointer: coarse) {
  .param-scale { height: 2.75rem; }
  .ledger .slider-info {
    width: 1.75em;
    height: 1.75em;
  }
}

/* --- Wide tabs: parameters stand side by side --------------------------- */
/* With one row per line a large monitor left most of the tab empty: the case
   text stops at 68ch, so at 2560px every row ended about 60% of the way
   across. Once the tab can hold two whole rows abreast it does, and three on
   an ultrawide. Rows still flow in taxonomy order, left to right and then
   down, which is also their DOM and Tab order. The column a parameter lands
   in is only where the order wraps -- never a ranking.

   Each threshold is a row's narrowest comfortable width times the number of
   columns, plus the gaps between them. A row needs its name and scale
   columns, two cell gaps and at least 24rem of case text (about 55
   characters a line): 11 + 14 + 3 + 24 = 52rem. Two columns therefore need
   2 x 52 + 3 = 107rem and three need 3 x 52 + 2 x 3 = 162rem. Container
   queries cannot read custom properties, so these are literals: change them
   together with the geometry tokens. The renderer emits label copies for up
   to three columns (LEDGER_MAX_COLUMNS in sliders.js), and
   tests/ledger-wide-columns.test.mjs fails if a step here outgrows it. */
@container ledger (min-width: 107rem) {
  .ledger {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--il-column-gap);
  }
  /* The lead, the column head and the group headings run across every
     column, so a sticky heading still pins over all of them. */
  .ledger > .ledger-coverage,
  .ledger > .ledger-head,
  .ledger > .ledger-group { grid-column: 1 / -1; }
  /* The column head and the headings take the ledger's own columns, so each
     copy of the labels lands over its own column of rows. */
  .ledger-head,
  #slidersSection h2.ledger-group {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--il-column-gap);
  }
  /* One pair of range words per column, on the heading's second line,
     pushed in by the name column and a cell gap so each pair sits exactly
     over its column's scales -- where the single pair sits in one column. */
  .param-group-caption {
    grid-column: auto;
    grid-row: 2;
    width: var(--il-scale-col);
    margin-inline-start: calc(var(--il-name-col) + var(--il-cell-gap));
  }
  .ledger .ledger-head-set[data-col="2"] { display: grid; }
  .ledger .param-group-caption[data-col="2"] { display: flex; }
  /* Every row keeps its closing rule. Side by side, the last row is rarely
     alone on its line, and a rule missing under one of two level rows reads
     as a gap rather than an ending. Rows sharing a line already stretch to
     the taller one (the ledger's default alignment), so their rules meet. */
  .ledger > .param-row:last-child { border-bottom: 1px solid var(--il-rule); }
}
@container ledger (min-width: 162rem) {
  .ledger,
  .ledger-head,
  #slidersSection h2.ledger-group { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ledger .ledger-head-set[data-col="3"] { display: grid; }
  .ledger .param-group-caption[data-col="3"] { display: flex; }
}

/* --- Narrower tabs: the case drops under the name and setting ----------- */
@container ledger (max-width: 50rem) {
  .ledger-head { display: none; }
  .ledger-group,
  .param-row { grid-template-columns: var(--il-name-col) minmax(0, 1fr); }
  .param-row {
    grid-template-areas:
      "name     setting"
      "evidence setting"
      "case     case";
    row-gap: 0.6rem;
  }
}

/* --- Narrowest: one column, in reading order ---------------------------- */
@container ledger (max-width: 30rem) {
  .ledger-group { grid-template-columns: 1fr; row-gap: 0.3rem; }
  .param-group-caption { grid-column: 1; }
  .param-row {
    grid-template-columns: 1fr;
    grid-template-areas: "name" "evidence" "setting" "case";
  }
  .ledger-legend { margin-left: 0; }
}

/* === Source badges ======================================================= */
/*  Inline chips above the analysis summary: "web research used" /          */
/*  "N documents".                                                          */

.source-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.source-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.78rem;
  background: var(--border);
  color: var(--text-muted);
}

.source-badge.active {
  background: var(--good-soft);
  color: var(--good);
}

.source-badge.inactive {
  background: var(--surface-muted);
  color: var(--text-subtle);
}

/* === Scenario cards (U3) ================================================= */
/*  Collapsible ScenarioCards: header row (id chip, direction arrow, title,
    Δpp badge, ✎/× icon buttons) + an expandable drawer (description, the
    shifted-params table, outcome line, Adopt, FOLD UP). Rendered by
    app/scenarios.js. All colours come from the existing tokens so both themes
    work unchanged.                                                            */

.scenarios-list {
  margin-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.scenarios-hint {
  margin: 0 0 0.25rem;
  font-size: 0.8rem;
  color: var(--text-subtle);
  font-style: italic;
}

.scenario-card {
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: 6px;
  overflow: hidden;
}
.scenario-card.scenario-up   { border-left-color: var(--good); }
.scenario-card.scenario-down { border-left-color: var(--bad); }

/* Adopted card: accent-tinted ring so the user can see which entry currently
   drives the sliders (shared visual language with .benchmark-card.adopted). */
.scenario-card.adopted {
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent) inset;
}

/* Header row — the expand/collapse affordance. */
.scenario-card-head {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 0.85rem;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.95rem;
}
.scenario-card-head:hover {
  background: var(--surface);
}
.scenario-card-head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.scenario-id-chip {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--accent);
}

.scenario-up .scenario-arrow   { color: var(--good); }
.scenario-down .scenario-arrow { color: var(--bad); }

/* Title takes the slack so the Δpp badge + icons sit flush right. */
.scenario-card-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Projected Δpp vs baseline (filled async from /score/batch). */
.scenario-pp {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--text-muted);
  white-space: nowrap;
}
.scenario-pp.pos { color: var(--good); }
.scenario-pp.neg { color: var(--bad); }

/* ✎ / × icon buttons (shared style with the benchmark cards). */
.scenario-card-actions,
.benchmark-card-actions {
  display: inline-flex;
  gap: 0.15rem;
}
.scenario-icon-btn,
.benchmark-icon-btn {
  background: none;
  border: 0;
  padding: 0.1rem 0.3rem;
  font-size: 0.9rem;
  line-height: 1;
  color: var(--text-subtle);
  cursor: pointer;
  border-radius: 4px;
}
.scenario-icon-btn:hover,
.benchmark-icon-btn:hover {
  color: var(--accent);
  background: var(--accent-soft);
}

/* --- Drawer ------------------------------------------------------------- */

.scenario-drawer,
.benchmark-drawer {
  padding: 0.75rem 0.85rem 0.85rem;
  border-top: 1px solid var(--border);
  cursor: auto;
}

.scenario-desc,
.benchmark-desc {
  font-size: 0.88rem;
  color: var(--text-muted);
  line-height: 1.5;
}
.scenario-desc p,
.benchmark-desc p {
  margin: 0 0 0.4rem;
}

/* Shared shifted-params / comparison table pattern. Column widths are fixed
   for the numeric cells; the wrapper scrolls horizontally on narrow screens
   (min-width on the rows) so the cards never overflow the panel. */
.scenario-table,
.benchmark-table {
  margin-top: 0.75rem;
  overflow-x: auto;
}
.scenario-table-head,
.scenario-tr,
.benchmark-table-head,
.benchmark-tr {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 30rem; /* horizontal scroll instead of squashing at ~900px */
  border-bottom: 1px solid var(--border);
  padding: 0.3rem 0;
}
.scenario-table-head,
.benchmark-table-head {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--text-subtle);
}
.scenario-th.col-name, .scenario-td.col-name,
.benchmark-th.col-name, .benchmark-td.col-name {
  flex: 1;
  min-width: 0;
}
.scenario-th.col-num, .scenario-td.col-num {
  width: 6.5rem;
  flex: none;
}
.benchmark-th.col-num, .benchmark-td.col-num {
  width: 6.5rem;
  flex: none;
}
.scenario-tr, .benchmark-tr {
  font-size: 0.85rem;
}
.scenario-td-code, .benchmark-td-code {
  color: var(--text-subtle);
  font-size: 0.72rem;
}
.scenario-td.mono, .benchmark-td.mono {
  font-family: ui-monospace, SFMono-Regular, monospace;
}
.scenario-td.strong, .benchmark-td.strong { font-weight: 700; }
.scenario-td.muted, .benchmark-td.muted { color: var(--text-muted); }
/* Favourability colouring: pos = favourable move, neg = unfavourable (an
   elasticity DECREASE is favourable — computed in JS per param direction). */
.scenario-td.pos, .benchmark-td.pos { color: var(--good); }
.scenario-td.neg, .benchmark-td.neg { color: var(--bad); }

/* Outcome line: Baseline P_success X% → Scenario P_success Y%. */
.scenario-outcome {
  margin-top: 0.75rem;
  font-size: 0.88rem;
}
.scenario-outcome strong {
  font-family: ui-monospace, SFMono-Regular, monospace;
}
.scenario-outcome strong.pos { color: var(--good); }
.scenario-outcome strong.neg { color: var(--bad); }

/* Adopt button row + FOLD UP footer (shared with benchmark cards). */
.scenario-adopt-row,
.benchmark-adopt-row {
  margin-top: 0.6rem;
  display: flex;
  justify-content: flex-end;
}
.scenario-adopt-btn,
.benchmark-adopt-btn {
  font-size: 0.78rem;
  font-weight: 700;
  padding: 0.45rem 1.1rem;
  border-radius: 6px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}
.scenario-adopt-btn:hover,
.benchmark-adopt-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}
/* Adopted state: filled accent + revert affordance in the label. */
.scenario-adopt-btn.active,
.benchmark-adopt-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
}

.scenario-foldup,
.benchmark-foldup {
  margin-top: 0.75rem;
  width: 100%;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: none;
  padding: 0.45rem;
  text-align: center;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  cursor: pointer;
}
.scenario-foldup:hover,
.benchmark-foldup:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* === Benchmark cards (U4) ================================================ */
/*  Collapsible BenchmarkCards: #rank / title / p_success% header + drawer
    (rationale, Sources line, comparison line, Adopt, full 14-param table).
    Rendered by app/peers.js; most drawer styles are shared with the scenario
    selectors above.                                                          */

.peers-list {
  margin-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.benchmark-card {
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.benchmark-card.adopted {
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent) inset;
}

.benchmark-card-head {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 0.85rem;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.95rem;
}
.benchmark-card-head:hover {
  background: var(--surface);
}
.benchmark-card-head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.benchmark-rank {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.78rem;
  color: var(--text-subtle);
}

.benchmark-card-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.benchmark-prob {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  white-space: nowrap;
}

/* Muted "Sources: …" line under the description; only http(s) entries are
   links (escaping + URL vetting happens in peers.js#buildSourcesLine). */
.benchmark-sources {
  margin-top: 0.4rem;
  font-size: 0.78rem;
  color: var(--text-subtle);
  overflow-wrap: anywhere; /* long URLs must not overflow the card */
}
.benchmark-sources a {
  color: var(--text-subtle);
  text-decoration: underline;
}
.benchmark-sources a:hover {
  color: var(--accent);
}

/* "<Name> X% vs Your baseline Y% (you trail/lead by N pp)" line. */
.benchmark-compare {
  margin-top: 0.75rem;
  font-size: 0.88rem;
}
.benchmark-compare .cmp-name { font-weight: 700; }
.benchmark-compare strong {
  font-family: ui-monospace, SFMono-Regular, monospace;
}
.benchmark-compare .cmp-pos { color: var(--good); }
.benchmark-compare .cmp-neg { color: var(--bad); }
.benchmark-compare .cmp-neutral { color: var(--text-muted); }

/* === Monte Carlo trigger + modal ========================================= */
/*  The trigger is the flagship action in its section, and now says so with
    the system's own accent rather than an orange-to-red gradient.

    What it was: `linear-gradient(135deg, #f97316, #dc2626)` with `!important`
    on three declarations. That broke the visual system in four ways at once —
    it was the only gradient in the stylesheet, it hard-coded two colours so
    neither theme reached it, it introduced a second accent hue, and white on
    its lighter stop measured ~2.4:1. It also read as an alarm on a control
    that merely opens a sensitivity analysis. The `!important` flags existed
    only to beat the `.btn-*` class it ships alongside; matching that
    specificity instead removes the need for them.                            */

.mc-trigger {
  background: var(--accent);
  color: var(--accent-on);
  border: 0;
  font-weight: 700;
}

.mc-trigger:hover:not(:disabled) {
  background: var(--accent-strong);
}

/* === Dialogs: the shared shell ============================================ */
/*
   Depth and radius for every <dialog> in the app, in one place, on the element
   selector — the six in index.html plus the three built in JS (the privacy
   confirmation, the Clerk privacy fallback, the report preview). Per-id rules
   below still own each dialog's size, padding and content; they no longer
   restate its radius or its backdrop.

   WHY THIS EXISTS. Until now no dialog in this app had a shadow at all. The
   Elevation doctrine says "flat by default; shadow means floating", and names
   the dialog as one of the three things that genuinely float — but the
   `0 6px 24px rgba(0,0,0,0.18)` recorded as the dialog shadow was in fact
   declared on `.rationale-tip`, a tooltip. Modals were bordered cards on a dim
   wash, which is why they read as pasted onto the page rather than above it.

   WHY BLUR AND NOT JUST A BIGGER SHADOW. A shadow needs a ground to fall on.
   Measured side by side in both themes, a black shadow over the dark theme's
   near-black field is close to invisible, so shadow alone would have solved
   this in light mode only. Blurring the backdrop is theme-independent: it
   separates by focus rather than by contrast, and it works identically in both.
   The shadow stays because it is what carries the effect in light mode. The
   wash is correspondingly lighter than it was (0.45 -> 0.28) — with the page
   behind now unreadable, dimming it as hard would only cost legibility of the
   dialog's own edge.

   The blur is not decoration: an unreadable background is the point of a modal.
   `backdrop-filter` costs one compositing layer, on an element that exists at
   most one at a time. */
dialog {
  /* Rounder than the 8px the rest of the system uses for its largest objects.
     A dialog is the only thing here that floats free of the page grid, so it is
     the one surface whose corners answer to nothing else's alignment. */
  border-radius: 14px;
  box-shadow: var(--shadow-dialog);
}

dialog::backdrop {
  background: var(--modal-backdrop);
  /* Safari shipped `backdrop-filter` unprefixed only in 18.0. Without the
     prefix, iOS/macOS 17 and earlier get no blur AND the lightened wash
     (0.45 -> 0.28) — strictly less separation than before this change, most
     consequentially on the Terms consent gate, whose whole point is that the
     page behind is not readable. */
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
}

#monteCarloModal {
  max-width: 760px;
  width: 92%;
  border: 1px solid var(--border-strong);
  padding: 1.25rem 1.5rem;
  background: var(--surface);
  color: var(--text);
  margin: auto;
}

#monteCarloModal h2 {
  margin: 0 0 0.25rem;
  font-size: 1.15rem;
  color: var(--accent-strong);
}

.modal-subtitle {
  margin: 0 0 1rem;
  font-size: 0.9rem;
  color: var(--text-muted);
}

#monteCarloModal label span {
  font-family: ui-monospace, SFMono-Regular, monospace;
  color: var(--accent);
  font-weight: 700;
  margin-left: 0.25rem;
}

#monteCarloModal input[type="range"] {
  width: 100%;
}

.mc-status {
  margin: 0.6rem 0 0;
  font-style: italic;
  color: var(--accent);
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
}

/* Destructive confirmations keep the shared footer's right-aligned cluster and
   its 0.5rem gap — the arrangement every other dialog in the app uses, and the
   dominant convention on the desktop web (Material, macOS alerts). Ordering is
   the part that carries meaning and is near-universal: Cancel first, the
   confirming action rightmost.

   What they do NOT keep is the default auto width. Both buttons are given the
   same floor so the pair reads as one either/or rather than as two controls of
   whatever size their labels happened to need — "Cancel" against "Clear
   History" differ by more than double at natural width.

   An opposite-edges layout was tried here and reverted: at a gap wide enough to
   protect against a mis-click, the two stopped reading as a single decision.

   Applied by descendant selector rather than a modifier class because the
   privacy footer is built in JS and this needs no second place to keep in step. */
#confirmDeleteModal .modal-footer > button,
.privacy-confirm-dialog .modal-footer > button {
  /* 9rem ~= 144px: comfortably past the widest label ("Clear History") so both
     buttons settle on the same width rather than the floor being decorative.

     Capped at 45% of the footer because a flat 9rem overflows a phone. Measured
     rather than reasoned: two 144px buttons plus the 0.5rem gap need 296px, and
     at a 360px viewport this footer has 281px (delete) / 274px (Clear History),
     at 375px it has 295px / 288px. `min-width` blocks flex shrink, so the excess
     spilled past the dialog's LEFT border — taking Cancel, the safe choice in a
     destructive dialog, outside the card. `flex-wrap` is the backstop for the
     case where even 45% is below a label's own min-content width. */
  min-width: min(9rem, 45%);
}

#confirmDeleteModal .modal-footer,
.privacy-confirm-dialog .modal-footer {
  flex-wrap: wrap;
}

/* A hairline above the buttons, INSET — it spans the text column and stops at
   the dialog's padding, rather than running to the card edge.

   That is this app's pattern, not a generic modal-footer one. `.terms-footnote`
   is the only other rule-above-a-block inside a padded dialog here, and it does
   exactly this: margin-top, padding-top, border-top, no negative margins. Where
   the app does reach an edge (`.sidebar-run`, `.clarify-chat-input-row`,
   `.benchmark-drawer`) it gets there STRUCTURALLY — the container carries no
   padding, so a band with its own padding meets the edge naturally. Nothing in
   this stylesheet cancels a parent's padding with negative margins, and a
   full-bleed footer here would have been the first. `--border` is the system's
   stated primary separation device.

   The 1rem sits on the footer rather than as a margin-bottom on the last body
   line because which line IS last varies — the delete dialog's third line is
   hidden unless the entry is adopted, so a rule keyed to `:last-of-type` lands
   on a display:none element and never applies. The footer owns its own approach
   instead, which holds whatever precedes it, and wins the margin collapse
   against every body line above it.

   Equal air above and below the rule: a divider with more space on one side
   reads as belonging to that side rather than as separating the two. */
#confirmDeleteModal .modal-footer,
.privacy-confirm-dialog .modal-footer {
  margin-top: 1rem;
  border-top: 1px solid var(--border);
  padding-top: 1rem;
}

.mc-results {
  margin-top: 1rem;
}

.mc-chart {
  width: 100%;
  max-height: 220px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: 6px;
}

/* SVG marker styling — extracted from inline SVG attributes into CSS
 * classes so the chart adapts to the theme. Bars keep their semantic
 * red/amber/green inline-fills (those convey meaning, not theming);
 * markers and axis labels follow the theme's text colour.
 */
.mc-marker {
  stroke: var(--text);
  stroke-width: 1.2;
  stroke-dasharray: 3 3;
}

.mc-marker-label {
  fill: var(--text);
  font-size: 10px;
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.mc-axis-label {
  fill: var(--text-muted);
  font-size: 10px;
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.mc-stats {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.75rem;
  margin: 0.85rem 0 0.5rem;
}

.mc-stat-label {
  font-size: 0.82rem;
  color: var(--text-subtle);
  font-weight: 700;
}

.mc-stat-value {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text);
}

.mc-stat-value.mc-stat-low  { color: var(--bad); }
.mc-stat-value.mc-stat-high { color: var(--good); }
.mc-stat-value.mc-stat-mean { color: var(--accent); }

.mc-band {
  margin: 0.25rem 0 0;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.mc-verdict {
  margin-top: 0.85rem;
  padding: 0.75rem 0.9rem;
  background: rgba(249, 115, 22, 0.08);
  border-left: 3px solid var(--orange);
  border-radius: 4px;
}

.mc-verdict-label {
  font-size: 0.82rem;
  color: var(--warn);
  font-weight: 700;
}

.mc-verdict-text {
  margin-top: 0.25rem;
  font-size: 0.95rem;
  color: var(--text);
  line-height: 1.45;
}

/* === Model clarification modal ========================================== */

#modelClarificationModal {
  max-width: 560px;
  width: 90%;
  border: 1px solid var(--border-strong);
  padding: 1.25rem 1.5rem;
  color: var(--text);
  background: var(--surface);
  margin: auto;
}

#modelClarificationModal h2 {
  margin: 0 0 0.25rem;
  font-size: 1.1rem;
  color: var(--accent-strong);
}

#modelClarificationModal .modal-body {
  white-space: pre-wrap;
  line-height: 1.5;
  font-size: 0.95rem;
  color: var(--text);
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.75rem 0.85rem;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: 4px;
}

/* === Responsive tweaks =================================================== */

@media (max-width: 600px) {
  /* No `.param-row` rule here any more. The old row shape (`8rem 1fr 3.5rem`)
     outlived the design it belonged to and, at phone width, forced three
     columns onto the Ink Ledger's one-column stack -- measured at 390px, every
     case was squeezed into a 128px track at 13 characters a line. The ledger
     sizes itself with container queries against its own width instead. */
  .mc-stats {
    grid-template-columns: repeat(2, 1fr);
  }
  main {
    padding: 1rem 0.85rem 3rem;
  }
}

/* === Phase 6d: Terms modal ============================================== */

/* Terms modal: tall, scrollable body with a pinned header banner + footer.
   Flex layout is scoped to [open] so the dialog's closed (display:none)
   state from the UA stylesheet still applies. */
#termsModal {
  max-width: 760px;
  width: 92%;
  max-height: 85vh;
  border: 1px solid var(--border-strong);
  padding: 1.25rem 1.5rem;
  background: var(--surface);
  color: var(--text);
  margin: auto;
}
#termsModal[open] {
  display: flex;
  flex-direction: column;
}
#termsModal h2 {
  margin: 0 0 0.5rem;
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: -0.4px;
}
.terms-tag {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-weight: 500;
  margin-left: 0.6rem;
  padding-left: 0.6rem;
  border-left: 1px solid var(--border-strong);
}

/* Amber "out of date" notice — pinned above the scrollable body. */
.terms-stale-banner {
  background: var(--warn-soft);
  border: 1px solid var(--warn);
  border-radius: 5px;
  padding: 0.7rem 0.85rem;
  margin: 0 0 0.6rem;
  font-size: 0.85rem;
  line-height: 1.5;
}
.terms-stale-banner strong {
  color: var(--warn);
}

/* Blue consent banner — shown only in gate mode. */
.terms-consent-banner {
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: 5px;
  padding: 0.7rem 0.85rem;
  margin: 0 0 0.6rem;
  font-size: 0.85rem;
  line-height: 1.5;
}

/* The scrollable legal text. It takes the remaining height between the banners
   and the footer; overflow-y makes it scroll.

   `flex: 1 1 auto` (NOT `flex: 1`) is load-bearing, and the difference is a
   blank Terms modal on every iPhone/iPad. `flex: 1` expands to `flex-basis: 0`,
   which makes this item contribute *nothing* to the dialog's intrinsic height.
   #termsModal has no definite height — only `max-height: 85vh` — so the flex
   container has to size itself from its content. Blink and Gecko resolve that
   through the item's max-content flex fraction and correctly size the dialog to
   the terms text (then cap it at 85vh); WebKit does not, and instead shrink-wraps
   the dialog to the banners + footer. With no free space left to grow into, this
   element lands at 0px of content height and the entire terms document is
   scrolled out of view — a bordered empty strip, and, because gate mode has no
   dismiss route and Accept only unlocks on scroll-to-end, a hard lock-out of the
   Platform (issue #74).

   `flex-basis: auto` gives the item a content-based base size, so every engine
   sizes the dialog to the text, hits the 85vh cap, and shrinks this item back
   down; `min-height: 0` is what permits that shrink below content height.
   Verified against Safari/WebKit and Chrome/Blink at desktop, tablet and phone
   viewports — do not "simplify" this back to `flex: 1`. */
.terms-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.75rem 1rem;
  font-size: 0.82rem;
  line-height: 1.55;
}
.terms-body h3 {
  color: var(--accent-strong);
  font-size: 0.9rem;
  margin: 1rem 0 0.35rem;
}
.terms-body h3:first-of-type {
  margin-top: 0.25rem;
}
.terms-body ul {
  padding-left: 1.3rem;
  margin: 0.35rem 0;
}
.terms-body a {
  color: var(--accent);
}
.terms-effective,
.terms-footnote {
  color: var(--text-muted);
  font-size: 0.75rem;
}
.terms-footnote {
  margin-top: 1.2rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}

/* === Per-parameter ⓘ rationale popover (slider rows) =================== */

/* The ⓘ trigger sits inline after each slider's label text. position:relative
   makes it the anchor for the absolutely-positioned popover. */
.slider-info {
  position: relative;
  display: inline-block;
  margin-left: 0.35rem;
  color: var(--accent);
  cursor: help;
  font-size: 0.85rem;
  line-height: 1;
  font-weight: 700;
}
.slider-info:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
/* THE ⓘ HOVER BRIDGE — applies to both triggers that collapse their leading.

   `line-height: 1` shrinks a trigger to its glyph while its popover still sits
   at `top: 1.5em`, so a strip of dead space opens between them. Measured in a
   browser: `.slider-info` 13.6px tall against 19.2px (5.6px of gap), and
   `.headline-info` 12px against 18.72px (6.7px). A pointer travelling from the
   ⓘ down into the popover crossed that strip, lost `:hover`, and the popover
   closed under it. This invisible extension joins the two — the same expander
   idiom the touch-target section below uses.

   Both keep `line-height: 1` for reasons of their own (the headline one's is
   documented at `.headline-info`), so the bridge is the fix rather than the
   leading. The app's other two ⓘ (`.brand-info`, `.sub-info`) never had the
   gap: they inherit the body leading, so their boxes are already as tall as
   their offset. Anything new that collapses a trigger's leading belongs in
   this rule. */
.slider-info::after,
.headline-info::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  height: 0.5em;
}

/* The popover. Hidden until the trigger is hovered or focused. Wider than the
   label column, so it overlays the slider area (high z-index). Scrolls
   internally if a rationale + long source list overflow the max height. */
.rationale-tip {
  display: none;
  position: absolute;
  top: 1.5em;
  left: 0;
  z-index: 50;
  width: 300px;
  max-width: 78vw;
  max-height: 320px;
  overflow-y: auto;
  padding: 0.7rem 0.85rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.5;
  text-align: left;
  white-space: normal;
  cursor: auto;
}
.slider-info:hover .rationale-tip,
.slider-info:focus .rationale-tip,
.slider-info:focus-within .rationale-tip {
  display: block;
}
.rationale-tip > strong {
  display: block;
  margin-bottom: 0.25rem;
  color: var(--accent-strong);
}
.rationale-tip .rt-info {
  color: var(--text-muted);
}

/* --- Popovers stay on screen -------------------------------------------- */
/* Every ⓘ popover opens below its trigger, from its left edge. tip-position.js
   measures one as it opens and, when it would leave the visible area, adjusts
   it through the two hooks below: --tip-dx slides it left by exactly the
   overshoot, and data-tip-flip="up" on the trigger opens it above instead.
   Measured before: all 18 popovers ran past the right edge of a 390px phone
   (by up to 105px), and the Engine tab's lower definitions opened below the
   fold of a 1728px laptop (by up to 98px), cut off by the content panel.
   The flip rules outrank each popover's own `top` by specificity, and follow
   the Engine tab's `.ledger .rationale-tip` (equal specificity) in source. */
.rationale-tip,
.headline-tip,
.brand-tip,
.sub-tip { translate: calc(-1 * var(--tip-dx, 0px)) 0; }
[data-tip-flip="up"] > .rationale-tip,
[data-tip-flip="up"] > .headline-tip,
[data-tip-flip="up"] > .brand-tip,
[data-tip-flip="up"] > .sub-tip {
  top: auto;
  bottom: calc(100% + 0.25em);
}
/* THE ⓘ HOVER BRIDGE (above) joins a trigger to its popover across the gap
   between them. When the popover opens above, the bridge must too. The brand
   and sub-label triggers have no bridge of their own, so this rule is
   complete in itself rather than a patch on `.slider-info::after`. */
[data-tip-flip="up"]::after {
  content: "";
  position: absolute;
  top: auto;
  bottom: 100%;
  left: 0;
  width: 100%;
  height: 0.5em;
}

/* ========================================================================= *
 *  UI-overhaul phase U0 — app-shell layout
 *
 *  The main app moved from a single centred column of progressively-revealed
 *  cards to an app shell: a fixed top bar, a left sidebar (380px, growing to
 *  520px on large screens -- see .app-body), and a right content panel with a
 *  four-tab bar. Everything below is expressed in the
 *  existing token palette (`--surface`, `--border`, `--accent`, `--good`, …) so
 *  both the light and dark themes work without any new colour values.
 * ========================================================================= */

/* === Shell: grid ========================================================= */
/*  The shell is a vertical stack: the top bar, then a body that fills the rest
    of the viewport. The body is a two-column grid (sidebar + content), each
    column scrolling independently so the headline strip / tab bar stay put
    while long content scrolls. Below ~900px the grid collapses to a single
    column and the whole page scrolls normally (a fuller responsive pass is
    deferred to U5). */

.app-shell {
  display: flex;
  flex-direction: column;
  /* Exactly viewport height — NOT min-height. With min-height the shell
     grows past the viewport when sidebar/content get tall, the page itself
     scrolls, and the sidebar's pinned RUN PROJECT footer scrolls away with
     it. A fixed height forces all overflow into the sidebar's and content
     panel's own scroll containers (2026-08-03). */
  height: 100vh;
}

.shell-main {
  /* Override the global `main` rule (max-width 880px, centred, padded) — the
     shell body must span the full width so the sidebar/content grid fills it. */
  max-width: none;
  margin: 0;
  padding: 0;
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
}

/* The signed-in shell body: sidebar | content. `hidden` (display:none from the
   UA stylesheet) still wins when #appSection is hidden pre-login.

   The sidebar grows with the screen rather than staying 380px. Fixed, it took
   22% of a 16" laptop (1728px wide) but only 15% of a 2560px monitor, where it
   read as a strip and the tabs sprawled. It now holds 380px up to laptop
   widths, then grows in a straight line to about 470px at 2560px, and stops
   at 520px so an ultrawide does not get a slab of empty sidebar. 470px is
   deliberately halfway between the old 380px and a strictly proportional 22%
   (563px), which was tried and read as too wide. The line runs through
   (1728px, 380px) and (2560px, 470px): slope 90 / 832 = 10.82vw, intercept
   380 - 0.1082 x 1728 = 193px. Below 900px the shell stacks and the
   max-width: 900px block replaces this grid. */
.app-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: clamp(380px, 193px + 10.82vw, 520px) minmax(0, 1fr);
}

/* Flex column: the scrollable body (.sidebar-scroll) takes the free height
   and the RUN PROJECT footer (.sidebar-run, styled with the other sidebar
   pieces further down) stays pinned at the bottom, always on screen. */
.sidebar {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Grid items refuse to shrink below their content by default
     (min-height:auto); without this the tall sidebar stretches the grid row
     past the viewport and the pinned footer overflows off screen. */
  min-height: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.sidebar-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.1rem 1.25rem;
}

.content {
  overflow-y: auto;
  min-width: 0;
  /* Same grid-item shrink allowance as .sidebar: tall tab content must
     scroll in here, not stretch the shared grid row past the viewport. */
  min-height: 0;
  padding: 1.25rem 1.5rem 3rem;
}

/* The login view fills the shell body and centres Clerk's card.
   .login-wrap is a COLUMN flex (so the admin-only notice stacks above the
   Clerk card — see the base rule), which rotates the axes here: top-pinning
   the card is justify-content, not align-items. An align-items override
   would instead LEFT-shift the card (that exact bug shipped once —
   left-justified sign-in form). Horizontal centring comes from the base
   rule's align-items: center. */
.shell-main > .login-wrap {
  flex: 1;
  justify-content: flex-start;
  /* The shell is hard-capped at 100vh now (see .app-shell); on a short
     window the Clerk card must scroll within the login view rather than
     clip. Vertical only — horizontal centring still comes from the base
     rule's align-items and is not affected. */
  overflow-y: auto;
}

/* === Shell: top bar ====================================================== */
/*  Two clusters — brand (left), API chip + hamburger (right; New Project
    lives in the hamburger menu). Fixed height, own surface, bottom border. */

.topbar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  height: 60px;
  padding: 0 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar-brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
}

.brand-wordmark {
  font-weight: 800;
  font-size: 1.56rem;
  letter-spacing: -0.01em;
  color: var(--text);
  white-space: nowrap;
}

/* The Synesis ⓘ tooltip trigger. position:relative anchors the popover. */
.brand-info {
  position: relative;
  display: inline-block;
  margin-left: 0.3rem;
  color: var(--accent);
  cursor: help;
  font-size: 0.8rem;
  font-weight: 700;
}
.brand-info:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* The Synesis blurb popover — two paragraphs, revealed on hover/focus. */
.brand-tip {
  display: none;
  position: absolute;
  top: 1.6em;
  left: 0;
  z-index: 300;
  width: 320px;
  max-width: 80vw;
  padding: 0.85rem 1rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.55;
  text-align: left;
  white-space: normal;
  cursor: auto;
}
.brand-info:hover .brand-tip,
.brand-info:focus .brand-tip,
.brand-info:focus-within .brand-tip {
  display: block;
}
.brand-tip-p {
  display: block;
}
.brand-tip-p + .brand-tip-p {
  margin-top: 0.6rem;
}

/* Thin vertical divider between the wordmark and the product name. */
.brand-divider {
  width: 1px;
  height: 31px;
  background: var(--border-strong);
}

.brand-product {
  font-weight: 700;
  /* Match the wordmark's enlarged size (was the inherited 1rem body default). */
  font-size: 1.56rem;
  color: var(--accent);
  white-space: nowrap;
}

/* Small "BETA" chip. */
.brand-beta {
  /* 0.6rem (9.6px) previously — below an 11px legibility floor for functional
     text. 0.7rem is 11.2px and the chip's proportions are unchanged. */
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
}

/* Right cluster: API chip + hamburger. margin-left:auto pushes it to the far
   edge now that the centre cluster is gone (New Project moved into the
   hamburger menu, 2026-08-03 styling pass). */
.topbar-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: none;
  margin-left: auto;
}

/* API status chip: a pill REPORT with a status dot — not a button. It used to
   open the API-settings modal; that modal is gone (one server-pinned model,
   nothing left to choose), so the element is a <span> and has no hover
   affordance. `cursor: default` is explicit rather than inherited so it stays
   right if this rule is ever reused on something interactive by mistake. */
.api-chip {
  cursor: default;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.85rem;
  border-radius: 999px;
  background: var(--surface-muted);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.03em;
}
/* Status dot: green when the server's Anthropic key probes healthy, red when
   Anthropic rejects it, and the neutral base below for "we could not ask" —
   see `app/api-dot.js#updateApiDot`, which owns all three. The neutral default
   also covers the moment before the first probe returns. */
.api-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-subtle);
  flex: none;
}
.api-dot-good { background: var(--good); }
.api-dot-bad { background: var(--bad); }

/* === Shell: hamburger menu =============================================== */

.menu-wrap {
  position: relative;
}

.menu-toggle {
  background: transparent;
  border: 0;
  padding: 0.3rem 0.55rem;
  border-radius: 6px;
  font-size: 1.2rem;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
}
.menu-toggle:hover {
  background: var(--surface-muted);
  color: var(--text);
}

/* Dropdown panel, anchored to the toggle. `hidden` closes it (main.js). */
.app-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 200;
  min-width: 250px;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.4rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
}

/* One menu row — used for links, buttons, and the theme toggle. Applies to both
   <button> and <a> so they read as one list. */
.menu-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}
.menu-item:hover {
  background: var(--surface-muted);
}

/* The signed-in email chip: quieter, and allowed to wrap a long address. */
.menu-email {
  color: var(--text-muted);
  font-size: 0.82rem;
  word-break: break-all;
}

/* Theme toggle as a menu item: the icon stack sits before the "Theme" label. */
.theme-toggle-icons {
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
}

/* === Shell: sidebar ====================================================== */

/* (The "PROJECT MENU" heading and its .sidebar-title rule were dropped on
   2026-07-28 — the sidebar's contents are self-describing and the label was
   just consuming vertical space at the top of the column.) */

.sidebar-block {
  margin-bottom: 1rem;
}

/* ======================================================================== */
/* Question panel (U1) — the clarify-chat authoring surface.                */
/*                                                                          */
/* Rendered by `app/clarify-chat.js`. Two states share the bordered panel:  */
/* the chat (message log + input row) and the collapsed ACCEPTED QUESTION   */
/* card. All colours come from the existing theme tokens (no new palette),  */
/* so both light and dark themes work unchanged. The accepted brief lands   */
/* in the hidden #projectDescription field; these rules only style the UI.  */
/* ======================================================================== */

.question-panel {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
  margin-bottom: 1rem;
}

/* Panel header row ("Question"). */
.question-head {
  display: flex;
  align-items: center;
  padding: 0.65rem 0.8rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-muted);
}

.question-title {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
}

/* --- Chat state --------------------------------------------------------- */

.clarify-chat-log {
  /* min-height gives the panel presence on first load: the welcome bubble sits
     at the top with breathing room below it, instead of the panel collapsing
     to a cramped strip (user feedback, 2026-07-22). max-height still caps the
     log once a conversation grows; between the two it scrolls. */
  min-height: 200px;
  max-height: 320px;
  overflow-y: auto;
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* One message bubble. Bot/system align left; the user's own turns align
   right. Bot/user carry one square corner to match the wireframe's shape. */
.clarify-msg {
  max-width: 88%;
  padding: 0.5rem 0.7rem;
  border-radius: 10px;
  font-size: 0.9rem;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
}

.clarify-msg-bot {
  align-self: flex-start;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  color: var(--text);
  border-bottom-left-radius: 2px;
}

/* User bubble: accent fill. `color: var(--surface)` is the theme-safe inverse
   of the accent (accent-on-surface is used for links, so surface-on-accent is
   equally legible) — readable in both light and dark modes. */
.clarify-msg-user {
  align-self: flex-end;
  background: var(--accent);
  color: var(--surface);
  border-bottom-right-radius: 2px;
}

.clarify-msg-system {
  align-self: center;
  background: transparent;
  color: var(--text-muted);
  font-style: italic;
  font-size: 0.83rem;
  max-width: 100%;
  text-align: center;
}

.clarify-msg-thinking {
  align-self: flex-start;
  color: var(--text-muted);
  letter-spacing: 0.15em;
}

/* Amber advisory, shown under a closed brief when the clarifier flagged the
   question as describing something other than what the engine models — one
   identified party's capacity to finish work against constraints by a date
   (`engine_fit: false`). Full width rather than a left-aligned bubble:
   it annotates the brief above it instead of being another turn in the chat,
   and it matches `.terms-stale-banner`'s amber-rule treatment. Advisory only —
   the Accept button in the bubble above stays live. */
.clarify-msg-warning {
  align-self: stretch;
  max-width: 100%;
  background: var(--warn-soft);
  border: 1px solid var(--warn);
  border-radius: 5px;
  color: var(--text);
  font-size: 0.85rem;
}
/* The "here" link to the recommended use cases. Underlined rather than
   colour-only so it reads as clickable against the amber fill in both themes. */
.clarify-msg-warning a {
  color: var(--accent);
  text-decoration: underline;
}
.clarify-msg-warning a:hover {
  color: var(--text);
}
/* Second line of the warning: the model's own one-sentence reason for the
   label. Muted and a step smaller than the advisory above it — it is
   supporting detail, and the advisory is the part that must land first. */
.clarify-warning-why {
  margin-top: 0.35rem;
  color: var(--text-muted);
  font-size: 0.8rem;
}

/* The closed brief, shown inside the bot's "ready" message as a quoted block. */
.clarify-brief-block {
  margin-top: 0.4rem;
  padding: 0.5rem 0.6rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 0.88rem;
  color: var(--text);
}

/* Accept button, inside the "ready" bubble (extends .btn-primary). */
.clarify-accept-btn {
  display: block;
  width: 100%;
  margin-top: 0.6rem;
}

.clarify-chat-input-row {
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem;
  border-top: 1px solid var(--border);
}

.clarify-chat-input {
  flex: 1;
  resize: vertical;
  /* Roomy default: fits the wrapped placeholder without a native scrollbar and
     gives the user space to type a real description (user feedback). */
  min-height: 5.75rem;
}

/* Send stretches to match the input's height so the row reads as one control
   (align-self: stretch, not flex-end — the button previously sat short at the
   bottom of the row). */
.clarify-chat-input-row .btn-primary {
  align-self: stretch;
  white-space: nowrap;
}

/* --- Accepted state ----------------------------------------------------- */

.clarify-accepted {
  padding: 0.8rem;
}

/* The ACCEPTED QUESTION content. Deliberately *unstyled* as a card: the
   surrounding .question-panel already provides the border, radius and
   background, so giving this a second border/fill nested it inside its own
   well (user feedback, 2026-07-28). It keeps only `position: relative`, which
   anchors the ✎ edit button to the content area's top-right corner. */
.clarify-accepted-card {
  position: relative;
}

.clarify-accepted-label {
  /* 0.65rem (10.4px) previously — under the 11px floor for functional text. */
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--text-subtle);
  margin-bottom: 0.5rem;
  padding-right: 1.8rem; /* clear the edit button */
}

.clarify-accepted-brief {
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ✎ Edit affordance, top-right of the accepted content. Zero offsets align it
   with the content box (which is already inset by .clarify-accepted's padding)
   so it sits flush with the label's baseline row rather than floating inward. */
.clarify-edit-btn {
  position: absolute;
  top: 0;
  right: 0;
  width: 1.6rem;
  height: 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 0.9rem;
  color: var(--text-subtle);
  background: transparent;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}

.clarify-edit-btn:hover {
  background: var(--accent-soft);
  color: var(--text);
}

/* Accordion: a bordered group with a clickable header (a real <button> for
   keyboard access) and a body toggled via the `hidden` attribute. */
.accordion {
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 0.6rem;
  background: var(--surface);
  overflow: hidden;
}

.accordion-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.65rem 0.8rem;
  background: var(--surface-muted);
  border: 0;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}

/* Chevron indicator: a rotated corner that points down when collapsed and up
   when the accordion is expanded (`aria-expanded="true"`). */
.chev {
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--text-subtle);
  border-bottom: 2px solid var(--text-subtle);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
  flex: none;
}
.accordion-head[aria-expanded="true"] .chev {
  transform: rotate(-135deg);
}

.accordion-body {
  padding: 0.75rem 0.8rem 1rem;
}

.accordion-desc {
  margin: 0 0 0.6rem;
  font-size: 0.82rem;
  color: var(--text-muted);
  line-height: 1.45;
}

/* Stacked, full-width action buttons inside an accordion. */
.sidebar-actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.sidebar-actions button {
  width: 100%;
}

/* Small uppercase sub-labels inside the Information Source accordion
   ("Online Information", "Private Documents"). */
.sub-label {
  font-size: 0.82rem;
  color: var(--text-subtle);
  font-weight: 700;
  margin: 0.85rem 0 0.4rem;
}
.sub-label:first-child {
  margin-top: 0;
}

/* Info tooltip anchor + popover for a sub-label (same idiom as .brand-info). */
.sub-info {
  position: relative;
  display: inline-block;
  margin-left: 0.25rem;
  color: var(--accent);
  cursor: help;
  font-size: 0.75rem;
}
.sub-info:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
.sub-tip {
  display: none;
  position: absolute;
  top: 1.5em;
  left: 0;
  z-index: 50;
  width: 260px;
  max-width: 70vw;
  padding: 0.6rem 0.75rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow: var(--shadow-md);
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.5;
  text-transform: none;
  letter-spacing: normal;
  text-align: left;
}
.sub-info:hover .sub-tip,
.sub-info:focus .sub-tip,
.sub-info:focus-within .sub-tip {
  display: block;
}

/* WEB SEARCH ON/OFF toggle chip: a labelled switch. The track + thumb move on
   the `aria-checked` state that main.js maintains. */
.toggle-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  background: transparent;
  border: 0;
  padding: 0.2rem 0;
  cursor: pointer;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--text);
}
.toggle-track {
  position: relative;
  width: 34px;
  height: 18px;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background-color 0.15s ease;
  flex: none;
}
.toggle-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease;
}
.toggle-chip[aria-checked="true"] .toggle-track {
  background: var(--good);
}
.toggle-chip[aria-checked="true"] .toggle-thumb {
  transform: translateX(16px);
}

/* Document caps note under the file list. */
.docs-caps {
  margin: 0.55rem 0 0;
  font-size: 0.72rem;
  color: var(--text-subtle);
  line-height: 1.4;
}

/*  Shown under the file list when the parsed documents exceed TOTAL_CAP.
 *  Sits in the same size register as `.docs-caps` above so it reads as part
 *  of the same quiet caption area rather than an alert bolted on — but tinted
 *  with the `--bad` pair, because unlike that line this one means content is
 *  about to be LOST rather than merely describing a limit.
 *
 *  `--bad-soft` / `--bad` are theme tokens, so this stays legible in dark mode
 *  without a second rule. The left border does the visual work at this small
 *  size; a full-strength red background would shout louder than a truncation
 *  warning deserves.
 */
.docs-over-cap {
  margin: 0.55rem 0 0;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--bad);
  border-radius: 4px;
  background: var(--bad-soft);
  color: var(--bad);
  font-size: 0.72rem;
  line-height: 1.45;
  font-variant-numeric: tabular-nums;
}

/* === Shell: headline strip =============================================== */
/*  Three large metrics (P_success / P_failure / Change). Metrics-only
    since the RUN PROJECT button moved to the sidebar (2026-07-22 feedback). */

.headline {
  display: flex;
  align-items: flex-start;
  /* flex-start, not space-between: the reset control is meant to read as part
     of the metrics row, sitting just after the Change figure it zeroes, rather
     than as a separate cluster pinned to the far right edge (2026-09-07). */
  justify-content: flex-start;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding: 0.25rem 0 1rem;
}

.headline-metrics {
  display: flex;
  gap: 2.5rem;
  flex-wrap: wrap;
  align-items: flex-start;

  /* The cell's own type metrics, hoisted to the container so the height
     reserved below and the cells that fill it are sized from ONE set of
     numbers. `.headline-metric`, `.headline-label` and `.headline-value` all
     read them back; changing a figure size here moves the reservation with it,
     which is the whole point of them being variables rather than three
     literals repeated in a calc. */
  --headline-label-size: 0.72rem;
  --headline-label-leading: 1.5;
  --headline-figure-size: 2.4rem;
  --headline-figure-leading: 1.1;
  --headline-cell-gap: 0.25rem;

  /* Hold the settled strip's height in EVERY state.

     The strip has two: three labelled figures, and the single "Calculating…"
     message shown between a slider moving and the debounced /score returning.
     The message has no label row, so the strip measured 42.23px against the
     settled 64.23px — and since it sits above the tab bar and all four
     panels, every parameter change bounced the entire page 22px and back.
     A row whose height depends on which of its states is showing is not a
     row; reserving the taller state makes the transition invisible, which is
     what an optimistic in-flight state is for. */
  min-height: calc(
    var(--headline-label-size) * var(--headline-label-leading)
    + var(--headline-cell-gap)
    + var(--headline-figure-size) * var(--headline-figure-leading)
  );
}

/* In flight. The message is bottom-aligned in the height reserved above, which
   is where the figures themselves sit: a settled cell spends its top on the
   label row and hangs the figure off the bottom. Centring the message instead
   put it ~10px higher than the numbers it replaces, so the text moved even
   though the strip did not — a smaller version of the very jump this whole
   rule exists to remove. Both are 2.4rem at the same leading, so aligning the
   boxes aligns the baselines.

   The old rule here dimmed the whole strip to 0.55. That was written when the
   strip still had three figures to grey out; applied to a lone status message
   it only cost contrast, landing it at 3.64:1. The placeholder carries its own
   tone at full opacity instead — see `.headline-placeholder` below for the
   numbers. */
.headline-metrics.calculating {
  align-items: flex-end;
}

/* The reset steps aside while a score is in flight (user request).

   It is a control defined by what it sits next to: its own title explains it
   as discarding an adopted scenario and "restoring the analysis baseline",
   which reads against three figures. Beside a lone "Calculating…" there are no
   figures to restore anything to, and offering to reset a number that is
   being recomputed right now reads as a race the user could lose.

   `visibility`, not `display`. The wrapper is the last item in the headline
   row, so removing it from the flow would move nothing — but `visibility`
   costs no reflow at all, which is the property this whole row has just been
   made to have. It also stops the button taking focus while hidden, which
   `opacity: 0` would not: a control that cannot be seen must not be reachable
   by keyboard.

   Focus ALREADY resting on the button is a separate case, and the browser
   keeps it there rather than dropping it (measured in Chrome). That case is
   reachable — `applyParams` drives Reset as well as Adopt/revert, so pressing
   the control puts the strip into this very state — and retaining focus is the
   better of the two outcomes: the button returns under the user's place a
   moment later, where blurring would have dropped a keyboard user back to the
   top of the document for the sake of a transient state.

   Keyed off the sibling, because `.calculating` lands on the metrics
   container. `headline-calculating.test.mjs` runs the real stylesheet against
   the real class names, so a rename of either fails the build rather than the
   page. */
.headline-metrics.calculating ~ .headline-actions {
  visibility: hidden;
}

/* The in-flight message.

   SIZED FROM THE FIGURES, not from the labels. It stands in for the three
   numbers, so it occupies their scale — `--headline-figure-size` is the same
   property `.headline-value` reads, so the two can never disagree. An earlier
   pass had it at body size, which put it within a few pixels of the 0.72rem
   labels; but a label titles a figure, while this replaces all three of them,
   and matching the smaller of the two things on screen made the strip read as
   though it had emptied rather than as though it were working.

   It is still NOT `.headline-value`. That class is the figure VOICE as well
   as its size — weight 800, monospace, tabular numerals — and that voice is
   for measured quantities. Rendered in it, "Calculating…" read as though the
   model had returned a 277px-wide result. The body face at a lighter weight
   and a secondary tone keeps the scale while making it unmistakably a message
   in the numbers' place rather than a number.

   `--text-subtle` at full opacity is 5.12:1 on the light background and
   6.62:1 on the dark one — clear of 4.5:1, let alone the 3:1 that applies at
   this size. The rule this replaced dimmed the whole strip to 0.55, which
   landed the old message at 3.64:1. */
.headline-placeholder {
  font-size: var(--headline-figure-size);
  line-height: var(--headline-figure-leading);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text-subtle);
}

/* Control cluster at the right end of the headline row (issue #59): today just
   "Reset Changes". Bottom-aligned so the button sits on the baseline of the
   2.4rem figures rather than floating level with their small labels — .headline
   itself is align-items: flex-start for the metrics' sake.

   The wrapper is layout only. It was also the mid-run lock's handle on this
   button — run-indicator.js named `.headline-actions` as an inert region,
   because .headline is a sibling of .tab-panels and so is not covered by the
   regions that lock everything else — until the per-panel lock landed and
   dropped it. Reset is now gated by its own `disabled` attribute alone, so
   that a user watching one section finish while another runs on still has a
   way out of an adoption; tests/reset-changes.test.mjs records why. */
.headline-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  align-self: flex-end;
  /* Tuned for TEXT, not a box: the cluster is bottom-aligned against a 2.4rem
     figure, and bare text needs more lift than a bordered button did to land
     on the figures' baseline rather than under it. */
  padding-bottom: 0.7rem;
}

/* The reset is underlined text, not a button box and not an icon.

   A boxed control here would read as a fourth item in a row of three figures,
   and a bare "↺" asks the reader to decode a symbol next to numbers that are
   already self-describing. Underlined text is the quietest thing that still
   looks activatable, which is the right weight for a control the user reaches
   for only after changing something.

   The underline is the affordance, so it is not decoration and must not be
   removed on hover — hover shifts colour instead. */
.headline-reset-btn {
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--text-subtle);
  text-decoration: underline;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
  cursor: pointer;
}
.headline-reset-btn:hover:not(:disabled) {
  color: var(--accent);
}

/* NO `body.run-locked` RULE HERE, deliberately. This cluster used to dim to
   0.55 mid-run, following the sidebar, because it was inert mid-run and the dim
   said so. It is not inert any more (see the wrapper's comment above), and a
   live control painted at the opacity this app uses for dead ones is a worse
   lie than no signal at all. What the button's availability now looks like is
   its own `disabled` attribute, which the base `button:disabled` rule near the
   top of this file already paints at exactly this 0.55 — so nothing was lost
   for the state the button is genuinely in before a run has landed. */

/* `align-items: center` is what centres each label over its figure: the cell
   is a column flex box, so it shrink-wraps both children and centres them on
   the cross axis. The figure is always the wider of the two, so in practice
   the label centres over the number (2026-09-07). */
.headline-metric {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--headline-cell-gap);
}

/* The label is itself a flex row (text + the ⓘ trigger), so it needs its own
   centring — `align-items` on the parent centres the label BOX, and this
   centres the text and glyph within it once the box is wider than they are. */
.headline-label {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: var(--headline-label-size);
  /* Pinned rather than left at `normal`: the reserved height above multiplies
     this number, so it has to be a number and not a font-dependent guess. */
  line-height: var(--headline-label-leading);
  font-weight: 600;
  color: var(--text-subtle);
}

.headline-value {
  font-size: var(--headline-figure-size);
  font-weight: 800;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  line-height: var(--headline-figure-leading);
}

/* Semantic tones for the headline figures (styling spec 2026-08-03):
   P_success is always --good, P_failure always --bad, and Change is
   toned by its magnitude with a ±0.05 dead-band (> +0.05 → good, < −0.05 →
   bad, otherwise the neutral --text inherited from .headline-value above — so
   a displayed "0.0%" is never coloured).
   Applied by result-strip.js via buildMetric's `tone` argument. --accent is
   reserved for a raw-score figure should one return to the strip. */
.headline-value.tone-good   { color: var(--good); }
.headline-value.tone-bad    { color: var(--bad); }
.headline-value.tone-accent { color: var(--accent); }

/* Inline error rendered into the headline strip when a /score call fails. */
.headline-error {
  color: var(--bad);
  font-size: 0.95rem;
}

/* P_success / P_failure ⓘ tooltip (same idiom as .brand-info). */
.headline-info {
  position: relative;
  display: inline-block;
  margin-left: 0.3rem;
  color: var(--accent);
  cursor: help;
  font-size: 0.75rem;
  font-weight: 700;
  /* The glyph sits INSIDE the label's line box; it does not set it. At the
     default leading its 0.75rem box came to 18px against the label's own
     17.28px, so the two cells that carry a ⓘ were 0.72px taller than the
     Change cell that does not — and, more to the point, taller than the height
     `.headline-metrics` reserves for the in-flight state, which is derived
     from the label's type rather than this glyph's. `.headline-label` is a
     centred flex row, so collapsing the leading here changes where nothing
     sits and what everything measures.

     The cost is that the trigger no longer reaches its own popover — see THE
     ⓘ HOVER BRIDGE, up with `.slider-info`, which carries this selector too. */
  line-height: 1;
}
.headline-info:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
.headline-tip {
  display: none;
  position: absolute;
  top: 1.5em;
  left: 0;
  z-index: 300;
  width: 300px;
  max-width: 80vw;
  padding: 0.75rem 0.9rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.55;
  text-align: left;
  white-space: normal;
  cursor: auto;
}
.headline-info:hover .headline-tip,
.headline-info:focus .headline-tip,
.headline-info:focus-within .headline-tip {
  display: block;
}

/* The primary RUN PROJECT button — the flagship action of the panel. */
.btn-run {
  background: var(--accent);
  color: var(--accent-on);
  font-weight: 800;
  letter-spacing: 0.5px;
  padding: 0.85rem 2.5rem;
  border-radius: 8px;
  font-size: 1rem;

  /* ONE height for both states, so clicking RUN does not resize the button.

     The button says "RUN PROJECT" at rest and carries a two-line run status
     while a run is in flight, and the taller of those two set the size: 3.1rem
     against a resting 45.19px, so the button grew 4.4px on click and the
     sidebar's scrolling body absorbed the difference. That predates the status
     text — the snake it replaced was pinned to the same 3.1rem and grew by the
     same 4.4px — but a flagship control that changes size when you press it is
     wrong either way, so the floor now applies at rest as well.

     Flex rather than relying on the button's default content centring: the
     in-flight state stacks two lines, and declaring the centring once here
     means that state only has to say `flex-direction: column`. */
  min-height: 3.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.btn-run:hover:not(:disabled) {
  background: var(--accent-strong);
}

/* === Shell: tab bar + panels ============================================= */

.tab-bar {
  display: flex;
  border: 1.5px solid var(--border-strong);
  border-radius: 6px;
  overflow: hidden;
}

.tab-btn {
  flex: 1;
  text-align: center;
  padding: 0.7rem 0.5rem;
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text-muted);
  background: var(--surface-muted);
  border: none;
  border-right: 1px solid var(--border);
  border-radius: 0;
  cursor: pointer;
}
.tab-btn:last-child {
  border-right: 0;
}
.tab-btn:not(.active):hover {
  background: var(--surface);
  color: var(--text);
}
.tab-btn.active {
  background: var(--accent);
  color: var(--accent-on);
}

.tab-panels {
  margin-top: 1rem;
}

/* Pre-run empty-state line shown before an analysis run. The RUN PROJECT
   reference inside is bolded but not italic. */
.tab-empty {
  margin: 0.5rem 0;
  font-size: 0.9rem;
  font-style: italic;
  color: var(--text-muted);
}
.tab-empty strong {
  font-style: normal;
}

/* A section's header row on Scenarios and Benchmarks: the SUMMARY heading at
   one end, RE-RUN at the other.

   It reached that shape by subtraction. The "SCENARIOS" / "BENCHMARKS" /
   "PARAMETERS" titles were dropped on 2026-09-07 (the tab bar above already
   names the active tab), taking `.tab-head-title` and its `flex: 1` with them —
   that spacer was what held the controls against the right edge. Issue #117
   then took the "+", leaving RE-RUN alone on a full-width row, which read as a
   button parked in whitespace rather than a control belonging to anything. So
   the Summary heading moved up out of the body to share the line, and the row
   went from `flex-end` to `space-between`: RE-RUN sits exactly where it always
   has, now with something at the other end of the row to be opposite.

   The row survives on those two panels holding one button each: it kept a "+"
   until issue #117 removed hand-authored scenarios and benchmarks. The class
   itself is load-bearing in exactly one place — `tabs.js#applyRunState` selects
   `.tab-head` to hide the row while its section loads, which is the rule
   keeping a header off the top of its own loader. (An earlier comment here
   also claimed `main.js#bindRunButton` reached the panel via
   `btn.closest('.tab-head')`. It does not, and no longer needs to: the runner
   is handed its `tab` name explicitly and `tab-loaders.js#locate` looks the
   panel up by `.tab-panel[data-tab=…]`. The hand-restore that did walk up from
   the button was refactored out — see `tab-loaders.js`'s own header.)
   Parameters has no such row at all — it had no controls left to hold. */
.tab-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
/* Block padding is 0.5rem rather than the 0.35rem this started with: at
   0.72rem type that took the button from roughly 27px to 32px, which reads as
   a control with a box rather than a label with a border, and clears WCAG
   2.5.8's 24px target floor with room to spare. */
.tab-run-btn {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding: 0.5rem 0.9rem;
  border-radius: 6px;
  background: var(--surface-muted);
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
}
.tab-run-btn:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

/* === Browser surfaces ==================================================== */
/*  The parts of the UI nobody draws still carry the design. Selection, the
    caret and the scrollbars ship with browser defaults that belong to no
    design system, and on a dark theme the stock light scrollbar is the single
    loudest un-themed element on the page. All three read from the tokens, so
    they follow a theme swap like everything else.                            */

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

/* Inputs and textareas only — the caret is invisible everywhere else. */
input,
textarea {
  caret-color: var(--accent);
}

/* Firefox + Chromium 121+. `thin` rather than `auto`: the app's two scroll
   containers sit inside a fixed 100vh shell where a stock-width bar eats
   usable column width. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

/* WebKit fallback for the same result. Kept narrow and un-bordered so the
   sidebar and content scrollers read as part of their panel. */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--text-subtle);
}

/* A single consistent keyboard focus ring. Individual components already
   opt out where they must (see .dialog-close-btn), and this never overrides
   an explicit rule — it only supplies one where none existed. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* === Touch-target floor ================================================== */
/*  Several controls are painted deliberately small — the headline reset is
    underlined text, and an icon button sized to a 44px visual box would
    dominate the row it sits in. Both keep their painted size and gain a
    pointer target. Two different fixes, because one blanket rule is unsafe
    here.

    ISOLATED controls get an invisible centred 44x44 pointer target via a
    pseudo-element: the painted size and the layout are untouched, only the
    hit area grows.

    A DESTRUCTIVE control inside a clickable row does NOT. The scenario /
    benchmark × is the last of the pair that once sat side by side (issue #117
    took the ✎), but being alone does not earn it the expander: it lives in a
    card header that is itself a click-to-expand target, so an invisible 44px
    box around DELETE would turn a near-miss aimed at the drawer into a
    deletion. It gets real padding instead, taking it to ~34px — comfortably
    past WCAG 2.5.8's 24px minimum, and honest about the constraint rather
    than pretending to 44.                                                   */

.headline-reset-btn,
.dialog-close-btn {
  position: relative;
}
.headline-reset-btn::after,
.dialog-close-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
/* The dialog ✕ is positioned in its dialog's corner; `relative` above would
   break that, so it keeps `absolute` and the expander still centres on it. */
.dialog-close-btn {
  position: absolute;
}

/* Real padding, no overlay. 0.1rem 0.3rem -> 0.45rem 0.6rem takes a 0.9rem
   glyph from roughly 22x16 to 34x34. */
.scenario-icon-btn,
.benchmark-icon-btn {
  padding: 0.45rem 0.6rem;
}

/* === Shell: responsive =================================================== */
/*  Below ~900px the two-pane grid collapses: the sidebar stacks above the
    content and the whole page scrolls as one document (a fuller polish pass is
    deferred to U5). */

@media (max-width: 900px) {
  .shell-main {
    display: block;
    overflow-y: auto;
  }
  .app-body {
    display: block;
  }
  .sidebar {
    display: block;
    overflow: visible;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }
  .sidebar-scroll {
    /* Single-column mode: the page scrolls as one, so the inner scroll
       container (and the RUN footer pin) stand down. */
    overflow: visible;
  }
  .content {
    overflow: visible;
  }
}

/* ======================================================================== */
/* Define Project modal + sidebar RUN (2026-07-22 feedback)                 */
/*                                                                          */
/* The clarification chat and the project-name input live in the            */
/* #projectDefineModal (auto-opened on first load / New Project); the       */
/* sidebar Question panel shows either the pre-accept placeholder or the    */
/* ACCEPTED QUESTION card. RUN PROJECT moved from the headline strip into   */
/* the sidebar, above the (pre-run greyed) Generate Reports accordion.      */
/* ======================================================================== */

/* All three are themed explicitly (like every other dialog in this file):
   a <dialog> with no background/colour of its own falls back to the UA's
   white card, which reads as broken in dark mode. There is no `color-scheme`
   anywhere in this stylesheet, so that UA fallback is white in BOTH themes —
   which is how #confirmDeleteModal shipped unreadable for a moment: its body
   carries .modal-subtitle (--text-muted, #cbd5e0), roughly 1.5:1 on white. */
#projectDefineModal,
#confirmReacceptModal,
#confirmDeleteModal {
  border: 1px solid var(--border-strong);
  padding: 1.25rem 1.5rem;
  background: var(--surface);
  color: var(--text);
  margin: auto;
}
#projectDefineModal h2,
#confirmReacceptModal h2 {
  margin: 0 0 0.25rem;
  font-size: 1.15rem;
  color: var(--accent-strong);
}

/* The delete confirmation's title deliberately does NOT take --accent-strong,
   unlike every other dialog title above. Two reasons, and the second is the
   real one:

   1. The Earned Colour Rule — colour is spent where it carries a decision, and
      a title reports rather than decides. The destructive colour goes one line
      down, on the sentence that actually costs something.
   2. The accent marks what is ACTIVE OR ADOPTED in this system — it is the
      adopted card's inset ring. Painting a "Delete this scenario?" heading in
      the adoption colour, on a dialog whose own body may say the adoption is
      about to be cleared, is a semantic collision.

   This follows .privacy-confirm-dialog h3, the app's other destructive
   confirmation, which also leaves its heading in Record Ink. */
#confirmDeleteModal h2 {
  /* 1rem, the same gap the footer keeps above the buttons — the two are a pair
     and must stay equal. The dialog reads as three bands (question, answer,
     decision) separated by one interval rather than two different ones. Still
     four times the 0.25rem the app's other dialog titles use, which is right: a
     destructive confirmation is read, not skimmed. */
  margin: 0 0 1rem;
  font-size: 1.15rem;
}

/* The three body lines share a tighter rhythm than .modal-subtitle's 1rem tail,
   so they read as one block with a hierarchy rather than three paragraphs. */
.confirm-delete-line {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
  line-height: 1.45;
  /* Carried here rather than by also putting `.modal-subtitle` on the element:
     the two classes set the same three properties and this one only won by
     source order, which is the fragile arrangement the top-bar rules warn
     about. `.confirm-delete-warning` / `-aside` / `-name` override it. */
  color: var(--text-muted);
  /* The card title is ellipsised, so this dialog is the first place an entry's
     name renders unconstrained — an imported project can carry one long
     unbroken token. Same remedy as `.source-chip` uses for long URLs. */
  overflow-wrap: anywhere;
}

/* The one line the user must not skim — same treatment, and the same reasoning,
   as .privacy-confirm-warning. */
.confirm-delete-warning {
  color: var(--bad);
  font-weight: 600;
}

/* The entry's name inside the otherwise-muted identity sentence. The line
   carries .modal-subtitle (--text-muted) because it is prose; the name is not
   prose, it is the answer to "did I hit the right ×?", so it sits at full
   --text strength while the words around it stay back. */
.confirm-delete-name {
  color: var(--text);
}

/* The adoption consequence is real but secondary: it recedes so the
   irreversibility above it stays the loudest thing in the dialog. */
.confirm-delete-aside {
  color: var(--text-subtle);
}

/* The define modal is wider than the settings dialogs so the chat has room.
   Scaled up 25% on 2026-07-28 (was 680px) — this is the app's main authoring
   surface and the chat wants the space. The 92vw cap keeps it from overflowing
   narrow viewports. */
#projectDefineModal {
  width: min(850px, 92vw);
}

/* ---- Shared corner ✕ dismiss button ------------------------------------ */
/*
   Every dialog dismisses from a ✕ in its top-right corner rather than a
   "Close"/"Cancel" button in the footer (user feedback, 2026-07-28). Footer
   buttons that only dismissed were removed; footers now carry actions only.
   The one exception is #confirmReacceptModal, a destructive confirmation
   where an explicitly labelled Cancel beside "Clear and continue" is worth
   keeping — there the ✕ is a second route to the same cancel.

   No `position: relative` on the dialogs: the UA stylesheet already gives
   `dialog:modal` `position: fixed` (and a plain open dialog `position:
   absolute`), either of which makes it the containing block for this
   absolutely positioned child. Overriding to `relative` would defeat the
   `inset: 0` + `margin: auto` centering these dialogs rely on.

   Positioned inside each dialog's existing padding, so it costs no extra
   height or padding — it just occupies corner whitespace that was already
   there. Visual weight matches the ✎ affordance on the sidebar's accepted
   card.
*/
.dialog-close-btn {
  position: absolute;
  top: 0.55rem;
  right: 0.6rem;
  width: 1.9rem;
  height: 1.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--text-subtle);
  background: transparent;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
.dialog-close-btn:hover {
  color: var(--accent);
  background: var(--accent-soft);
}

/* Focus handling. `app/modals/dialog-focus.js` is what actually keeps the ✕
   from being the target when a dialog opens; these two rules are the backstop,
   so a stray programmatic focus can never draw a ring around the close button
   (user feedback, 2026-07-28 — "should not be highlighted, ever"). Keyboard
   users still get a clear indicator: :focus-visible only matches genuine
   keyboard navigation. */
.dialog-close-btn:focus {
  outline: none;
}
.dialog-close-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The dialog shell receives initial focus (see dialog-focus.js) and is never
   tab-reachable, so it should never render a ring of its own. */
dialog:focus {
  outline: none;
}

/* Reserve the corner so a long title can never run under the ✕. Applies to
   every dialog carrying one. */
#projectDefineModal h2,
#monteCarloModal h2,
#modelClarificationModal h2,
#confirmReacceptModal h2,
#confirmDeleteModal h2,
#termsModal h2 {
  padding-right: 2rem;
}
/* Both are one-question confirmations, so they take the same narrow width —
   without it `width: fit-content` stretches a single body sentence across a
   wide viewport. */
#confirmReacceptModal,
#confirmDeleteModal {
  width: min(460px, 92vw);
}

/* Name field: label on its own line above the input (feedback 2026-07-22).
   The name is mandatory — the chat can't be used until it's filled. */
.define-name-row {
  margin: 1rem 0;
}
.define-name-row label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
}
.define-name-row input {
  width: 100%;
}
/* Required marker + the hint shown while the field is empty. */
.define-name-req {
  color: var(--bad);
  margin-left: 0.15rem;
}
.define-name-hint {
  margin: 0.35rem 0 0;
  font-size: 0.75rem;
  color: var(--text-subtle);
}
.define-name-hint.is-required {
  color: var(--bad);
}

/* The chat inside the modal: give the log real presence (the modal IS the
   authoring surface now) while keeping it scrollable on short viewports. */
.define-chat {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
/* Scaled up 25% alongside the modal width (was 260px / 45vh, 2026-07-28) so the
   dialog grows in both dimensions rather than just getting wider. The vh cap
   still bounds it on short viewports; past that the log scrolls as before. */
.define-chat .clarify-chat-log {
  min-height: 325px;
  max-height: 56vh;
}

/* Sidebar pre-accept placeholder: hint + full-width "Define project". */
.question-placeholder {
  padding: 0.85rem;
}
.question-placeholder-hint {
  margin: 0 0 0.75rem;
  font-size: 0.83rem;
  line-height: 1.5;
  color: var(--text-muted);
}
.question-placeholder .btn-primary {
  display: block;
  width: 100%;
}

/* Project name on the ACCEPTED QUESTION card (above the brief). */
.clarify-accepted-name {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 0.35rem;
  padding-right: 1.8rem; /* clear the ✎ button */
  overflow-wrap: anywhere;
}

/* Sidebar RUN PROJECT block: pinned footer of the sidebar flex column
   (2026-08-03) — the flagship action goes full-width and never scrolls
   off screen. */
.sidebar-run {
  flex: none;
  margin: 0;
  padding: 0.85rem 1.1rem 1rem;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.sidebar-run .btn-run {
  /* `display` is deliberately NOT set here: the base rule makes this a centred
     flex box, and `block` would take that away and put the in-flight state's
     two lines back on the button's own default alignment. */
  width: 100%;
}
.sidebar-run .error {
  margin: 0.5rem 0 0;
}

/* === Snake loading indicator (app/snake-loader.js) ======================= */
/*  Each of the four tab panels animates a wandering snake in its loading
    field while that tab's result is outstanding (app/tab-loaders.js). The
    field hosts an absolutely-positioned <canvas>, so it needs a positioning
    context and a clip. `--snake-ink` is the only thing the renderer reads out
    of CSS.

    The sidebar RUN PROJECT button used to host one too. It no longer does:
    since issue #82 the button carries the run's phase and elapsed time as
    text, and the snake it used to hold is what now marks the tabs whose
    content has not arrived yet.                                             */

.snake-canvas {
  position: absolute;
  inset: 0;
  display: block;
}

/* In-flight state (run-indicator.js). Full opacity despite `disabled` — this
   is "working", not the greyed-out gated state — with a progress cursor
   signalling the interaction block. The height comes from the shared
   `min-height` on `.btn-run`, so this state does not resize the button.

   The two lines stack: the phase name in the button's own weight, and a
   quieter meta line under it carrying the step count and the clock. */
.btn-run.running:disabled {
  opacity: 1;
  cursor: progress;
  /* Both axes are re-set, and both for the same reason: the resting padding
     is sized around a single short word.

     Horizontally, 2.5rem a side leaves too little for "Scenarios and
     benchmarks" (measured at 207px against a 320px content box here, so it
     fits on one line — at the resting padding it would not).

     Vertically, 0.85rem a side plus TWO lines of text comes to 61px, which
     would push past the shared 3.1rem floor and resize the button on click.
     Trimmed to 0.3rem, the two lines sit inside that floor, so the button
     measures the same pressed as it does at rest. */
  padding: 0.3rem 0.75rem;
  /* The base rule already centres on both axes; only the stacking differs. */
  flex-direction: column;
  gap: 0.1rem;
  /* The phase names are ordinary prose, not a shouted action word, so the
     resting button's wide tracking works against them. */
  letter-spacing: 0.2px;
  line-height: 1.15;
}

/* The phase name. Slightly smaller than the resting "RUN PROJECT" so the
   longest label ("Scenarios and benchmarks") fits the sidebar's width without
   wrapping mid-run, which would move the meta line under it. */
.btn-run .run-phase {
  font-size: 0.94rem;
}

/* Step count + elapsed clock. `tabular-nums` is load-bearing rather than
   decorative: the clock repaints every second, and proportional digits make
   the whole line shuffle sideways on each tick.

   Its subordination to the phase line comes from size and weight alone
   (0.76rem/600 against 0.94rem/inherited 800), not from a dim. It carried
   `opacity: 0.78` until this was measured: at 12.16px the small-text bar is
   4.5:1, and 0.78 on the light accent fill gives 3.99:1. The first opacity
   that clears the bar is 0.88 — close enough to opaque to be doing no visual
   work at all, so the value is gone rather than tuned. Full opacity reads
   5.42:1 light and 8.79:1 dark. */
.btn-run .run-meta {
  font-size: 0.76rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The tab loading field, shown only while that section is running. Sits where
   the old status line did, left-aligned under the tab header.

   280px is deliberate rather than arbitrary: it is about a quarter of the tab
   panel on a typical window, so the field reads as an object sitting under the
   header rather than a band spanning it, while staying long enough for the
   snake to read as travelling rather than pacing on the spot. `max-width`
   keeps it inside the panel on a narrow window. The 32px height is half the
   original, which keeps the same flat, wide proportion as the RUN PROJECT
   button it echoes. */
.snake-field {
  position: relative;
  width: 280px;
  max-width: 100%;
  height: 32px;
  margin: 0.6rem 0 0;
  /* No border or radius: the snake is the whole indicator, and a frame around
     it read as a component that had failed to fill. The box still bounds the
     movement — the engine is told the field's dimensions — it just isn't
     drawn. `overflow` is belt-and-braces for the canvas. */
  overflow: hidden;
  --snake-ink: var(--accent);
}

/* Reduced motion: snake-loader.js draws the trail once and never animates it.
   A slow opacity fade keeps the field reading as "working" without anything
   changing position, which is the part that causes trouble. */
@media (prefers-reduced-motion: reduce) {
  .snake-canvas {
    animation: snake-breathe 2s ease-in-out infinite;
  }
}

@keyframes snake-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Input lock while a run is in flight (run-indicator.js puts `run-locked` on
   <body> and makes the input regions inert). This class is the run-WIDE half:
   it is on for any run at all, which is why the sidebar — the one region that
   is also locked for any run — is the only thing it dims, to signal that its
   controls are dead.

   The tab panels stay at full opacity, because they hold results the user is
   reading, and their controls are dead via `inert` rather than greyed. They
   also could not use this class even if they wanted to: since the lock became
   per panel, WHICH tab bodies are inert depends on what is running (a lone
   scenarios re-run locks scenarios and parameters and leaves benchmarks and
   the summary live), and a body-level class cannot express that. The cut is
   the tab bodies rather than the whole panel (issue #120) so the `.tab-head`
   rows above them, and the RE-RUN button each one carries, stay live while
   another section runs. */
body.run-locked .sidebar-scroll {
  opacity: 0.55;
}

/* A disabled accordion head (Generate Reports pre-run): visibly muted and
   inert — sidebar.js's toggle never fires on a disabled button. */
.accordion-head:disabled {
  color: var(--text-subtle);
  cursor: not-allowed;
}
.accordion-head:disabled .chev {
  opacity: 0.4;
}

/* ===========================================================================
   Report preview overlay (app/report.js)
   ===========================================================================

   The generated report used to open as a same-origin `blob:` tab, which gave
   anything inside it read access to the BYOK key in localStorage
   (F-D3-REPORT-XSS). It now renders into a sandboxed iframe inside this
   near-fullscreen dialog: scripting off, opaque origin, no way back into the
   app. The dialog is built in JS on demand, so these rules are keyed off the
   `.report-preview` class rather than an id in index.html.
*/
.report-preview {
  /* Themed explicitly like the app's other dialogs. It went without for as long
     as its children covered every pixel, but the shared `dialog` rule now gives
     it a radius and a shadow, and the UA's default `border: solid` (medium
     currentColor, ~3px) reads as a thick pale frame around the new corner. */
  border: 1px solid var(--border-strong);
  background: var(--surface);
  width: min(1180px, 96vw);
  height: 92vh;
  max-width: none;
  max-height: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Toolbar: the controls that used to be inline `onclick` buttons inside the
   report document itself. They live out here, in the trusted parent, so the
   report can be rendered with scripting disabled entirely. */
.report-preview-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.65rem 3rem 0.65rem 1rem; /* right padding clears the ✕ */
  border-bottom: 1px solid var(--border);
  background: var(--surface-2, var(--surface));
}
.report-preview-title {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text-subtle);
  margin-right: 0.3rem;
}
.report-preview-hint {
  font-size: 0.75rem;
  color: var(--text-subtle);
}
/* The shared corner-✕ rules position against the dialog box; nudge it to sit
   centred in the toolbar strip rather than over the report content. */
.report-preview-close {
  top: 0.5rem;
  right: 0.5rem;
}

/* The isolated frame fills whatever the toolbar leaves. White background
   because the report is a print artefact with hardcoded light colours and is
   deliberately not themed. */
.report-preview-frame {
  flex: 1 1 auto;
  width: 100%;
  border: 0;
  background: #fff;
}

/* ----- privacy controls (Terms footer + Clerk account modal) -------------- */
/* Rendered by `app/privacy.js` into both surfaces, so these rules have to hold
   inside our own Terms dialog AND inside Clerk's account modal, whose own
   styles we don't control. Everything is scoped under .privacy-controls and
   uses the app palette rather than inherited colours, so the panel reads the
   same in either host and in both themes. */
.privacy-controls {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  color: var(--text);
}

/* Page variant (`asPage`) — the account-modal surface. Sections supply their
   own spacing here, so the column gap that separates the inline control from
   its status line would only double up. */
.privacy-page {
  gap: 0;
}

/* Matched declaration-for-declaration against the computed style of Clerk's own
   page header (Profile / Security), so the Privacy tab's title is the same type
   rather than an approximation of it.

   The `--clerk-*` custom properties are read rather than their values inlined:
   the panel is a plain DOM node inside Clerk's shell, not an iframe, so those
   properties inherit down to us and the title tracks Clerk's scale if it ever
   shifts. The fallbacks are what Clerk itself falls back to, which is also what
   the dev-bypass dialog gets — there is no Clerk shell there to inherit from,
   and 0.8125rem × 17/13 resolves to the same 1.0625rem either way.

   `font-family: inherit` for the same reason: inheriting means this is literally
   Clerk's face, not a near-match. `color` resolves to rgb(231, 236, 243) in
   dark, exactly Clerk's own — `clerkAppearance()` feeds it our `--text`, so the
   mapping round-trips. */
.privacy-page-title {
  box-sizing: border-box;
  margin: 0 0 var(--clerk-spacing, 1rem);
  padding: 0;
  font-family: inherit;
  font-size: var(--clerk-font-size-lg, calc(var(--clerk-font-size, 0.8125rem) * 17 / 13));
  font-weight: var(--clerk-font-weight-bold, 700);
  line-height: 1.41176;
  letter-spacing: normal;
  color: var(--text);
}

/* Clerk's header divider: a hairline carrying the same spacing below it as the
   title carries above it. */
.privacy-page-divider {
  height: 1px;
  margin: 0 0 var(--clerk-spacing, 1rem);
  border: 0;
  background: var(--border);
}

.privacy-section-title {
  margin: 0 0 0.85rem;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}

/* The shared row shape for both surfaces: copy on the left, control hard right,
   vertically centred against however many lines the copy runs to. */
.privacy-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* `min-width: 0` is what actually lets the copy wrap: without it a flex item
   refuses to shrink below its longest unbreakable run, so a long subtitle would
   push the control off the right edge instead of running to a second line. */
.privacy-item-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* Never shrink the control — the copy beside it absorbs the width instead. */
.privacy-item-control {
  flex: 0 0 auto;
}

.privacy-toggle-row {
  cursor: pointer;
}

/* The checkbox is repainted as a sliding switch: `appearance: none` strips the
   native tick box, the element itself becomes the track, and ::after is the
   knob. Purely presentational — it stays a real <input type="checkbox">, so
   the label wrapper, keyboard toggling and the `checked` reads in privacy.js
   all keep working untouched. Colours come from the palette vars so the switch
   inverts correctly in dark mode and inside Clerk's modal. */
.privacy-toggle-row input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  position: relative;
  width: 2.75rem;
  height: 1.5rem;
  margin: 0;
  border: 0;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background-color 0.15s;
  cursor: pointer;
}

.privacy-toggle-row input[type="checkbox"]::after {
  content: '';
  position: absolute;
  top: 0.1875rem;
  left: 0.1875rem;
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 50%;
  background: var(--surface);
  transition: transform 0.15s;
}

.privacy-toggle-row input[type="checkbox"]:checked {
  background: var(--accent);
}

/* Track 44px − knob 18px − 3px inset either side = 20px of travel. */
.privacy-toggle-row input[type="checkbox"]:checked::after {
  transform: translateX(1.25rem);
}

.privacy-toggle-row input[type="checkbox"]:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* `appearance: none` also drops the native focus ring, so put one back. */
.privacy-toggle-row input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Three weights, descending: section headings are the heaviest (700, set
   below), an item's own title is bold but lighter (600), and its subtitle is
   plain (400) and grey. That ordering is what keeps a settings page scannable —
   the eye finds the sections first, then the items, then the explanations. */
.privacy-item-text strong,
.privacy-item-text h3 {
  display: block;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
}

/* `--text-subtle`, not `--text-muted`: muted is only a shade off body text and
   barely reads as secondary against Clerk's dark card, which is what put the
   item title and its explanation on the same visual level. Subtle is the
   palette's actual grey and it flips per theme, so this stays legible in both
   rather than being one fixed grey that only suits one of them. */
.privacy-toggle-sub,
.privacy-item-text p {
  display: block;
  margin: 0.15rem 0 0;
  color: var(--text-subtle);
  font-size: 0.85rem;
  font-weight: 400;
}

/* Reserves its line height while empty so showing a status message doesn't
   shift the controls underneath it. */
.privacy-status {
  color: var(--text-muted);
  font-size: 0.8rem;
  margin: 0;
  min-height: 1.2em;
}

/* No rule between the sections: once each carries its own heading, a divider
   as well is redundant — the headings do the separating. The only divider on
   the page is the one under the "Privacy" title, matching Clerk's own pages. */
.privacy-section + .privacy-section {
  margin-top: 2rem;
}

/* Confirmation for Clear History — the app's only irreversible action, so it
   gets a modal rather than an inline row: `showModal()` puts it in the browser's
   top layer, which means it renders above Clerk's account modal even though it
   lives inside our panel in the DOM, and it cannot be clicked past.

   Styled here rather than by id like the app's other dialogs because this one is
   created in JS per render and has no stable id. */
.privacy-confirm-dialog {
  max-width: 460px;
  width: 90%;
  margin: auto;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--border-strong);
  color: var(--text);
  background: var(--surface);
}

.privacy-confirm-dialog h3 {
  /* Twice the 0.6rem it previously used. NOT the same as the delete
     confirmation's title gap, which is 1rem (4x the 0.25rem the app's other
     dialog titles use) — the two destructive dialogs share the footer
     treatment below, not this measure. */
  margin: 0 0 1.2rem;
  font-size: 1.05rem;
  font-weight: 700;
}

.privacy-confirm-dialog p {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
  line-height: 1.45;
  /* Muted, matching `.confirm-delete-line`: the two destructive dialogs share a
     body hierarchy — the consequence loud in Adverse Red, the explanation in
     muted prose beneath it, the surviving-data note quieter still. */
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* The one line the user must not skim. Carries the destructive colour rather
   than being merely bold, so it reads as a warning and not as emphasis. */
/* Both are scoped through the dialog rather than left as bare classes. The rule
   above is `.privacy-confirm-dialog p` (0,1,1); a lone `.privacy-confirm-warning`
   is (0,1,0) and would LOSE to it now that it carries a colour, silently muting
   the one line that must not be. */
.privacy-confirm-dialog .privacy-confirm-warning {
  color: var(--bad);
  font-weight: 600;
}

.privacy-confirm-dialog .privacy-confirm-kept {
  color: var(--text-subtle);
}

/* No reserved height here, unlike the panel's status line: this one appears
   inside a dialog that is already sized to its content. */
.privacy-confirm-error {
  min-height: 0;
  color: var(--bad);
}

.privacy-confirm-error:empty {
  display: none;
}

/* Terms footer: the consent row spanning the footer's width, with Accept on a
   second row beneath it. Overrides the shared .modal-footer's single
   right-aligned row — the toggle is part of the consent act, so it reads as
   something to settle immediately before pressing Accept rather than as a
   control competing with it across the footer. In read-only mode `terms.js`
   hides both and the footer collapses entirely; the ✕ is the only affordance
   there. */
.terms-footer {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0.85rem;
}

/* Accept keeps the footer's usual right-hand position, directly under the
   switch. */
.terms-footer > button {
  align-self: flex-end;
}

/* Full width here, unlike the account-modal surface: the row's whole point is
   that the copy sits at the left edge and the switch at the right, above the
   button. `flex: 0 0 auto` because the footer is a column now, where a grow
   factor would stretch this block vertically instead of horizontally. */
.terms-privacy {
  flex: 0 0 auto;
  max-width: none;
}

/* Copy left, switch right is now the shared `.privacy-item` shape, so there is
   nothing to override here — both surfaces want the same arrangement.

   In the Terms footer the consent copy is set in the Accept button's own type
   — same family and size — so the two sides of the footer read as one row. The
   only difference between the two lines is weight: the label carries the
   button's 600, the subtitle drops to normal. This is deliberately scoped to
   the Terms surface; the same control inside Clerk's account modal keeps its
   quieter, smaller subtitle, where it sits in a settings list rather than
   beside a button. */
.terms-privacy .privacy-toggle-row strong,
.terms-privacy .privacy-toggle-sub {
  font-family: inherit;
  font-size: 0.85rem;
}

.terms-privacy .privacy-toggle-row strong {
  font-weight: bold;
}

.terms-privacy .privacy-toggle-sub {
  font-weight: 400;
  color: grey;
}

/* The reserved status line exists to stop messages shoving the Clear History
   block around; in the footer there is no such block, so the reserved height —
   plus the column gap above it — only opens a dead band between the toggle and
   the Accept button below. Collapse both while it is empty and give the
   message its own spacing only when there is one to show. */
.terms-privacy .privacy-controls {
  gap: 0;
}

.terms-privacy .privacy-status {
  min-height: 0;
}

.terms-privacy .privacy-status:not(:empty) {
  margin-top: 0.35rem;
}

/* === Responsive: top bar (must stay last) ================================ */
/*  MUST REMAIN AT THE END OF THIS FILE. These override `.topbar`,
    `.topbar-brand`, `.brand-wordmark` and `.brand-beta`, all of which are
    declared further down than the other `@media (max-width: 600px)` block
    near the top. Same specificity means source order decides, so an override
    placed with the other mobile rules silently loses — it did, on the first
    attempt at this fix.

    The problem: at 375px the top bar needed 557px of content (40 padding +
    193 wordmark + 124 product + 47 BETA + 24 gaps + 112 actions + 16 gap) in a
    non-wrapping flex row, so the document scrolled horizontally on every page.
    Found by measuring a real render; the arithmetic is invisible to a source
    read.

    Three concessions, smallest first: tighter padding and gaps, brand type
    from 1.56rem down to 0.95rem, and the BETA chip hidden. BETA is the only
    element actually removed, and it goes last because of the three brand
    pieces it carries the least — the wordmark is the identity, P_success is
    the product, and "beta" is restated in the Terms and throughout the
    product copy. Delete the final rule to restore it.                       */
@media (max-width: 600px) {
  .topbar {
    padding: 0 0.75rem;
    gap: 0.5rem;
  }
  .topbar-brand {
    gap: 0.35rem;
    min-width: 0;
  }
  .brand-wordmark,
  .brand-product {
    font-size: 0.95rem;
  }
  .brand-beta {
    display: none;
  }
}
