/* console.css — UI overhaul, Phases 1-4 (tokens, typography, layout, reset,
   core components, tables, navigation).

   LOADED AFTER `app.css`, ON PURPOSE. `app.css` is the domain palette: the
   confusion-matrix severity ramp, the arm-shading gradient, the tripwire
   chip, the status pill's ten-way vocabulary (asserted by name in
   `tests/test_web_screens.py::test_EVERY_STATUS_IN_BOTH_VOCABULARIES_...`,
   which reads `app.css` specifically). None of that moves here and none of
   it is restated here. This file is the generic layer underneath it: the
   tokens, the type scale, the buttons, the form fields, the alert shape and
   the table/nav polish that apply everywhere BUT never override a rule that
   prices a domain fact in colour. Every selector below is either a plain
   element (lower specificity than app.css's classed and attributed
   selectors, so it can never shadow them) or a NEW class this file alone
   owns (`.btn`, `.alert`, `.container`, `.table-wrap`).

   No inline color literal anywhere below; every colour is one of the tokens
   in the block immediately following this comment. */

:root {
  --color-bg: #ffffff;
  --color-surface: #f8f9fa;
  --color-border: #e2e4e8;
  --color-text: #1a1a2e;
  --color-text-muted: #6b7280;
  --color-accent: #2563eb;
  --color-accent-hover: #1d4ed8;
  --color-success: #16a34a;
  --color-warning: #d97706;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;
  --color-running: #2563eb;
  --color-badge-control: #6b7280;
  --color-badge-experimental: #d97706;
  --color-badge-holdout: #7c3aed;
  /* 10C: `--color-success` and `--color-warning` are correct where they are
     used as a BACKGROUND or a BORDER (badges, alerts -- WCAG 1.4.11's 3:1
     non-text minimum, which both clear against white and against each
     other). Neither clears 1.4.3's 4.5:1 TEXT minimum on a white page
     (measured: success 3.30:1, warning 3.19:1) -- the one place either was
     used as small text colour (`.verdict-arrow`) gets its own darker,
     text-safe variant instead of a second meaning for the token everything
     else already keys its background/border on. */
  --color-success-text: #15803d;
  --color-warning-text: #b45309;
}

/* ---------------------------------------------------------------------- *
 * 1. Foundation: reset, layout, typography
 * ---------------------------------------------------------------------- */

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

/* A minimal reset -- margins only, and only on the elements the brief names.
   Every element here gets its REAL spacing restated in the same rule (never
   zeroed and left bare), so nothing above this line and nothing below it
   fights over the same property. */
h1, h2, h3, h4, h5, h6, p, ul, ol, table { margin: 0; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-bg);
}

.container {
  max-width: 1280px;
  margin-inline: auto;
  padding-inline: 24px;
}

/* `main` already carries the vertical padding app.css set; `.container`
   only ever constrains width, so the two compose instead of duplicating
   each other's job. */
main.container { padding-top: 1rem; padding-bottom: 4rem; }

/* The vertical rhythm scale (8/16/24/32/48), applied where app.css did not
   already give a section its own margin. `section` covers every named panel
   on run detail and experiment detail without touching the panels' own
   internal spacing rules in app.css. */
main.container > section,
main.container > .decision-panel,
main.container > details.prereg,
main.container > .progress-pane { margin-bottom: 32px; }
main.container > h2 { margin-top: 32px; margin-bottom: 16px; }

h1 { font-size: 1.5rem; font-weight: 700; margin: 0.4rem 0 1rem; }
h2 { font-size: 1.25rem; font-weight: 600; margin: 1.6rem 0 0.5rem; }
h3 { font-size: 1rem; font-weight: 600; margin: 1.2rem 0 0.4rem; }

.text-sm { font-size: 0.8125rem; color: var(--color-text-muted); }

/* Technical identifiers: architecture names, config hashes, run slugs.
   `app.css`'s own `.hash` and `code` already carry a monospace stack; this
   only widens the family list and fixes the relative size the brief asks
   for, so neither rule needs to name the other. */
code, .hash, .mono {
  font-family: "SF Mono", "Cascadia Code", "Fira Code", ui-monospace, Menlo,
    monospace;
  font-size: 0.875em;
}

/* ---------------------------------------------------------------------- *
 * 2A. Buttons
 * ---------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 8px 16px;
  border-radius: 4px;
  border: 1px solid transparent;
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  line-height: 1.2;
}

.btn-primary {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}
.btn-primary:hover:not(:disabled) { background: var(--color-accent-hover);
  border-color: var(--color-accent-hover); }

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

.btn-danger {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: #fff;
}
.btn-danger:hover:not(:disabled) { background: var(--color-danger-hover);
  border-color: var(--color-danger-hover); }

.btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* THE GREY-WITH-TITLE-HOVER RULE (FRONTEND.md #7). Every disabled control in
   this console already carries `disabled` plus a `title` naming the reason
   (`data-locked-reason`, the pipeline delete/set-default guards, the run
   pause/resume/stop guards) -- this is the one place that reason becomes
   visibly grey instead of looking identical to an enabled control.

   **Re-audited 2026-09-23** (`docs/runs-pages-re-audit.md` #1): `opacity: 0.5`
   alone left `.btn-primary`/`.btn-danger`'s own blue/red `background` showing
   through at half strength, which still reads as "a live button, slightly
   faded" rather than "cannot succeed right now" -- a run detail page for a
   `succeeded` run showed Start (blue) and Stop (red) looking clickable. The
   background/border/colour are overridden here to the same neutral grey
   regardless of which `.btn-*` variant is disabled, `pointer-events: none`
   is added so a disabled control cannot even receive the click a `title`
   alone would still let through, and the opacity drops to 0.4 to read as
   OFF rather than dimmed. Specificity: two classes/attribute selectors beats
   `.btn-primary`/`.btn-secondary`/`.btn-danger`'s one, so this always wins
   over whichever variant it lands on. */
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------- *
 * 2B. Badges — a visual refinement of the EXISTING `.pill`, `.badge` and
 * `.track` components, never a second vocabulary beside them. `app.css` is
 * still the one file naming a colour per status word (G80's own guard reads
 * it by name); this only adds the colour-blind-safe dot every variant of
 * those components was missing.
 * ---------------------------------------------------------------------- */

.pill, .badge, .track {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.pill::before, .track::before {
  content: "\25CF"; /* ● — a second, non-colour channel on every status/track
    mark, so a status is never carried by hue alone. */
  font-size: 0.65em;
}

/* ---------------------------------------------------------------------- *
 * 2C. Form inputs
 * ---------------------------------------------------------------------- */

input, select, textarea {
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 1rem;
  font-family: inherit;
  background: var(--color-bg);
  color: var(--color-text);
}
input:focus, select:focus, textarea:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
  outline: none;
}
/* 6C item 3: EDITABLE VS READ-ONLY, on every disabled field everywhere --
   not only the experiment designer, which is the one screen that renders
   every field always and toggles `disabled` on the ones a status locks. A
   disabled field that still looks like an empty box invites a click that the
   server will refuse; dropping its border and background reads as the plain
   text it effectively is, while an EDITABLE field keeps the visible border
   the rule above already gives it. */
input:disabled, select:disabled, textarea:disabled {
  background: transparent;
  border-color: transparent;
  color: var(--color-text-muted);
  cursor: not-allowed;
  padding-left: 0;
  padding-right: 0;
}

label {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--color-text);
}
/* `app.css` already stacks a field's own label above its control
   (`display: flex; flex-direction: column`); this only restates the weight
   and colour the brief asks for, on top of that layout. */
.filters label, .actions label { color: var(--color-text); }

/* RADIO ALIGNMENT (2C). A `<label>` wrapping `<input type="radio">` plus
   text is a ROW, never the column layout a text-field label uses -- the two
   shapes share the tag but not the direction. Scoped to a `<fieldset>`
   because every radio group in this console (`experiments/new.html`'s
   "Kind") already sits inside one with a `<legend>`; this rule turns that
   existing markup into the standard convention without moving a tag. */
/* `min-width: 0` (U2): a fieldset's UA default is `min-content`, so one long
   `<option>` inside it widened the whole fieldset past a 390px viewport --
   the sideways scroll U1 caught on `/runs/new`. */
fieldset { border: 1px solid var(--color-border); border-radius: 4px;
  padding: 12px 16px; margin: 0 0 1rem; min-width: 0; }
fieldset > legend { font-weight: 600; font-size: 0.875rem; padding: 0 4px; }
fieldset label {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  font-weight: 400;
  color: var(--color-text);
}
fieldset label input[type="radio"] { order: -1; }

/* THE STATUS FILTER, as a row of toggle pills (re-audit fix #12, 2026-09-23)
   rather than a `<select multiple>` -- Ctrl+click, and no visual read of what
   is selected without opening it. Reuses the `fieldset`/`legend` shape above;
   this only lays its labels out as a WRAPPING ROW instead of the one-per-line
   stack a bare `fieldset label { display: flex }` would otherwise give a
   group of several. Each pill is the SAME `.filters` fieldset a status filter
   already sits inside -- no new form, no JS, one checkbox per status,
   `name="status"` repeated exactly as `StatusQuery` on the route parses. */
.status-toggle-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 8px 12px;
}
.status-toggle {
  display: inline-flex !important;
  width: auto;
  padding: 4px 10px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 0.8125rem;
  cursor: pointer;
  user-select: none;
}
.status-toggle:has(input:checked) {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}

/* ---------------------------------------------------------------------- *
 * 2D. Alerts — the SAME three panels this console already renders
 * (`.refusal`, `.verdict-ok` / `.verdict-refused`, `.info`, and the
 * `data-locked` disabled-state paragraph on the experiment designer), given
 * the shared shape the brief asks for. Nothing here replaces `app.css`'s own
 * colour choice for those elements; it only adds the tinted background and
 * left border consistently across the three.
 * ---------------------------------------------------------------------- */

.alert, .refusal, .verdict, p.info, p[data-locked] {
  padding: 12px 16px;
  border-radius: 4px;
  border-left: 4px solid var(--color-border);
  font-size: 0.875rem;
}
.alert-info, .verdict-ok { border-color: var(--color-accent);
  background: rgba(37, 99, 235, 0.05); }
.alert-warning, p[data-locked] { border-color: var(--color-warning);
  background: rgba(217, 119, 6, 0.05); }
.alert-danger, .refusal, .verdict-refused { border-color: var(--color-danger);
  background: rgba(220, 38, 38, 0.05); }

/* ---------------------------------------------------------------------- *
 * 2E. Form primitives (U2, U2b) -- the CSS half of `templates/_form.html`.
 *
 * DESKTOP: A LEFT LABEL COLUMN (founder's choice, 2026-09-24, over label-
 * above): labels in a fixed column sized to "Dataset version", so every
 * control starts at the SAME x and the eye scans one edge down the form.
 * Legends, help and errors align with the control column, not the labels.
 * PHONE (<= 640px): label above the control -- a 9rem column beside a
 * 390px viewport leaves the control too little room.
 * WIDTH FITS CONTENT: `.form-control--{xs,s,m,l,full}`, never a seed
 * number stretched across the page. The form is exactly label + gap + `l`
 * wide, so the action row's right edge is the widest control's right edge.
 * `/styleguide` renders every rule below in every state.
 * ---------------------------------------------------------------------- */

:root {
  --form-label-w: 8.5rem;
  --form-gap: 16px;
  --form-w-xs: 5rem;
  --form-w-s: 8rem;
  --form-w-m: 16rem;
  --form-w-l: 26rem;
}

form:has(.form-field), form:has(> .form-actions) {
  max-width: calc(var(--form-label-w) + var(--form-gap) + var(--form-w-l));
}
form.filters { max-width: none; }

.form-field {
  display: grid;
  grid-template-columns: var(--form-label-w) minmax(0, 1fr);
  column-gap: var(--form-gap);
  align-items: baseline;
  margin: 0 0 12px;
  min-width: 0;
}
.form-body { grid-column: 2; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.form-label {
  display: block;
  grid-column: 1;
  /* Centred on the 2.5rem control's first line, not its top edge. */
  align-self: start;
  padding-top: calc((2.5rem - 1.5em) / 2);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--color-text);
  text-align: left;
}
.form-required { color: var(--color-danger); margin-left: 2px; }

.form-control { width: 100%; min-width: 0; }
.form-control--xs { max-width: var(--form-w-xs); }
.form-control--s { max-width: var(--form-w-s); }
.form-control--m { max-width: var(--form-w-m); }
.form-control--l { max-width: var(--form-w-l); }
.form-control--full { max-width: none; }
/* U3b: `app.css`'s pre-macro `[data-panel] input[type=...], select, textarea
   { max-width: 44rem }` (the experiment designer's own old per-screen CSS,
   written for the raw markup it had before this migration) beats the four
   rules above on specificity -- `[data-panel] input[type="number"]` is
   `(0,2,1)`, `.form-control--s` alone is `(0,1,0)`. Found live: "Seeds per
   arm" rendered the full 44rem box on `/experiments/{id}/design` despite
   carrying `form-control--s`. Scoped to `.form-control` rather than editing
   the legacy rule, which un-migrated `[data-panel]` forms
   (`experiments/_arm_form.html`, `_reference_picker.html`) still depend on
   for their own raw controls' width. */
[data-panel] .form-control.form-control--xs { max-width: var(--form-w-xs); }
[data-panel] .form-control.form-control--s { max-width: var(--form-w-s); }
[data-panel] .form-control.form-control--m { max-width: var(--form-w-m); }
[data-panel] .form-control.form-control--l { max-width: var(--form-w-l); }
[data-panel] .form-control.form-control--full { max-width: none; }
/* One control height, so rows share a rhythm and a label centres on it. */
input.form-control, select.form-control { height: 2.5rem; line-height: 1.25; }
/* A locked select reads as plain text: no box (2C above) AND no chevron. */
select.form-control:disabled { appearance: none; }

.form-help, .form-error { margin: 0; font-size: 0.8125rem; }
.form-help { color: var(--color-text-muted); }
.form-error { color: var(--color-danger); font-weight: 500; }
.has-error .form-control { border-color: var(--color-danger); }

/* A read-only row: the value sits where a control would, on its baseline. */
.form-static {
  display: flex;
  align-items: center;
  min-height: 2.5rem;
  color: var(--color-text);
}

/* THE CHECKBOX: the box in the control column, its label text to its right,
   ONE line. Help and error start under the text, not the box. */
.form-check {
  display: inline-flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 8px;
  font-weight: 400;
  font-size: 0.9375rem;
  color: var(--color-text);
  cursor: pointer;
}
.form-check input[type="checkbox"] {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin: 0.2em 0 0;
  padding: 0;
  accent-color: var(--color-accent);
}
.form-field--check .form-help,
.form-field--check .form-error { padding-left: calc(1rem + 8px); }

/* A SECTION of a form: a rule with its legend on the CONTROL column, no box
   -- a boxed fieldset would indent its controls off the shared edge. */
.form-section {
  border: 0;
  border-top: 1px solid var(--color-border);
  border-radius: 0;
  margin: 20px 0 12px;
  padding: 12px 0 0;
}
.form-section > legend {
  margin-left: calc(var(--form-label-w) + var(--form-gap) - 8px);
  padding: 0 8px;
  font-weight: 600;
  font-size: 0.875rem;
}

/* A section INSIDE a section (Preprocessing inside Model) is subordinate:
   a dashed rule and a muted, lighter legend, so it never reads as a sibling
   of the section that contains it. */
/* A section's last field or nested section carries no bottom margin, so a
   section ends at its content and the next one's rule sets the spacing. */
.form-section > :last-child,
.form-section > :last-child > .form-field:last-child,
.form-section > :last-child > .form-section:last-child { margin-bottom: 0; }
.form-section .form-section { border-top-style: dashed; margin-top: 12px; }
.form-section .form-section > legend { font-weight: 500; color: var(--color-text-muted); }

/* THE ACTION ROW: last in the form, one row, right-aligned. The order is
   CSS's, not the template's: primary is always far right, a destructive
   action always far left, separated by the free space between them. */
.form-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 8px 12px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--color-border);
}
.form-actions .btn-primary { order: 10; }
.form-actions .btn-danger { order: -1; margin-right: auto; }

/* A one-button form in a row or an action bar: inline, no margins. Several
   sit in a `<div class="row-actions">` (app.css: a flex row with a gap). */
form.row-action { display: inline-flex; margin: 0; }
.row-actions { flex-wrap: wrap; }

@media (max-width: 640px) {
  /* Label above; the control column becomes the only column. */
  .form-field { grid-template-columns: minmax(0, 1fr); row-gap: 4px; margin-bottom: 16px; }
  .form-label, .form-body { grid-column: 1; }
  .form-label { padding-top: 0; }
  .form-static { min-height: 0; }
  .form-section > legend { margin-left: -8px; }
}

@media (max-width: 480px) {
  /* Buttons share the row and wrap whole; the primary still ends it. */
  .form-actions .btn { flex: 1 1 auto; justify-content: center; }
  .form-actions .btn-danger { margin-right: 0; }
}

/* `inline_select_action` (U3a): a select and its own button on one line in a
   table cell, no label column reserved. ADDITIVE ONLY -- overrides nothing
   `.form-field` already does for any other caller: same class, later rule,
   `display: flex` instead of `display: grid`, so every existing `.form-field`
   (which never carries `--inline`) is untouched. */
.form-field--inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.form-field--inline .form-control { flex: 1 1 auto; max-width: none; }
.form-field--inline .btn { flex: 0 0 auto; }

/* A `.form-actions` row rendered OUTSIDE its own `<form>` (U3a round 3) --
   `pipelines/builder.html`'s Rename/Delete row, whose submit reaches back
   into its `<form>` via `form="..."` because the confirm swap Delete opens
   contains a REAL `<form>` and nesting it inside Rename's would be invalid
   HTML. `form:has(> .form-actions)` (above) only matches an ACTUAL `<form>`
   ancestor, so a `.form-actions` outside one loses the shared column width
   without this. Additive: a new class, not a change to `.form-actions` or
   `form:has(...)` themselves -- every existing form-in-a-`<form>` caller is
   unaffected. */
.form-actions-frame { max-width: calc(var(--form-label-w) + var(--form-gap) + var(--form-w-l)); }

/* `.form-actions .btn-danger { order: -1; margin-right: auto }` (above) only
   repositions a `.btn-danger` that is ITSELF the flex item -- `order` and an
   auto margin have no effect on an element that is not a flex item, and
   `_delete_pipeline_button.html`'s button sits inside its own wrapping
   `<span data-field="delete-pipeline-control">` for its htmx swap target, so
   it is the SPAN that is the flex child here, not the button. Additive: a
   new selector for that one wrapper, not a change to the existing rule
   (which still fires correctly for every OTHER `.form-actions` caller, none
   of which wraps its danger button in a span). */
.form-actions > [data-field="delete-pipeline-control"] { order: -1; margin-right: auto; }

/* ---------------------------------------------------------------------- *
 * 3. Tables
 * ---------------------------------------------------------------------- */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table.grid {
  width: 100%;
  border-collapse: collapse;
}
table.grid th, table.grid td {
  padding: 8px 12px;
}
table.grid tbody tr:nth-child(even) { background: var(--color-surface); }
table.grid tbody tr:hover { background: rgba(37, 99, 235, 0.04); }

table.grid thead th {
  position: sticky;
  top: 0;
  background: var(--color-surface);
  border-bottom: 2px solid var(--color-border);
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  z-index: 1;
}

table.grid td, table.grid th { font-variant-numeric: tabular-nums; }

/* Run-detail redesign (2026-09-23), item 11: every numeric column in the
   epochs table right-aligned, scoped to `.num` rather than to that one table
   so any other `table.grid` gets the same rule for free. */
table.grid td.num, table.grid th.num { text-align: right; }

/* Long-text columns already capped in Python and in `app.css` (`.config`,
   `.decision`) keep their own rule; this is the fallback for a column with
   no such cap, so a long value still clips rather than blowing out the
   table's width. `title` is the template's job wherever this applies. */
table.grid td.truncate {
  max-width: 24rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Empty states (`data-empty` rows, already emitted by every list partial):
   one centred, muted sentence, never the default left-aligned cell. */
tr[data-empty] td {
  text-align: center;
  color: var(--color-text-muted);
  padding: 32px 12px;
}
tr[data-empty] td p { margin: 0 0 0.35rem; }
tr[data-empty] td p:last-child { margin-bottom: 0; }

/* Bare dashes read lighter than a real (if absent) value. */
table.grid .muted, table.grid td > .muted:only-child { color: var(--color-text-muted); }

/* Table links: legible as text first, underlined only on interaction. Plain
   navigation links elsewhere keep the accent colour the brief asks for. */
a { color: var(--color-accent); }
table.grid a { color: var(--color-text); text-decoration: none; font-weight: 500; }
table.grid a:hover { text-decoration: underline; }

/* ---------------------------------------------------------------------- *
 * 4A. Navigation
 * ---------------------------------------------------------------------- */

.topbar {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  padding: 12px 24px;
}
.topbar nav {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.topbar nav a {
  margin-right: 0;
  padding: 4px 8px;
  border-radius: 4px;
  border-bottom: 2px solid transparent;
  text-decoration: none;
}
.topbar nav a:hover { background: var(--color-bg); }
.topbar nav a[aria-current="page"] {
  font-weight: 600;
  color: var(--color-accent);
  border-bottom-color: var(--color-accent);
}
/* Sign-out, pushed to the far edge of the bar. */
.topbar form { margin-left: auto; }

/* The hamburger label (9B). Hidden above 768px (see the responsive section
   below); at and under it, this IS the nav's own toggle button -- no JS, the
   sibling `<input type="checkbox">` it targets carries the open/closed state. */
.nav-toggle {
  display: none;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 4px;
  margin-left: auto;
}
.nav-toggle:hover { background: var(--color-bg); }
.nav-toggle-check:focus-visible ~ .nav-toggle {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------- *
 * 4B / 6. Breadcrumbs -- deferred at cycle A, landing with the
 * page-specific structure it is scoped alongside (`_breadcrumb.html`).
 * ---------------------------------------------------------------------- */

.breadcrumb {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  margin-bottom: 16px;
}
.breadcrumb a { color: var(--color-accent); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--color-text); font-weight: 500; }

/* ---------------------------------------------------------------------- *
 * 5. Error pages
 * ---------------------------------------------------------------------- */

.error-page {
  max-width: 40rem;
  margin: 48px auto;
  text-align: center;
}
.error-page .error-status {
  font-size: 3rem;
  font-weight: 700;
  color: var(--color-text-muted);
  margin-bottom: 8px;
}
.error-page .error-message { margin-bottom: 24px; }
.error-page .error-detail {
  text-align: left;
  background: var(--color-surface);
  border-radius: 4px;
  padding: 12px 16px;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-bottom: 24px;
}

/* ---------------------------------------------------------------------- *
 * 6A. Run detail
 * ---------------------------------------------------------------------- */

/* THE ACTION BAR, redesigned 2026-09-23 (`tasks/reports/2026-09-23-run-detail-
   redesign.md`). One bar, one variant per `run.status`, chosen in
   `detail.html` -- this rule only lays out whichever variant rendered: a
   handful of `<form>`s and bare buttons for `queued`/`reported`/terminal, or
   ONE `<form class="control-form">` for `running` (Pause/Stop or
   Resume/Stop, the checkbox and the note toggle all share it, because
   FRONTEND.md rule 6 -- "a control repeated per row does not repeat its
   inputs per row" -- extends to one run's OWN three actions sharing one Note
   field instead of three). */
.action-bar { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; }
.action-bar form { margin: 0; }
.action-bar form.control-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
/* The vertical divider between the controls that can succeed right now and
   the ones greyed beside them (FRONTEND.md rule 7: greyed, titled, same
   place -- never a second row). */
.action-bar .separator {
  width: 1px;
  align-self: stretch;
  min-height: 1.5em;
  background: var(--color-border);
}
/* The running-state checkbox: FRONTEND.md's terse-prose rule -- the label is
   "Immediately", the cost is a MUTED sublabel beside it, never a sentence. */
.immediate-check {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 0.4em;
  font-weight: 400;
  font-size: 0.875rem;
  color: var(--color-text);
}
/* U3b: `app.css`'s bare `label { display: flex; flex-direction: column }`
   loses the DISPLAY property here (`.immediate-check`'s class beats its type
   selector) but not FLEX-DIRECTION -- cascade resolves per property, and
   this rule never declared one, so the checkbox stacked above "Immediately"
   on both viewports (U1's #2 worst-form defect for `/runs/1`, still present
   after the macro migration because the bug was never in the macro). */
.immediate-check .muted { font-size: 0.8125rem; }

/* THE "RUN AGAIN" PANEL: a checkbox nobody sees drives whether it shows, the
   same CSS-only disclosure `#nav-toggle-check` (10, above) already uses --
   no JS, and `<label for>` toggles it OPEN from the action bar and CLOSED
   from the "Cancel" button inside the panel itself, because a label's click
   always toggles the checkbox it names. */
.relaunch-panel {
  display: none;
  margin-top: -8px;
  margin-bottom: 24px;
  padding: 16px;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-surface);
}
.relaunch-panel form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
  margin: 0;
}
#relaunch-toggle:checked ~ .relaunch-panel { display: block; }

/* ADR-060 D3.1, 60-4. "Replaces this run" lives INSIDE the panel above, so a
   second CSS-only disclosure -- `:has()`, already used for `.status-toggle`
   above -- swaps which of the two submit buttons shows and reveals the Note
   field, all without a script. `#replaces-toggle` is a REAL checkbox this
   time (`_form.html::checkbox`, not a CSS_TOGGLES exemption): its value is
   also what the form submits. */
.relaunch-panel .replaces-note { display: none; width: 100%; }
.relaunch-panel form:has(#replaces-toggle:checked) .replaces-note { display: block; }
.relaunch-panel [data-action="retry-confirm"] { display: none; }
.relaunch-panel form:has(#replaces-toggle:checked) [data-action="relaunch-confirm"] { display: none; }
.relaunch-panel form:has(#replaces-toggle:checked) [data-action="retry-confirm"] { display: inline-block; }

/* THE "MOVE TO NODE" PANEL: the same CSS-only disclosure, its own checkbox
   (`#reassign-toggle`) so checking one panel open never opens the other --
   both carry `.relaunch-panel` for the shared box/border/spacing, and
   `.reassign-panel` scopes ITS toggle rule to itself alone. */
#reassign-toggle:checked ~ .relaunch-panel.reassign-panel { display: block; }

/* THE DETAILS GRID (item 10): label/value pairs, at a glance -- a lighter
   sibling of `.facts`/`table.grid`'s denser rows, for the eight facts an
   operator reads FIRST rather than the full provenance table further down
   the page (unchanged, below it). */
.meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 1px;
  background: var(--color-border);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  overflow: hidden;
}
.meta-item { background: var(--color-bg); padding: 10px 14px; }
.meta-label {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}
.meta-value { font-size: 0.875rem; font-weight: 500; color: var(--color-text); }
.meta-value.mono { font-family: "SF Mono", "Cascadia Code", "Fira Code", ui-monospace, Menlo, monospace;
  font-size: 0.8125rem; }

/* THE PROGRESS HEADER (item 9): "Training", the epoch position and the
   percent, on one line, above the bar -- replacing the native `<progress>`
   cycle B added (removed: the founder wants the fill under full CSS
   control, cross-browser, which a native element does not give). */
.progress-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
}
.progress-label { font-weight: 600; }
.progress-count { font-variant-numeric: tabular-nums; color: var(--color-text-muted); }
/* A paused fit's fill, in the SAME amber `.pill[data-status=paused]` already
   uses (`app.css`) -- a run that needs a person to move it along, without
   `running`'s blue or `failed`'s red. */
.progress-bar.paused { background: #d9a13a; }

/* The epochs table's "last 5, or all" disclosure (6A item 6): a native
   `<details>` around the older rows, so nothing leaves the DOM and a guard
   counting `tr[data-row=epoch]` still counts every one of them. */
details.epoch-history summary {
  cursor: pointer;
  color: var(--color-accent);
  font-size: 0.875rem;
  padding: 4px 0;
}

/* The collapsed Note field, now shared by whichever of Pause/Resume/Stop the
   running-state form submits (item 5), rather than one copy per action. */
.note-disclosure summary {
  cursor: pointer;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

/* The best-epoch row (6A item 6; item 11 adds the visible marker beside the
   epoch number, the background alone having been the founder's own read of
   "highlighted" before the marker existed). */
table.grid tr[data-row="epoch"][data-best="true"] {
  background: rgba(22, 163, 74, 0.08);
}
/* THE PATIENCE REFERENCE row/pill (founder, 2026-10-16): the epoch early stopping counts
   from, distinct from the KEPT epoch above. Amber (--color-warning) against the kept
   epoch's green, so the two are never confused; the same tokens, no new palette. */
table.grid tr[data-row="epoch"][data-patience-ref="true"] {
  background: rgba(217, 119, 6, 0.08);
}
table.grid tr[data-row="epoch"][data-best="true"][data-patience-ref="true"] {
  background: rgba(22, 163, 74, 0.08);
}
.patience-ref-marker {
  display: inline-block;
  font-size: 0.625rem;
  font-weight: 700;
  background: var(--color-bg);
  color: var(--color-warning-text);
  border: 1px solid var(--color-warning);
  padding: 0 4px;
  border-radius: 3px;
  margin-left: 6px;
  vertical-align: middle;
}
/* Kept pill: solid green, the same as BEST. Patience-ref pill: amber outline. */
.badge-kept, .badge-patience-ref { white-space: nowrap; }
.badge-kept {
  background: var(--color-success);
  color: #fff;
  border-color: var(--color-success);
}
.badge-patience-ref {
  background: var(--color-bg);
  color: var(--color-warning-text);
  border-color: var(--color-warning);
  font-weight: 600;
}
.badge-patience-ref[data-patience-exhausted="true"],
.badge-kept[data-patience-exhausted="true"] {
  box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.35);
}
.best-marker {
  display: inline-block;
  font-size: 0.625rem;
  font-weight: 700;
  background: var(--color-success);
  color: #fff;
  padding: 1px 5px;
  border-radius: 3px;
  margin-left: 6px;
  vertical-align: middle;
}

/* ---------------------------------------------------------------------- *
 * 6B. Experiment detail
 * ---------------------------------------------------------------------- */

/* The colour-blind channel beside the ranking shade (6B item 3): a small
   arrow, not a fill, so "better"/"worse" survives greyscale and a screen
   reader alike -- the same rule `SEVERITY_KEY`'s marks and `DELTA_KEY`'s
   marks already follow on this table, extended to the cell the marks had
   not reached yet. */
.verdict-arrow { font-size: 0.75em; margin-left: 0.3em; }
.verdict-arrow[data-reference-verdict="better"] { color: var(--color-success-text); }
.verdict-arrow[data-reference-verdict="worse"] { color: var(--color-warning-text); }

/* Collapsible `<details>` sections (6B item 4): a visible toggle glyph, on
   every disclosure this console already renders natively (the decision
   panel, the pre-registration, the table legend, an arm's full
   configuration) -- one rule rather than one per screen. `::marker` is
   supported in every browser this console targets; the default triangle is
   restyled rather than replaced with a second, competing glyph. */
details.disclosure > summary {
  cursor: pointer;
  list-style-position: outside;
}
details.disclosure > summary::marker { color: var(--color-text-muted); }

/* ---------------------------------------------------------------------- *
 * 6D. Filter bars
 * ---------------------------------------------------------------------- */

.filters .clear-filters {
  align-self: center;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}
.filters .clear-filters:hover { color: var(--color-accent); }

/* Active filters, restated below the bar (6D item 4) so a narrowed table is
   never read as the whole one. Informational, not a second set of controls:
   Clear (above) already resets every field at once. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 0.75rem;
}
.filter-chips .chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  border-radius: 12px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  font-size: 0.8125rem;
  color: var(--color-text);
}
.filter-chips .chip b { font-weight: 600; }

/* ---------------------------------------------------------------------- *
 * 8A. HTMX loading indicators
 *
 * `.htmx-indicator` is htmx's own convention: hidden by default, shown while
 * the element it decorates (or an ancestor carrying its request) is
 * in flight. A CSS-only spinner -- no image, no JS -- next to the Filter
 * button on every filter form; the generic `.htmx-request` rule below covers
 * every OTHER htmx-triggered control (sort headers, the two-step confirm
 * buttons, row actions) with no markup change per site, because htmx adds
 * that class to the requesting element itself whenever no explicit
 * `hx-indicator` names a different one.
 * ---------------------------------------------------------------------- */

.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline-block; }

.spinner {
  width: 0.9em;
  height: 0.9em;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spinner-rotate 0.6s linear infinite;
  vertical-align: middle;
  margin-left: 0.4em;
}
@keyframes spinner-rotate { to { transform: rotate(360deg); } }

/* Any element htmx marks as in-flight -- a sort-header link, a two-step
   confirm/cancel button, a row action -- reads as busy without a spinner span
   at every one of those sites. */
a.htmx-request { opacity: 0.6; pointer-events: none; }
.btn.htmx-request { cursor: progress; opacity: 0.75; }

/* ---------------------------------------------------------------------- *
 * 9. Responsive design
 *
 * `.table-wrap` (9A) is declared once already, in section 3 above -- every
 * table in the template tree is now wrapped in it (see the templates diff),
 * so the rule needed no restatement here.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
  main.container { padding-inline: 16px; }

  .topbar { flex-wrap: wrap; }
  /* CSS-only hamburger: a checkbox nobody sees drives whether `.topbar nav`
     is shown. No JS -- the `<label>` IS the button, native `:checked`
     styling is the toggle. Markup lives in `_base.html`; the nav's own links
     and `aria-current` marks are untouched, only their visibility at this
     width changes. */
  .topbar { position: relative; }
  .nav-toggle { display: inline-flex; }
  .topbar nav {
    flex-basis: 100%;
    display: none;
    flex-direction: column;
    align-items: flex-start;
    order: 3;
  }
  #nav-toggle-check:checked ~ nav { display: flex; }
  .topbar form { order: 2; margin-left: auto; }

  .filters { flex-direction: column; align-items: stretch; }
  .filters label { display: flex; flex-direction: column; align-items: stretch; }

  /* `.facts` (run/model/dataset metadata) already collapses to one column at
     640px -- `app.css`'s own rule, untouched, not restated here. */
}

@media (min-width: 769px) {
  .nav-toggle { display: none; }
}

@media (max-width: 480px) {
  body { min-width: 320px; font-size: 0.85rem; }
  h1 { font-size: 1.3rem; }
  h2 { font-size: 1.1rem; }
  /* `main.container`, not `.container`: the 768px rule above set the padding
     on the MORE SPECIFIC selector (element + class), and a bare `.container`
     rule here would lose the cascade to it regardless of media-query order. */
  main.container { padding-inline: 12px; }
}

/* ---------------------------------------------------------------------- *
 * 10. Accessibility
 * ---------------------------------------------------------------------- */

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--color-accent);
  color: #fff;
  padding: 8px 16px;
  z-index: 100;
  border-radius: 0 0 4px 0;
}
.skip-link:focus {
  left: 0;
}

/* Visually hidden but still reachable by keyboard and announced by a screen
   reader -- unlike `hidden`, which removes an element from the tab order
   entirely. The nav-toggle checkbox (9B) needs exactly this: invisible, but
   still focusable and toggled with Space. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* `/nodes`' idle/busy indicator -- NOT the run-status vocabulary in
   `app.css`'s `.pill[data-status=...]` block: a busy node already carries its
   run's own status pill beside this dot, so this is only the fleet-level
   "is anything on this machine at all" signal, scoped to this one screen. */
.node-dot {
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}
.node-dot--idle { background: var(--color-success, #16a34a); }
.node-dot--busy { background: var(--color-warning, #d97706); }
