/* Runtime console — self-contained styles (no external fonts/CDN; air-gap safe).
   Design system: see DESIGN.md at the repository root. Product register,
   light theme with a faint cool cast, Restrained colour (near-neutral
   tinted surfaces + one accent). */
:root {
  /* Neutrals. Near-neutral with a faint cool cast (hue 258, chroma .002-.013).
     No #fff, no #000: a trace of chroma reads as a considered surface, a pure
     one reads as a framework default.

     Why cool and not the previous warm cream (hue 85): a warm off-white is the
     current reflex "tasteful" AI surface — the detector flags it by name, and
     on a screen sitting beside a terminal and a ticket tracker it reads softer
     than the work deserves. Cool at this chroma is not Tailwind slate either
     (that runs .012-.02 and is visibly blue); it is close enough to neutral
     that the eye reads "paper, correctly lit" rather than "tinted". All the
     personality is spent in one place, the accent. */
  --bg: oklch(0.975 0.003 258);
  --surface: oklch(0.995 0.002 258);
  --surface-sunk: oklch(0.960 0.005 258);   /* table heads, toolbars, insets */

  /* TWO border roles, and the distinction is a WCAG requirement, not taste.
     --border is decoration: panel edges, table rules, separators. Nothing about
     the interface is lost if it is faint, so 1.35:1 is fine (1.4.11 exempts
     purely decorative boundaries).
     --border-strong is the boundary of an INTERACTIVE control — the only thing
     marking where a text field begins, on a fill identical to the panel behind
     it. That is a user-interface component boundary under WCAG 1.4.11 and needs
     3.0:1. The old value measured 1.64:1 against its own fill, i.e. the design
     system was less accessible than the unstyled browser default it replaced.
     0.628 is the lightest value that clears 3.0 on the darkest surface a
     control can sit on (3.14 / 3.31 / 3.51 on sunk / bg / surface). */
  --border: oklch(0.898 0.006 258);
  --border-strong: oklch(0.628 0.013 258);

  --text: oklch(0.230 0.011 258);
  /* --muted must clear 4.5:1 on the DARKEST surface it can land on, not the
     lightest: .subtitle sits in .page-head, i.e. directly on --bg. Measured
     4.94 / 5.11 / 5.42:1 on sunk / bg / surface. */
  --muted: oklch(0.518 0.011 258);
  /* Darker muted for small text (labels, badges, table headers) where --muted
     is legible but leaves too little margin at 12px. 7.28:1 worst case. */
  --muted-strong: oklch(0.428 0.013 258);

  /* Accent: plum. Chosen by elimination — red/green/amber are spoken for by
     danger/success/warning, and blue/violet is the category reflex this console
     previously fell into (indigo #4f46e5). Plum is distinct from every semantic
     hue, so a link can never be misread as a state.
     Deepened from L=0.45 to L=0.40 and chroma 0.14 to 0.115: the brighter plum
     read closer to magenta. Darker and less saturated is the
     more serious register, and it lifts white-on-accent from 7.9 to 9.8:1. */
  --accent: oklch(0.400 0.115 328);
  --accent-hover: oklch(0.328 0.104 328);
  --accent-soft: oklch(0.962 0.017 328);
  --accent-border: oklch(0.720 0.075 328);

  --success: oklch(0.500 0.105 158);
  --success-bg: oklch(0.970 0.019 158);
  --success-border: oklch(0.735 0.070 158);
  --success-text: oklch(0.385 0.080 158);
  --warning: oklch(0.600 0.120 75);
  --warning-text: oklch(0.428 0.088 75);
  --warning-bg: oklch(0.967 0.026 75);
  --warning-border: oklch(0.735 0.080 75);
  --danger: oklch(0.478 0.150 27);
  --danger-soft: oklch(0.969 0.015 27);
  /* Badge outline: reinforcement only, since the badge's own text names the
     state, so the decorative exemption applies. */
  --danger-border: oklch(0.720 0.090 27);
  /* The Remove/Rotate button's edge IS the control boundary, so it needs the
     same 3.0:1 as --border-strong. Measured 3.33:1 against a panel. */
  --danger-control: oklch(0.650 0.095 27);

  /* Neutral hover wash. Was a plum tint: accent means "actionable", and every
     row in a table is not. A tonal step keeps hover feedback without the
     interface flushing pink as the pointer crosses it. */
  --row-hover: oklch(0.968 0.004 258);

  /* Exactly one shadow, used only where something genuinely floats (the login
     panel over an empty field). Elevation elsewhere is a hairline border: a
     drop shadow under every panel is the SaaS-card tell. */
  --shadow: 0 1px 2px oklch(0.230 0.011 258 / .05), 0 2px 8px oklch(0.230 0.011 258 / .06);

  /* Landing-page-only tokens. The console is Restrained; the one public page is
     Committed, and a drenched field needs an ink and an on-ink ramp that no
     other surface here uses. Namespaced --lp- so they cannot leak into the
     operator UI by autocomplete.

     --lp-ink is the accent driven very dark (L 0.400 -> 0.205): a deep plum
     field rather than a black one, so the brand colour is doing the drenching
     instead of a neutral. White on it measures 15.5:1.
     The on-ink ramp is alpha over that field, so every step stays in the same
     hue without three more hardcoded colours. */
  --lp-ink: oklch(0.205 0.058 328);
  --lp-on-ink: oklch(0.995 0.002 258);
  --lp-on-ink-dim: oklch(0.995 0.002 258 / .78);
  --lp-on-ink-faint: oklch(0.995 0.002 258 / .52);
  --lp-hairline-strong: oklch(0.995 0.002 258 / .42);
  /* Accent text on --accent-soft. --accent itself measures 9.0:1 there and is
     fine; this is a touch deeper for display sizes on the tint. */
  --accent-ink: oklch(0.345 0.105 328);

  --radius: 8px;
  --radius-sm: 6px;

  /* Type scale: fixed rem. Not fluid — operators view at consistent DPI, and a
     clamp-sized h1 that shrinks inside a panel looks worse.
     Rendered ladder: 24 / 18 / 15 / 13.6 / 12px.
     The bottom three steps are 1.10x and 1.13x apart, under the 1.25 the design
     laws ask for, and a detector flags it. Kept deliberately. These are not
     hierarchy tiers competing for attention; they are body / secondary / label,
     already separated by weight, colour, and caps. Widening the gaps means
     shrinking labels below 12px in a dense tool operators read under time
     pressure — legibility is the higher law here (see PRODUCT.md). Tried
     15/13/11.5: the ratio moved 1.10x -> 1.15x and the text just got smaller. */
  --fs-h1: 1.5rem;      /* 24px */
  --fs-h2: 1.125rem;    /* 18px */
  --fs-body: 0.9375rem; /* 15px */
  --fs-sm: 0.85rem;     /* 13.6px — secondary text, descriptions */
  --fs-xs: 0.75rem;     /* 12px   — uppercase labels, badges, table heads */

  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

/* Skip link: off-screen until focused. Onboarding is a ~5,600px page behind a
   five-item nav, so without this a keyboard user tabs the entire header on
   every navigation before reaching any content. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: .55rem .9rem;
  background: var(--surface);
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 0 0 var(--radius) 0;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.skip-link:focus { left: 0; text-decoration: none; }

body {
  font: var(--fs-body)/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  margin: 0;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* ---- Top bar ----
   min-height rather than height, and the bar is allowed to wrap: with five nav
   items the row is ~396px wide, so on a 375px phone a fixed single line forced
   the whole document to scroll horizontally on every authenticated page. */
.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem 1rem;
  min-height: 56px;
  padding: .5rem 1.5rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  font-weight: 650;
  font-size: var(--fs-h2);
  color: var(--text);
  text-decoration: none;
  letter-spacing: -.01em;
}
/* Flat accent square, not a gradient tile. The gradient logo is the single most
   reliable "generated SaaS admin" tell, and it spent the accent colour on
   decoration in a UI where accent is supposed to mean "actionable". */
.brand .logo {
  display: inline-grid;
  place-items: center;
  width: 24px; height: 24px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--surface);
  font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: -.02em;
}
.topbar nav { display: flex; align-items: center; flex-wrap: wrap; gap: .125rem; margin-left: auto; }
.topbar nav a, .topbar nav .navbtn {
  padding: .35rem .65rem;
  border-radius: var(--radius-sm);
  color: var(--muted-strong);
  text-decoration: none;
  font-weight: 500;
  font-size: var(--fs-sm);
}
.topbar nav a:hover { background: var(--surface-sunk); color: var(--text); text-decoration: none; }
/* Current page: an ink-weight label, not an accent fill. Accent stays reserved
   for things you can act on. */
.topbar nav a[aria-current="page"] { color: var(--text); font-weight: 620; }
.topbar nav form { margin: 0; }
.topbar nav .navbtn {
  background: transparent;
  border: 1px solid var(--border-strong);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}
.topbar nav .navbtn:hover { background: var(--bg); color: var(--text); }

/* ---- Layout ---- */
.container { max-width: 1040px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
/* Rhythm is deliberately uneven: a generous gap below the page head, tighter
   gaps between sibling panels. Uniform spacing everywhere is the monotony that
   makes an interface read as generated. */
.page-head { margin: 0 0 2rem; }
h1 { font-size: var(--fs-h1); font-weight: 640; margin: 0 0 .35rem; letter-spacing: -.022em; }
h2 { font-size: var(--fs-h2); font-weight: 620; margin: 0 0 .85rem; letter-spacing: -.012em; }
/* Was unstyled: the browser default computed to 17.55px/700, a size belonging to
   no token and sitting between --fs-h2 (18px) and --fs-body (15px). Two live
   instances, both subheads inside the Credentials panel. */
h3 { font-size: var(--fs-body); font-weight: 640; margin: 0 0 .5rem; letter-spacing: -.008em; }
.subtitle { color: var(--muted); font-size: var(--fs-sm); margin: 0; max-width: 68ch; }
.breadcrumb { font-size: var(--fs-sm); margin: 0 0 1rem; }
/* inline-block + padding-block so the link is a 24px-tall target rather than a
   16px line of text (WCAG 2.5.8). On agent, session, and eval-run pages this is
   the only in-page back navigation, so it was the smallest target on the page
   and the one a lost user reaches for first. The negative inline margin keeps
   the text optically aligned with the page head despite the new padding. */
.breadcrumb a {
  display: inline-block;
  padding: .25rem .3rem;
  margin-inline: -.3rem;
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-decoration: none;
}
.breadcrumb a:hover { color: var(--accent); background: var(--accent-soft); text-decoration: none; }

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

/* ---- Panels (was: cards) ----
   A bordered region with a heading, not a floating tile. No shadow: on a tinted
   field one hairline separates plenty, and a shadow under every panel is the
   thing that makes an operator tool look like a template. Never nested. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.4rem 1.6rem;
  /* 1.75rem, up from 1rem. At the old spacing nine consecutive panels on the
     onboarding page read as one continuous ruled sheet: the gap between two
     panels (16px) was smaller than the padding inside one (20px), so the
     hairline borders looked like row rules rather than boundaries. The gap now
     exceeds the internal padding, which is what makes a panel read as a
     discrete object. */
  margin-bottom: 1.75rem;
}
.card:last-child { margin-bottom: 0; }
.card > h2:first-child { margin-top: 0; }
/* Pulls the description up under its heading. Scoped to the h2-precedes case:
   as a bare margin it also applied when .card-desc came FIRST in a panel (the
   Credentials section leads with a button), where -.5rem dragged the text 8px
   up UNDER that button and the two overlapped. */
.card-desc { color: var(--muted); font-size: var(--fs-sm); margin: 0 0 1rem; max-width: 68ch; }
h2 + .card-desc { margin-top: -.5rem; }
/* A panel containing a wide table scrolls ITSELF rather than the document.
   Only agent.html wrapped its tables in .table-wrap; the other 17 tables across
   six templates did not, so a six-column eval-run table pushed the whole page
   sideways on a phone. Putting the guard on the container fixes every table at
   once instead of asking seventeen call sites to remember. */
.card:has(> table) { overflow-x: auto; }

/* ---- Tables ---- */
table { border-collapse: collapse; width: 100%; }
/* Sunk header: a tonal shift reads as structure without adding a second border
   weight, and keeps the header legible when a table scrolls under it. */
thead th {
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .045em;
  color: var(--muted-strong);
  font-weight: 600;
  padding: .55rem .8rem;
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--border);
}
thead th:first-child { border-top-left-radius: var(--radius-sm); }
thead th:last-child { border-top-right-radius: var(--radius-sm); }
tbody td { padding: .7rem .8rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--row-hover); }
td.actions { text-align: right; width: 1%; white-space: nowrap; }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
/* .mono sets a font-size, and as a class it outranks the `h1` element selector.
   agent.html's <h1 class="mono"> therefore computed to 13.6px — SMALLER than the
   15px body text, and exactly the size of its own subtitle. The console's
   most-visited detail page had no title in the visual hierarchy. Restore the
   heading sizes and keep only the family. */
h1.mono { font-size: var(--fs-h1); }
h2.mono { font-size: var(--fs-h2); }

/* ---- Badges ----
   Enumerated state only (role, health, model) — never decoration. Each carries
   a hairline as well as a tint so it survives forced-colors and low-quality
   displays where a pale wash disappears. */
.badge {
  display: inline-block;
  padding: .15rem .55rem;
  border-radius: 999px;
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.45;
  border: 1px solid transparent;
}
.badge-role { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
.badge-ok { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.badge-muted { background: var(--surface-sunk); color: var(--muted-strong); border-color: var(--border); }
.badge-danger { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }
/* Degraded / in-between state. --warning existed as a token but nothing used
   it, so "unknown" and "degraded" had to borrow ok or danger — both wrong.
   PRODUCT.md calls those first-class states; this is the badge for them. */
.badge-warn { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .4rem; vertical-align: middle; }
/* Not color alone: ok is a filled dot, off is a hollow ring (WCAG 1.4.1). */
.dot-ok { background: var(--success); }
.dot-off { background: transparent; box-shadow: inset 0 0 0 1.5px var(--muted); }

/* ---- Row (1D flex group: stat tiles, inline controls) ---- */
/* Base rule: any .row is a wrapping flex line. Without this, bare .row
   containers (stat tiles on the agent + observability pages) have no flex
   context and their children collapse to a vertical stack. */
.row { display: flex; flex-wrap: wrap; gap: .75rem; }

/* ---- Inline form (a control strip above a table) ----
   Referenced by observability.html but never defined, so it fell back to
   display:block: the labels rendered inline at 16px next to a 39px button, and
   the gap between the primary action and the table below it computed to 0px,
   making "Launch run" read as a table row. */
.inline-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .75rem;
  margin-bottom: 1.25rem;
}
.inline-form label { display: flex; flex-direction: column; gap: .25rem; }

/* ---- Forms ---- */
/* flex-end, not center: once a control carries a visible label above it, a
   centred row floats the bare button halfway up the field it submits. Aligning
   on the baseline of the controls keeps buttons level with the inputs. */
form .row { gap: .6rem; align-items: flex-end; }
label { font-size: var(--fs-sm); font-weight: 500; color: var(--text); }
/* A label that wraps its control stacks: text, then field. */
form .row > label { display: flex; flex-direction: column; gap: .25rem; }
/* Inverted selector, deliberately. The previous rule ENUMERATED the types it
   styled (text, password, untyped, select) and so silently missed `textarea`
   and `input[type=number]`: five of the thirty-nine controls on the onboarding
   page rendered as raw browser widgets (0px radius, 2px inset system border,
   13.3px Arial, 1px padding) directly beside styled siblings — and they were
   the Cedar-policy and JSON-cases fields, the highest-stakes inputs in the
   console. An allowlist fails open every time someone adds an input type;
   `:not()` fails closed. hidden/submit/checkbox/radio are excluded because they
   are not text-entry surfaces. */
input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]),
select, textarea {
  font: inherit;
  padding: .5rem .65rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  min-width: 0;
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* Monospace for the fields that hold code, not prose: Cedar policies and JSON
   arrays are read character-by-character, and proportional digits/brackets make
   a misplaced brace genuinely hard to spot. */
textarea { font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.55; resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--muted); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunk);
  color: var(--muted);
  cursor: not-allowed;
}
/* A required field should be identifiable before submission, not after a server
   round-trip rejects it: eleven fields carried `required` with no visual mark.

   The marker hangs off the label TEXT (<span class="req">), not off the <label>
   element. These labels wrap their control, so a ::after on the label lands
   after the input and renders as a stray asterisk floating below the field.

   Assistive tech gets the requirement from the control's own `required`
   attribute, so the asterisk is purely a visual affordance. */
.req::after {
  content: "*";
  color: var(--danger);
  font-weight: 700;
  margin-left: .15rem;
}

/* Keyboard focus for everything else: links, buttons, nav. Inputs keep their
   ring above; this restores a visible indicator the custom styling otherwise
   suppresses (WCAG 2.4.7). :focus-visible so mouse clicks don't show a ring. */
a:focus-visible, button:focus-visible, .btn:focus-visible, .navbtn:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
fieldset { border: 1px solid var(--border); border-radius: var(--radius); padding: .9rem 1.1rem; margin: .9rem 0; }
/* Sentence case, not uppercase. A <legend> here is not a one-word label: the
   longest reads "Transport — fill exactly one of the two below" (45 characters),
   which is an instruction, and 45 characters of 12px all-caps is measurably
   slower to read because uppercase removes the ascender/descender word shapes we
   recognise. Uppercase stays on thead th and .stat-label, which are genuinely
   short labels (max 11 characters measured). */
legend {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted-strong);
  padding: 0 .4rem;
}

button, .btn {
  font: inherit;
  font-weight: 600;
  font-size: var(--fs-sm);
  padding: .5rem .9rem;
  border-radius: var(--radius);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--surface);
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
button:hover, .btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
button:active, .btn:active { background: var(--accent-hover); }
/* Disabled was previously unstyled: a primary-looking button that silently does
   nothing. Every interactive element here ships default/hover/focus/active/
   disabled. */
button:disabled, .btn:disabled, button[aria-disabled="true"] {
  background: var(--surface-sunk);
  border-color: var(--border);
  color: var(--muted);
  cursor: not-allowed;
}
button:disabled:hover, .btn:disabled:hover { background: var(--surface-sunk); border-color: var(--border); }
button.play {
  width: 2.1rem;
  height: 2.1rem;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xs);
  line-height: 1;
}
button.ghost {
  background: transparent;
  color: var(--muted-strong);
  border-color: var(--border-strong);
}
button.ghost:hover { background: var(--surface-sunk); color: var(--text); border-color: var(--border-strong); }
button.danger {
  background: transparent;
  color: var(--danger);
  border-color: var(--danger-control);
  /* .35rem gave a 26px-tall control. Interactive targets should clear 24px
     (WCAG 2.5.8) with margin, not sit on the line. */
  padding: .4rem .75rem;
  font-size: var(--fs-sm);
}
button.danger:hover { background: var(--danger-soft); border-color: var(--danger); }

/* ---- Flash / alerts ----
   Full border plus a tint, never a thick left stripe: the side-stripe callout
   is a banned pattern, and it also breaks alignment with everything else on
   the page. */
.flash {
  display: flex; align-items: center; gap: .5rem;
  padding: .7rem .9rem;
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  color: var(--success-text);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
  margin-bottom: 1.25rem;
  word-break: break-all;
}
.flash-error { background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger); }

/* ---- New-key reveal (shown once, on creation) ---- */
.key-reveal {
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius);
  padding: .9rem 1rem;
  margin-bottom: 1.25rem;
}
.key-reveal-head { margin: 0 0 .5rem; font-size: var(--fs-sm); font-weight: 600; color: var(--accent-hover); }
.key-reveal-value {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: .55rem .7rem;
  overflow-wrap: anywhere;
  user-select: all;
}

/* ---- Empty state ----
   Teaches the next action rather than announcing absence; the templates supply
   the "how", this just gives it room to be a sentence instead of a shrug. */
.empty { color: var(--muted); font-size: var(--fs-sm); padding: 1.25rem .8rem; line-height: 1.6; }
.empty strong { color: var(--text); font-weight: 600; }
/* Inherit the parent's size rather than .95em: a relative step off --fs-sm
   landed on 12.35px, a sixth tier wedged between two tokens for no reason.
   Mono already reads smaller than sans at the same size. */
.empty code { font-family: var(--mono); background: var(--surface-sunk); border: 1px solid var(--border); border-radius: 4px; padding: .1rem .35rem; }

/* ---- Stream / pre ----
   The one dark surface in a light UI, and deliberately so: a transcript is
   verbatim machine output, and the tonal break says "this is not chrome, this
   is the log". Carries the same hue as every other neutral (258) so it reads as
   part of the system rather than a pasted-in slate block; it was left on the
   old warm hue 70 when the palette moved and the mismatch showed. Ink measures
   12.37:1 on this ground. */
pre {
  background: oklch(0.235 0.012 258);
  color: oklch(0.900 0.006 258);
  padding: 1rem 1.1rem;
  border-radius: var(--radius);
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.6;
  min-height: 6rem;
  max-height: 70vh;
  overflow: auto;
}

/* Cedar policy source shown inside a table cell. It is configuration you READ,
   not machine output you tail, so it must not inherit the dark log treatment
   above — that rendered a policy as a 426x96px black slab next to a Remove
   button. Quiet inset instead, sized to its content. */
pre.policy-text {
  background: var(--surface-sunk);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .5rem .6rem;
  margin: 0;
  min-height: 0;
  max-height: 12rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* ---- Centered auth panel (login) ----
   The one place a shadow is earned: this genuinely floats over an empty field
   with nothing else on screen to establish depth. */
.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: 1.5rem; }
.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 2.25rem 2rem;
  width: 100%;
  max-width: 360px;
  text-align: center;
}
.auth-card .brand { justify-content: center; margin-bottom: 1.5rem; font-size: var(--fs-h2); }
.auth-card form { display: flex; flex-direction: column; gap: .7rem; margin-top: 1rem; }
.auth-card input { width: 100%; padding: .6rem .7rem; }
.auth-card button { width: 100%; padding: .6rem; }
.auth-hint { color: var(--muted); font-size: var(--fs-sm); margin-top: 1.25rem; line-height: 1.6; }

/* ---- Top-bar accent button (landing sign-in) ---- */
.navbtn-accent {
  background: var(--accent) !important;
  color: var(--surface) !important;
  border: 1px solid var(--accent) !important;
  font-weight: 600;
}
.navbtn-accent:hover { background: var(--accent-hover) !important; color: var(--surface) !important; text-decoration: none; }

/* ---- Landing page (brand register) ----
   The console is Restrained by rule: tinted neutrals, one accent under 10%,
   colour reserved for meaning. This one surface is deliberately the opposite.
   It is the only page a visitor sees before authenticating, and a page whose
   job is to communicate rather than to get out of the way has to have a voice.

   Strategy: Committed. A drenched plum field carries the hero, the type is
   twice the size it is anywhere else in the product, and the imagery is a
   drawn diagram rather than a photograph — the deployment this product exists
   for has no route to the internet, so stock photography is not available and
   a coloured placeholder block would be worse than none.

   What this deliberately is NOT: the previous version of this page was ruled
   separators, monochrome restraint, and a small uppercase tracked label above
   every section. That is the editorial-typographic lane, which by now is the
   default "tasteful" AI brand surface. One kicker can be voice; repeating it
   as section grammar is scaffolding. There is now exactly one uppercase label
   on the page, inside the diagram, where it labels a boundary.

   Type is scale and weight only. Air-gap forbids a webfont, so voice has to
   come from a 4.4x range between the display and body sizes rather than from
   a display family. */

/* The drenched field bleeds full width; the topbar sits on it and inverts. */
body.on-landing { background: var(--lp-ink); }
.topbar-invert {
  background: transparent;
  border-bottom: none;
  position: relative;
  z-index: 2;
}
.topbar-invert .brand { color: var(--lp-on-ink); }
/* The logo tile is accent-on-white in the console. On plum, accent-on-accent
   would vanish, so it inverts to a light tile with the field showing through. */
.topbar-invert .brand .logo {
  background: var(--lp-on-ink);
  color: var(--lp-ink);
}
/* Scoped under .topbar-invert nav to match the specificity of the base
   `.topbar nav a` rule (0,1,1). Written as a plain `.navbtn-onDark` (0,1,0) it
   LOST the cascade silently: the label kept the light-theme --muted and
   measured 2.23:1 on the plum field, which is what the eye reported and the
   contrast probe confirmed. Specificity, not !important. */
.topbar-invert nav .navbtn-onDark {
  background: transparent;
  color: var(--lp-on-ink);
  border: 1px solid var(--lp-hairline-strong);
  font-weight: 600;
}
.topbar-invert nav .navbtn-onDark:hover {
  background: var(--lp-on-ink);
  color: var(--lp-ink);
  text-decoration: none;
}

.lp-hero {
  background: var(--lp-ink);
  color: var(--lp-on-ink);
  /* Asymmetric: copy left, diagram right, and NOT a 50/50 split. A centred
     stack of eyebrow-title-sub-button is the template this page is avoiding. */
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
  padding: clamp(3rem, 8vw, 6.5rem) clamp(1.25rem, 5vw, 4rem) clamp(3.5rem, 8vw, 6rem);
  margin-top: -57px;          /* pull the field up behind the transparent bar */
  padding-top: calc(clamp(3rem, 8vw, 6.5rem) + 57px);
}
.lp-hero-inner { max-width: 34rem; }

/* The one typographic risk the page takes. clamp() from 2.4rem to 4.6rem is a
   ~4.4x step over body text; the console's own h1 is 1.5rem. Tight tracking at
   this size stops the two lines reading as separate thoughts. */
.lp-title {
  font-size: clamp(2.4rem, 6.2vw, 4.6rem);
  line-height: 1.02;
  font-weight: 700;
  letter-spacing: -.038em;
  margin: 0 0 1.5rem;
  color: var(--lp-on-ink);
}
.lp-lede {
  font-size: clamp(1.0625rem, 1.6vw, 1.25rem);
  line-height: 1.6;
  color: var(--lp-on-ink-dim);
  margin: 0 0 2.25rem;
  max-width: 46ch;
}
.lp-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }

/* Diagram. Hairlines and one filled node, so it reads as a drawing rather than
   a chart: the control plane is the only solid shape because it is the only
   thing this product actually is. */
.lp-figure { justify-self: center; width: 100%; max-width: 30rem; }
.lp-svg { width: 100%; height: auto; display: block; color: var(--lp-on-ink); }
.lp-perim {
  fill: none;
  stroke: var(--lp-hairline-strong);
  stroke-width: 1;
  stroke-dasharray: 3 4;
}
.lp-node { fill: none; stroke: var(--lp-hairline-strong); stroke-width: 1; }
.lp-node-key { fill: var(--lp-on-ink); stroke: none; }
.lp-wire { fill: none; stroke: var(--lp-hairline-strong); stroke-width: 1; }
.lp-svg-label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .14em;
  fill: var(--lp-on-ink-faint);
}
.lp-svg-cap { font-size: 11px; fill: var(--lp-on-ink-dim); }
.lp-svg-node { font-size: 12px; font-weight: 600; fill: var(--lp-on-ink); }
.lp-node-key + .lp-svg-node { fill: var(--lp-ink); }

/* Below the fold returns to the product's own light surface. Art direction per
   section is a brand permission: the hero is the pitch, this is the substance,
   and they are allowed to look different. */
.lp-band {
  background: var(--surface);
  padding: clamp(3rem, 7vw, 5rem) clamp(1.25rem, 5vw, 4rem);
}
.lp-band-tight { padding-top: clamp(2rem, 4vw, 3rem); }
.lp-h2 {
  font-size: clamp(1.5rem, 3vw, 2.15rem);
  line-height: 1.15;
  font-weight: 680;
  letter-spacing: -.028em;
  margin: 0 0 2rem;
  max-width: 22ch;
  color: var(--text);
}
.lp-body {
  font-size: var(--fs-body);
  line-height: 1.7;
  color: var(--muted-strong);
  margin: 0 0 1.25rem;
  max-width: 60ch;
}

/* Six claims, one line each. auto-fit rather than a fixed column count so the
   list reflows without a breakpoint. No boxes, no icons: bold lead-in carries
   the scan, and an icon above each would be the template tell. */
.lp-claims {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: 1.5rem 3.5rem;
}
.lp-claims li {
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--muted-strong);
  max-width: 44ch;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}
.lp-claims b { color: var(--text); font-weight: 640; display: block; margin-bottom: .15rem; }

/* The differentiator gets its own fold, set at display size on the accent-soft
   tint. This is the sentence the whole page exists to deliver. */
.lp-quote {
  background: var(--accent-soft);
  padding: clamp(3rem, 7vw, 5.5rem) clamp(1.25rem, 5vw, 4rem);
  border-top: 1px solid var(--accent-border);
  border-bottom: 1px solid var(--accent-border);
}
.lp-quote-text {
  font-size: clamp(1.5rem, 3.4vw, 2.4rem);
  line-height: 1.2;
  font-weight: 660;
  letter-spacing: -.03em;
  color: var(--accent-ink);
  margin: 0 0 1rem;
  max-width: 24ch;
}
.lp-quote-sub {
  font-size: var(--fs-body);
  line-height: 1.65;
  color: var(--muted-strong);
  margin: 0;
  max-width: 54ch;
}

.lp-links { margin: 0; font-size: var(--fs-body); }
.lp-links a { font-weight: 600; }

/* Google button — Google brand guidance: white surface, neutral border. The
   #fff / #3c4043 / #f8f9fa values are Google's, not ours, and are the one place
   in this stylesheet exempt from the tinted-neutral rule: a sign-in button that
   deviates from the published spec is a trust signal in the wrong direction. */
.btn-google {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  background: #fff;
  color: #3c4043;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: .8rem 1.5rem;
  font-size: var(--fs-body);
  font-weight: 600;
  text-decoration: none;
  transition: background .15s ease;
}
.btn-google:hover { background: #f8f9fa; text-decoration: none; }
.btn-google .g-logo { flex: none; }

/* Token fallback form (shown only when OIDC is off). */
.token-form { display: flex; gap: .5rem; flex-wrap: wrap; }
.token-form input { min-width: 220px; padding: .6rem .7rem; }
.token-form-onDark input {
  background: var(--lp-on-ink);
  border: 1px solid transparent;
  color: var(--text);
}
.token-form-onDark button {
  background: var(--lp-on-ink);
  color: var(--lp-ink);
  border: 1px solid var(--lp-on-ink);
  font-weight: 600;
}
.token-form-onDark button:hover { background: var(--lp-on-ink-dim); border-color: var(--lp-on-ink-dim); }

/* The unflattering facts. A footnote by size, not by omission. */
.lp-foot {
  background: var(--bg);
  border-top: 1px solid var(--border);
  padding: 2.5rem clamp(1.25rem, 5vw, 4rem) 3.5rem;
}
.lp-foot p {
  color: var(--muted-strong);
  font-size: var(--fs-sm);
  line-height: 1.6;
  margin: 0 0 .6rem;
  max-width: 68ch;
}
.lp-foot p:last-child { margin-bottom: 0; }
.lp-foot b { color: var(--text); font-weight: 620; }

/* The hero's two columns cannot both survive a tablet. Diagram first would put
   a decoration above the headline, so the copy stays first and the figure
   follows it. */
@media (max-width: 900px) {
  .lp-hero { grid-template-columns: 1fr; gap: 2.5rem; }
  .lp-hero-inner { max-width: 40rem; }
  .lp-figure { justify-self: start; max-width: 26rem; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
}

@media (max-width: 640px) {
  /* Stat tiles: two-up grid so they wrap evenly instead of leaving an orphan. */
  .row .stat { flex: 1 1 calc(50% - .75rem); min-width: 0; }
  .container { padding: 1.25rem 1rem 3rem; }
  .card { padding: 1rem 1.1rem; }
  .status-strip { gap: .4rem .75rem; }
  /* Nav drops to its own full-width line below the brand rather than being
     squeezed beside it; margin-left:auto would otherwise push it off-canvas. */
  .topbar { padding: .5rem 1rem; }
  .topbar nav { margin-left: 0; width: 100%; gap: .125rem .25rem; }
}

/* ---- Stat tiles (observability) ----
   Left-aligned, label above value, no shadow. Centred big-number-over-small-
   label tiles are the hero-metric template; left alignment also lets the eye
   scan a row of figures down a common edge, which is what these are for. */
.stat {
  flex: 1;
  /* 140px, up from 110px. A 7-digit token count ("4,821,903") measures 107px at
     1.4rem tabular, so it overflowed a 94px content box by 13px and spilled into
     the neighbouring tile. Token counts on a busy agent are ALWAYS 7+ digits, so
     this was a guaranteed break, not an edge case. */
  min-width: 140px;
  padding: .75rem .9rem;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* The markup emits value-then-label; column-reverse lifts the label above the
     figure without touching eight template lines. The DOM order is also the
     better reading order for a screen reader ("1,240 — tokens in"). */
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  gap: .3rem;
}
.stat-num {
  font-size: 1.4rem;
  font-weight: 620;
  letter-spacing: -.022em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  /* Belt and braces with the min-width above: if a figure still exceeds its
     tile at some viewport, it wraps inside its own box rather than spilling
     across the neighbour's border. */
  overflow-wrap: anywhere;
}
/* Spacing between label and figure comes from the container's `gap`, not a
   margin. Under column-reverse a margin-top lands on the physical TOP of the
   label — which is the outer edge of the tile, not the space between the two
   lines — so the measured label-to-value gap was 0px while the comment here
   claimed otherwise. `gap` is direction-agnostic and cannot get this wrong. */
.stat-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .045em;
  color: var(--muted-strong);
}
/* A stat tile that carries a warning signal (e.g. non-zero error count). */
.stat-warn { border-color: var(--danger-border); background: var(--danger-soft); }
.stat-warn .stat-num { color: var(--danger); }

/* ---- Status strip (compact health line; not a metric tile) ---- */
.status-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem 1.1rem;
  padding: .7rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1.25rem;
  font-size: var(--fs-sm);
}
.status-strip .status-health { display: inline-flex; align-items: center; font-weight: 600; }
.status-strip .status-sep { width: 1px; height: 1.1rem; background: var(--border); }
.status-strip .status-item { color: var(--muted-strong); }
.status-strip .status-item b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.status-strip .status-item.is-error b { color: var(--danger); }
/* A health summary that is itself the bad news (e.g. "2 unreachable"). */
.status-strip .status-health.is-error { color: var(--danger); }
/* Table-cell variant: the word carries the state, so it must not rely on the
   dot's colour alone. */
td .is-error { color: var(--danger); font-weight: 500; }

/* ---- Table overflow guard (long mono IDs on narrow screens) ---- */
.table-wrap { overflow-x: auto; }
td .mono, .mono { overflow-wrap: anywhere; }

/* ---- Section index (onboarding) ----
   The onboarding page is ~5,100px across nine sections with no in-page
   navigation: the skip link lands a keyboard user at #main, i.e. the top of the
   same 5,100px, and reaching "Online eval policies" was an unassisted 4,500px
   scroll on every visit. Nine text links, no JS, no sticky positioning (a
   sticky bar steals vertical space on the laptop screens this is read on). */
.section-index {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem .5rem;
  margin: 0 0 1.75rem;
  padding: .85rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.section-index-head {
  width: 100%;
  margin: 0 0 .2rem;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .045em;
  color: var(--muted-strong);
}
.section-index a {
  font-size: var(--fs-sm);
  color: var(--muted-strong);
  padding: .3rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  text-decoration: none;
}
.section-index a:hover {
  color: var(--accent);
  border-color: var(--accent-border);
  background: var(--accent-soft);
  text-decoration: none;
}
/* An anchored panel must not land flush against the viewport edge, and the
   :target flash confirms which section the jump actually reached. */
section[id] { scroll-margin-top: 1rem; }

/* ---- Danger zone ----
   A destructive maintenance action set apart from the constructive controls
   above it: a sunk strip with a rule on top, the consequence spelled out beside
   the button rather than only inside a confirm() dialog. Not a coloured panel —
   a red box would shout on every page load; this stays quiet until read. */
.danger-zone {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem 1.5rem;
  margin-top: 1.75rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border);
}
.danger-zone strong { font-size: var(--fs-sm); font-weight: 620; }
.danger-zone-desc {
  margin: .15rem 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
  max-width: 60ch;
}
.danger-zone form { margin: 0; flex: none; }

/* ---- Stream states (session.html) ----
   The event <pre> previously rendered as an unlabelled 942x96px dark slab in
   all three states: connecting, connected-but-empty, and stream-dead were
   pixel-identical, because es.onerror closed the connection silently. These
   render the state as text ABOVE the transcript rather than inside it, so a
   status line is never confused with a log line. */
.stream-state {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0 0 .6rem;
}
.stream-state.is-error { color: var(--danger); font-weight: 500; }
.stream-state.is-live { color: var(--success-text); }
