@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
  color-scheme: light;

  /* The page recedes so that raised can mean raised. A card cannot be lighter
     than #ffffff, so the earlier light theme made base and raised the same
     colour and stepped the card *down* instead: same size of step as dark,
     opposite direction, and a card that reads as sunken rather than lifted.
     Stepping the page down instead keeps the direction the dark theme has and
     the size it uses, 1.06 between the two. */
  --surface-sunken: #f4f5f7;
  --surface-base: #f7f8f9;
  --surface-raised: #ffffff;
  --surface-card: var(--surface-raised);
  --surface-overlay: #ffffff;
  --surface-hover: #f4f5f7;
  --surface-active: #eceef1;
  --surface-selected: rgb(234 88 12 / 0.07);
  /* Recessed against the card it sits inside, which is now #ffffff. It was
     #f7f8f9, which is the page. */
  --surface-inset: #f4f5f7;

  --border-subtle: #eceef1;
  --border-default: #dfe2e7;
  --border-strong: #c3c8d0;
  --border-accent: rgb(234 88 12 / 0.35);

  --fg-primary: #14161a;
  --fg-secondary: #5b6270;
  --fg-tertiary: #69707e;
  --fg-disabled: #a8aeb9;
  --fg-on-accent: var(--grey-000);


  --accent-fg: var(--orange-600);
  --accent-solid: var(--orange-500);
  --accent-solid-hover: var(--orange-600);
  --accent-solid-active: #9a3412;
  --accent-muted: rgb(234 88 12 / 0.09);

  --status-running-fg: #1d64d8;
  --status-running-muted: rgb(59 130 246 / 0.1);
  --status-waiting-fg: #a55a00;
  --status-waiting-muted: rgb(245 158 11 / 0.12);
  --status-passed-fg: #047857;
  --status-passed-muted: rgb(16 185 129 / 0.1);
  --status-failed-fg: #c22626;
  --status-failed-muted: rgb(239 68 68 / 0.1);
  --status-idle-fg: #7b8290;
  --status-idle-muted: rgb(123 130 144 / 0.12);
  --status-idle-border: #dfe2e7;

  --data-1: var(--orange-500);
  --data-2: #1d64d8;
  --data-3: #047857;
  --data-4: #a55a00;
  --data-5: #c22626;
  --data-6: #7b8290;

  --shadow-xs: 0 1px 2px rgb(16 18 22 / 0.06);
  --shadow-sm: 0 1px 3px rgb(16 18 22 / 0.08), 0 0 0 1px rgb(16 18 22 / 0.04);
  --shadow-md: 0 4px 12px rgb(16 18 22 / 0.1), 0 0 0 1px rgb(16 18 22 / 0.05);
  --shadow-lg: 0 12px 32px rgb(16 18 22 / 0.12), 0 0 0 1px rgb(16 18 22 / 0.05);
  --shadow-overlay: 0 16px 48px rgb(16 18 22 / 0.16), 0 0 0 1px rgb(16 18 22 / 0.06);
  --scrim: rgb(20 22 26 / 0.32);
}
}
/* Dark is canonical. Crewbit is a build surface that engineers keep open next to
   an editor and a terminal, so the dark theme is designed first and the light
   theme is derived from it — not the other way round.

   Neutrals are a single cool-grey ramp with a trace of warmth so they sit with
   the orange accent instead of fighting it. Nothing here is a raw hex in a
   component: components only ever reference the semantic names. */
:root {
  color-scheme: dark;

  /* ── Neutral ramp ───────────────────────────────────────── */
  --grey-1000: #08090b;
  --grey-950: #0c0d10;
  --grey-900: #121418;
  --grey-850: #171a1f;
  --grey-800: #1d2027;
  --grey-750: #23272f;
  --grey-700: #2b2f38;
  --grey-600: #3a3f4a;
  --grey-500: #545a67;
  /* Lifted from #6e747f on 30 August 2026. `--fg-tertiary` reads it, and at
     12px that was 3.92:1 on `--surface-raised` against the 4.5:1 AA asks; it is
     4.52:1 now, and 4.88:1 on the darkest surface. `--status-idle-fg` and
     `--data-6` read it too and both improved. */
  --grey-400: #787e89;
  --grey-300: #8f96a2;
  --grey-200: #b4bac4;
  --grey-100: #d7dae0;
  --grey-050: #eceef1;
  --grey-000: #ffffff;

  /* ── Accent: the factory hue ─────────────────────────────── */
  --orange-600: #c2410c;
  --orange-500: #ea580c;
  --orange-400: #f97316;
  --orange-300: #fb923c;
  --orange-200: #fdba74;

  /* ── Raw semantic hues ───────────────────────────────────── */
  --green-500: #10b981;
  --green-400: #34d399;
  --amber-500: #f59e0b;
  --amber-400: #fbbf24;
  --red-500: #ef4444;
  --red-400: #f87171;
  --blue-500: #3b82f6;
  --blue-400: #60a5fa;

  /* ── Surfaces ────────────────────────────────────────────── */
  --surface-sunken: var(--grey-1000);
  --surface-base: var(--grey-950);
  --surface-raised: var(--grey-900);
  /* A card that has to read as a card where `--surface-raised` cannot do it.
     On dark the two are the same, because raised is already a step off the base;
     on light both are #ffffff, so a card there is its border and nothing else.
     #f4f5f7 is the darkest step that keeps `--fg-tertiary` above AA on top of
     it: 4.56:1, against 4.36:1 one step further down. */
  --surface-card: var(--surface-raised);
  --surface-overlay: var(--grey-850);
  --surface-hover: var(--grey-800);
  --surface-active: var(--grey-750);
  --surface-selected: rgb(234 88 12 / 0.1);
  --surface-inset: var(--grey-1000);

  /* ── Borders ─────────────────────────────────────────────── */
  --border-subtle: var(--grey-850);
  --border-default: var(--grey-750);
  --border-strong: var(--grey-600);
  --border-accent: rgb(234 88 12 / 0.4);

  /* ── Foreground ──────────────────────────────────────────── */
  --fg-primary: var(--grey-050);
  --fg-secondary: var(--grey-300);
  --fg-tertiary: var(--grey-400);
  --fg-disabled: var(--grey-500);
  /* Ink on a solid accent fill, and it is dark because white does not pass on
     this orange. Measured: #ffffff on --accent-solid is 3.56:1 and on
     --accent-solid-hover 2.80:1, against the 4.5:1 AA asks for text this size;
     --grey-1000 is 5.60:1 and 7.11:1, so the hover gets clearer rather than
     worse. Darkening the orange instead was the other way out and it would have
     moved the mark and the favicon, which read the same token. */
  --fg-on-accent: var(--grey-1000);

  /* ── Accent, applied ─────────────────────────────────────── */
  --accent-fg: var(--orange-300);
  --accent-solid: var(--orange-500);
  --accent-solid-hover: var(--orange-400);
  --accent-solid-active: var(--orange-600);
  --accent-muted: rgb(234 88 12 / 0.14);
  --accent-border: rgb(234 88 12 / 0.35);

  /* ── Status, applied. Mapped onto the Run state machine ──── */
  --status-running-fg: var(--blue-400);
  --status-running-muted: rgb(59 130 246 / 0.14);
  --status-running-border: rgb(59 130 246 / 0.32);

  --status-waiting-fg: var(--amber-400);
  --status-waiting-muted: rgb(245 158 11 / 0.14);
  --status-waiting-border: rgb(245 158 11 / 0.32);

  --status-passed-fg: var(--green-400);
  --status-passed-muted: rgb(16 185 129 / 0.14);
  --status-passed-border: rgb(16 185 129 / 0.32);

  --status-failed-fg: var(--red-400);
  --status-failed-muted: rgb(239 68 68 / 0.14);
  --status-failed-border: rgb(239 68 68 / 0.32);

  --status-idle-fg: var(--grey-400);
  /* `--grey-400` at 0.16, written out because a custom property cannot carry an
     alpha of another one. It moves when the ramp moves, or it derives from a
     colour the palette no longer has. */
  --status-idle-muted: rgb(120 126 137 / 0.16);
  --status-idle-border: var(--grey-700);

  /* ── Focus ───────────────────────────────────────────────── */
  --focus-ring: rgb(249 115 22 / 0.45);
  --focus-border: var(--orange-400);

  /* ── Charts and syntax, in accent-neighbour order ────────── */
  --data-1: var(--orange-400);
  --data-2: var(--blue-400);
  --data-3: var(--green-400);
  --data-4: var(--amber-400);
  --data-5: var(--red-400);
  --data-6: var(--grey-400);
}

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

  /* The page recedes so that raised can mean raised. A card cannot be lighter
     than #ffffff, so the earlier light theme made base and raised the same
     colour and stepped the card *down* instead: same size of step as dark,
     opposite direction, and a card that reads as sunken rather than lifted.
     Stepping the page down instead keeps the direction the dark theme has and
     the size it uses, 1.06 between the two. */
  --surface-sunken: #f4f5f7;
  --surface-base: #f7f8f9;
  --surface-raised: #ffffff;
  --surface-card: var(--surface-raised);
  --surface-overlay: #ffffff;
  --surface-hover: #f4f5f7;
  --surface-active: #eceef1;
  --surface-selected: rgb(234 88 12 / 0.07);
  /* Recessed against the card it sits inside, which is now #ffffff. It was
     #f7f8f9, which is the page. */
  --surface-inset: #f4f5f7;

  --border-subtle: #eceef1;
  --border-default: #dfe2e7;
  --border-strong: #c3c8d0;
  --border-accent: rgb(234 88 12 / 0.35);

  --fg-primary: #14161a;
  --fg-secondary: #5b6270;
  --fg-tertiary: #69707e;
  --fg-disabled: #a8aeb9;
  --fg-on-accent: var(--grey-000);


  --accent-fg: var(--orange-600);
  --accent-solid: var(--orange-500);
  --accent-solid-hover: var(--orange-600);
  --accent-solid-active: #9a3412;
  --accent-muted: rgb(234 88 12 / 0.09);

  --status-running-fg: #1d64d8;
  --status-running-muted: rgb(59 130 246 / 0.1);
  --status-waiting-fg: #a55a00;
  --status-waiting-muted: rgb(245 158 11 / 0.12);
  --status-passed-fg: #047857;
  --status-passed-muted: rgb(16 185 129 / 0.1);
  --status-failed-fg: #c22626;
  --status-failed-muted: rgb(239 68 68 / 0.1);
  --status-idle-fg: #7b8290;
  --status-idle-muted: rgb(123 130 144 / 0.12);
  --status-idle-border: #dfe2e7;

  --data-1: var(--orange-500);
  --data-2: #1d64d8;
  --data-3: #047857;
  --data-4: #a55a00;
  --data-5: #c22626;
  --data-6: #7b8290;
}

/* 13px is the interface base. Crewbit's screens are lists of many small facts —
   run ids, states, paths, turn counts — and 13/18 fits them without crowding.
   Prose (a plan, a verdict, a spec body) steps up to 14/22.

   Mono is not decoration: it marks something the machine produced or something
   you can paste back into a terminal. Never use it for interface labels. */
:root {
  --font-sans: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif;
  --font-mono: "Spline Sans Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-core: var(--font-sans);

  --text-2xs: 10px;
  --text-xs: 11px;
  --text-sm: 12px;
  --text-md: 13px;
  --text-lg: 14px;
  --text-xl: 16px;
  --text-2xl: 18px;
  --text-3xl: 21px;
  --text-4xl: 26px;
  --text-5xl: 34px;
  --text-6xl: 44px;

  --leading-tight: 1.15;
  --leading-snug: 1.35;
  --leading-normal: 1.5;
  --leading-relaxed: 1.6;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Headings tighten; small labels open up. */
  --tracking-tight: -0.02em;
  --tracking-snug: -0.01em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;
  --tracking-caps: 0.06em;

  /* Composed roles */
  --type-display: var(--weight-semibold) var(--text-5xl) / var(--leading-tight) var(--font-sans);
  --type-title: var(--weight-semibold) var(--text-3xl) / var(--leading-snug) var(--font-sans);
  --type-heading: var(--weight-semibold) var(--text-xl) / var(--leading-snug) var(--font-sans);
  --type-subheading: var(--weight-semibold) var(--text-lg) / var(--leading-snug) var(--font-sans);
  --type-body: var(--weight-regular) var(--text-lg) / var(--leading-relaxed) var(--font-sans);
  --type-ui: var(--weight-regular) var(--text-md) / var(--leading-snug) var(--font-sans);
  --type-ui-medium: var(--weight-medium) var(--text-md) / var(--leading-snug) var(--font-sans);
  --type-label: var(--weight-medium) var(--text-sm) / var(--leading-snug) var(--font-sans);
  --type-caption: var(--weight-regular) var(--text-sm) / var(--leading-normal) var(--font-sans);
  --type-code: var(--weight-regular) var(--text-sm) / var(--leading-normal) var(--font-mono);
  --type-code-lg: var(--weight-regular) var(--text-md) / var(--leading-normal) var(--font-mono);
}

/* A 4px grid with a 2px half-step for optical work (icon nudges, chip padding).
   Dashboard density lives between --space-2 and --space-4; page rhythm uses
   --space-6 and up. */
:root {
  --space-0: 0;
  --space-05: 2px;
  --space-1: 4px;
  --space-15: 6px;
  --space-2: 8px;
  --space-25: 10px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;
  --space-9: 48px;
  --space-10: 64px;
  --space-11: 80px;
  --space-12: 96px;

  /* Layout constants */
  --sidebar-width: 232px;
  --sidebar-width-collapsed: 56px;
  --topbar-height: 48px;
  --row-height: 40px;
  --row-height-compact: 32px;
  --content-max: 1200px;
  --prose-max: 68ch;
  --control-height-sm: 24px;
  --control-height-md: 30px;
  --control-height-lg: 36px;
}

/* Small and consistent. Nothing in a dense dashboard should look soft: 6px is
   the default for controls and cards alike, 4px for chips inside them. */
:root {
  --radius-xs: 3px;
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;
  --radius-2xl: 16px;
  --radius-full: 999px;
}

/* On dark, elevation is mostly a lighter surface plus a hairline; the shadow is
   there to separate an overlay from what it covers, not to make cards float.
   Every shadow carries a 1px inset ring so an overlay keeps its edge on any
   background. */
:root {
  --shadow-none: none;
  --shadow-xs: 0 1px 2px rgb(0 0 0 / 0.28);
  --shadow-sm: 0 2px 4px rgb(0 0 0 / 0.3), 0 0 0 1px rgb(0 0 0 / 0.2);
  --shadow-md: 0 4px 12px rgb(0 0 0 / 0.36), 0 0 0 1px rgb(0 0 0 / 0.22);
  --shadow-lg: 0 12px 32px rgb(0 0 0 / 0.44), 0 0 0 1px rgb(0 0 0 / 0.24);
  --shadow-overlay: 0 16px 48px rgb(0 0 0 / 0.5), 0 0 0 1px rgb(0 0 0 / 0.28);
  --shadow-focus: 0 0 0 3px var(--focus-ring);
  --scrim: rgb(4 5 7 / 0.6);
}

:root[data-theme="light"] {
  --shadow-xs: 0 1px 2px rgb(16 18 22 / 0.06);
  --shadow-sm: 0 1px 3px rgb(16 18 22 / 0.08), 0 0 0 1px rgb(16 18 22 / 0.04);
  --shadow-md: 0 4px 12px rgb(16 18 22 / 0.1), 0 0 0 1px rgb(16 18 22 / 0.05);
  --shadow-lg: 0 12px 32px rgb(16 18 22 / 0.12), 0 0 0 1px rgb(16 18 22 / 0.05);
  --shadow-overlay: 0 16px 48px rgb(16 18 22 / 0.16), 0 0 0 1px rgb(16 18 22 / 0.06);
  --scrim: rgb(20 22 26 / 0.32);
}

/* Short and decelerating. Nothing bounces, nothing overshoots, nothing springs:
   this is a tool people watch work, and a playful transition on a state that
   means "your pull request failed" reads as flippant.

   Above --duration-slow, use a progress indicator instead of an animation. */
:root {
  --duration-instant: 80ms; /* @kind other */
  --duration-fast: 120ms; /* @kind other */
  --duration-normal: 160ms; /* @kind other */
  --duration-slow: 240ms; /* @kind other */

  --ease-out: cubic-bezier(0.2, 0, 0, 1); /* @kind other */
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* @kind other */
  --ease-linear: linear; /* @kind other */

  --transition-color:
    color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
  --transition-transform: transform var(--duration-normal) var(--ease-out);
  --transition-opacity: opacity var(--duration-normal) var(--ease-out);
}

/* The minimum a consuming page needs so it does not start from browser defaults. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--surface-base);
  color: var(--fg-primary);
  font: var(--type-ui);
  letter-spacing: var(--tracking-snug);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: var(--accent-fg);
  text-decoration: none;
  transition: var(--transition-color);
}

a:hover {
  color: var(--accent-solid-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}

code,
kbd,
pre,
samp {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

::selection {
  background: var(--accent-muted);
}

:focus-visible {
  outline: 2px solid var(--focus-border);
  outline-offset: 1px;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* Three labels, one shown. CSS picks it from the root attribute, so the word is
   right on the first paint and no script is needed to make it right. */
.theme {
  display: inline-flex;
  align-items: center;
  height: var(--control-height-sm);
  padding: 0 var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: transparent;
  font: var(--type-code);
  color: var(--fg-secondary);
  text-transform: capitalize;
  cursor: pointer;
  transition: var(--transition-color);
}

.theme:hover {
  border-color: var(--border-strong);
  color: var(--fg-primary);
}

.theme > span {
  display: none;
}

:root:not([data-theme]) .theme-system,
:root[data-theme="light"] .theme-light,
:root[data-theme="dark"] .theme-dark {
  display: inline;
}
/* The operator pages, written in the names tokens/ declares. This file is ours,
   not the design project's: what that ships is React components, and what this
   server renders is strings. It sits outside tokens/ so Biome formats it and so
   re-syncing the design stays a diff of eight files. */

body {
  max-width: var(--content-max);
  margin: var(--space-7) auto;
  padding: 0 var(--space-4);
}

/* A page inside the shell is full-bleed: the shell owns the scrolling well, and
   a centred column inside a sidebar layout is a second, competing frame. */
body:has(.shell) {
  max-width: none;
  margin: 0;
  padding: 0;
}

h1 {
  font: var(--type-heading);
  margin: 0;
}

/* The wordmark stays neutral; base.css paints every other anchor with the accent. */
h1 a {
  color: var(--fg-primary);
}

header {
  border-bottom: 1px solid var(--border-default);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
}

.meta {
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin: var(--space-05) 0;
}

/* A Run's state is the one thing on a row worth finding first, so it holds the
   primary foreground while everything around it in .meta is secondary. */
.state {
  font: var(--type-ui-medium);
  color: var(--fg-primary);
}

.job {
  font-style: italic;
}

.empty {
  color: var(--fg-tertiary);
}

pre {
  font: var(--type-code);
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--surface-inset);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.events {
  list-style: none;
  padding: 0;
}

.events li {
  padding: var(--space-05) 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-3) 0;
}

/* #219: `flex` is still block-level - a lone visible button in one of these
   forms started a new line after whatever sat before it regardless of width,
   which on a capability row put the remove button under the badge and close
   enough to the row below it that a tap could land there instead. */
.inline {
  display: inline-flex;
  gap: var(--space-1);
}

/* #219: the badge and the button (or form) in a capability row, as one flex
   row rather than two inline things a narrow column could wrap - `justify-
   content: flex-end` is what right-aligns the pair, since the cell's own
   `text-align: right` has nothing left to align once its content is a block
   flex child. */
.capability-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* #222: the same shape as `.capability-actions`, for the Specs row - a Run at
   `ingested` reads its badge and its "Plan it" button as one line rather
   than the button falling to a second one under a plain `<form>`'s own
   block-level default.

   #290: `flex-wrap` rather than `nowrap`'s default, for the Spec page's own
   card - narrower than the list's table cell, it cannot always hold the
   badge and both buttons on one line. The buttons stay paired (this class
   nests, #288: an outer `.spec-actions` holds the badge and an inner one
   holding both buttons together) and drop to their own row under the badge
   rather than each shrinking below its label's own width, which is what
   `white-space: nowrap` on `.button` above no longer allows - it wrapped
   the label itself instead, "Run" over "it". Harmless on the list, where
   the column already has room or is hidden below 1000px. */
.spec-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-2);
}

button,
input {
  font: var(--type-ui);
  color: var(--fg-primary);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: 0 var(--space-3);
  height: var(--control-height-md);
  transition: var(--transition-color);
}

button:hover {
  background: var(--surface-hover);
  border-color: var(--border-strong);
}

input {
  min-width: 0;
}

.mono {
  font-family: var(--font-mono);
}

/* Before anything that scopes an icon, so the plain selector is not the more
   specific one arriving later. */
.icon {
  flex: none;
}

/* ── Tone ─────────────────────────────────────────────────────
   A tone class sets three variables and paints nothing itself, so a badge, a
   bar, a pipeline step and a card edge all read the same status the same way
   without any of them naming a colour. */

.tone-running {
  --tone-fg: var(--status-running-fg);
  --tone-muted: var(--status-running-muted);
  --tone-border: var(--status-running-border);
}

.tone-waiting {
  --tone-fg: var(--status-waiting-fg);
  --tone-muted: var(--status-waiting-muted);
  --tone-border: var(--status-waiting-border);
}

.tone-passed {
  --tone-fg: var(--status-passed-fg);
  --tone-muted: var(--status-passed-muted);
  --tone-border: var(--status-passed-border);
}

.tone-failed {
  --tone-fg: var(--status-failed-fg);
  --tone-muted: var(--status-failed-muted);
  --tone-border: var(--status-failed-border);
}

.tone-idle {
  --tone-fg: var(--fg-secondary);
  --tone-muted: var(--status-idle-muted);
  --tone-border: var(--status-idle-border);
}

.tone-accent {
  --tone-fg: var(--accent-fg);
  --tone-muted: var(--accent-muted);
  --tone-border: var(--accent-border);
}

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

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  height: 100vh;
}

/* No `overflow-y: auto` here, deliberately. The open switcher is absolutely
   positioned and wider than this column, and any overflow on an ancestor clips
   it: the list would be cut off at the sidebar's edge. The nav is two entries,
   so there is nothing to scroll; a longer one puts the scroll on the groups
   rather than back on here. */
.sidebar {
  display: flex;
  flex-direction: column;
  background: var(--surface-sunken);
  border-right: 1px solid var(--border-subtle);
}

/* The mark stays; the name beside it is the active Project rather than the
   product, because everything below is scoped to one. The square is the
   placeholder mark the brand guideline draws, and it is all that is left of the
   wordmark here. */
/* The anchor the open list hangs off, and the reason the sidebar does not scroll
   it away: `overflow-y: auto` on an ancestor would clip an absolute child. */
.switcher {
  flex: none;
  position: relative;
  border-bottom: 1px solid var(--border-subtle);
}

.switcher > summary,
.switcher:not([open]) > summary,
div.switcher {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--topbar-height);
  padding: 0 var(--space-3);
  color: var(--fg-primary);
  cursor: pointer;
  list-style: none;
}

.switcher > summary::-webkit-details-marker {
  display: none;
}

.switcher > summary:hover {
  background: var(--surface-hover);
}

.switcher .mark {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--accent-solid);
}

.switcher-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.switcher-text strong {
  font: var(--type-ui-medium);
  letter-spacing: var(--tracking-tight);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.switcher .icon {
  color: var(--fg-tertiary);
}

/* Over the sidebar rather than inside it. Expanding in flow pushed Runners and
   All projects down the page, which moves the thing you were about to click. */
.switcher-list {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 10;
  width: 264px;
  display: flex;
  flex-direction: column;
  padding: var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  background: var(--surface-overlay);
  box-shadow: var(--shadow-overlay);
}

.switcher-list a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-15) var(--space-2);
  border-radius: var(--radius-md);
  color: var(--fg-secondary);
  font: var(--type-ui);
  min-width: 0;
}

.switcher-list a:hover {
  background: var(--surface-hover);
  color: var(--fg-primary);
  text-decoration: none;
}

.switcher-list a[aria-current] strong {
  color: var(--accent-fg);
}

.entry-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.entry-text strong {
  font: var(--type-ui-medium);
  color: var(--fg-primary);
}

/* One line that clips: a Project with four sources would otherwise set the width
   of the whole list. */
.entry-text .meta,
.switcher-text .meta {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Scoped rather than `!important`: two classes beat the one-class rule above it. */
.switcher-list .switcher-manage {
  margin-top: var(--space-1);
  padding-top: var(--space-25);
  border-top: 1px solid var(--border-subtle);
  border-radius: 0;
}

.nav-group {
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
}

.nav-group.nav-group--project {
  flex: 1;
}

.nav-title {
  font: var(--type-label);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--fg-tertiary);
  margin: 0 0 var(--space-2) var(--space-2);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--row-height-compact);
  padding: 0 var(--space-2);
  border-radius: var(--radius-md);
  color: var(--fg-secondary);
  font: var(--type-ui);
  transition: var(--transition-color);
}

/* #199: excludes .disabled, or hovering a span that goes nowhere still
   painted the hover surface and --fg-primary over it, reading as enabled. */
.nav-item:not(.disabled):hover {
  background: var(--surface-hover);
  color: var(--fg-primary);
  text-decoration: none;
}

.nav-item.here {
  background: var(--surface-selected);
  color: var(--accent-fg);
}

/* #189: the onboarding shape - an entry drawn where it will open up, not
   hidden, while the step it waits on has not happened yet.
   #199: --fg-disabled, not --fg-tertiary, which sat one step from the
   enabled --fg-secondary and read the same in a screenshot. */
.nav-item.disabled {
  color: var(--fg-disabled);
  cursor: not-allowed;
}

.nav-item span:first-of-type {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav-count {
  font: var(--type-code);
  color: var(--tone-fg, var(--fg-tertiary));
}

.sidebar-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border-top: 1px solid var(--border-subtle);
}

.sidebar-foot .meta {
  flex: 1;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Round, and the only round thing in the interface: a picture of a person reads
   as one, and every other 22px square here is the mark. */
.sidebar-foot .avatar {
  flex: none;
  border-radius: var(--radius-full);
  background: var(--surface-hover);
}

.well {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: var(--topbar-height);
  flex: none;
  padding: 0 var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
  margin: 0;
}

.crumb-project {
  display: inline-flex;
  align-items: center;
  gap: var(--space-15);
  font: var(--type-ui);
  color: var(--fg-secondary);
}

/* Back for the theme control, which is the one thing on the right of the bar.
   It went when the spec search and `Plan a spec` did, because it was pushing
   those two and nothing else. */
.spacer {
  flex: 1;
}

.content {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5) var(--space-6) var(--space-9);
}

/* ── Buttons ──────────────────────────────────────────────── */

.button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-15);
  height: var(--control-height-md);
  padding: 0 var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--fg-primary);
  font: var(--type-ui-medium);
  cursor: pointer;
  transition: var(--transition-color);
  /* #290: a label's own two words wrapped to "Run" over "it" the moment a
     flex row around it ran out of space, since nothing here reset the
     `white-space: normal` a plain `<div>` (unlike a table `td`) leaves in
     force. A button's label is never meant to wrap; the row it sits in is
     what gives, through `.spec-actions`'s own `flex-wrap` below. */
  white-space: nowrap;
}

.button:hover {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  text-decoration: none;
}

/* #243: without this, a disabled button reads exactly like an enabled one -
   `.button` sets `color` unconditionally, so the browser's own disabled
   dimming never shows through. Same token as `.nav-item.disabled`, #199. */
.button:disabled {
  color: var(--fg-disabled);
  cursor: not-allowed;
}

.button:disabled:hover {
  background: var(--surface-raised);
  border-color: var(--border-default);
}

.button.primary {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--fg-on-accent);
}

.button.primary:hover {
  background: var(--accent-solid-hover);
  border-color: var(--accent-solid-hover);
}

/* The foot is chrome rather than content, so the one control on it is the small
   size: a full `--control-height-md` button reads as the page's own action. */
.sidebar-foot .button {
  height: var(--control-height-sm);
  padding: 0 var(--space-2);
  font: var(--type-caption);
}

/* ── Badges ───────────────────────────────────────────────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font: var(--type-label);
  padding: var(--space-05) var(--space-2);
  border-radius: var(--radius-full);
  color: var(--tone-fg, var(--fg-secondary));
  background: var(--tone-muted, var(--status-idle-muted));
  border: 1px solid var(--tone-border, var(--border-default));
  white-space: nowrap;
}

a.badge:hover {
  text-decoration: none;
  border-color: var(--tone-fg);
}

.badge .dot {
  width: 5px;
  height: 5px;
  border-radius: var(--radius-full);
  background: currentColor;
}

/* #212: the dot-and-count reading of the same badge, for a topbar with no room
   for "3 busy · 0 runners". A second element rather than a span around half the
   text, because a page test elsewhere reads the full sentence as one
   unbroken string, and that string is real regardless of screen width. */
.badge-compact {
  display: none;
}

/* ── Cards ────────────────────────────────────────────────── */

.card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  /* Not `--surface-raised`: on light that is `#ffffff` and so is the page, and
     a card there would be its border and nothing else. `--surface-card` is the
     token that has an answer on both themes. */
  background: var(--surface-card);
  margin-bottom: var(--space-4);
  overflow: hidden;
}

/* The one card asking a question gets the coloured edge. Nothing else does, so
   that when something is waiting on you it is the only thing on the page that
   looks like it is. */
.card.edge-waiting {
  border-color: var(--status-waiting-border);
}

/* The Project the sidebar is scoped to. The same edge, for the same reason: it
   is the one card on the page that is about where you already are. */
.card.edge-accent {
  border-color: var(--accent-border);
}

.card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  /* It is a `<header>`, so the bare element rule above hands it a margin the
     card does not want: a gap between the hairline and the body. */
  margin-bottom: 0;
}

/* #204: a header with nothing under it is the whole card, and the hairline
   above would just sit on top of the card's own edge. */
.card-head:last-child {
  border-bottom: none;
}

/* The title block takes the slack so the actions sit against the far edge
   rather than trailing the words. */
.card-head > div:first-child {
  flex: 1;
  min-width: 0;
}

.card-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.card-head h3 {
  font: var(--type-subheading);
  margin: 0;
}

/* A pencil is a control that acts on the thing beside it, so it is quiet until
   the row or the card is under the pointer. A button in a table that has words
   on it is an action of its own and opts out with `act`. */
.card-title .button,
td .button:not(.act) {
  height: var(--control-height-sm);
  padding: 0 var(--space-1);
  border-color: transparent;
  background: none;
  color: var(--fg-tertiary);
}

.card-title .button:hover,
td .button:not(.act):hover {
  border-color: var(--border-default);
  color: var(--fg-primary);
  background: var(--surface-hover);
}

.card-head p {
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin: var(--space-05) 0 0;
  max-width: var(--prose-max);
}

.card-body {
  padding: var(--space-4);
}

.card-body.flush {
  padding: 0;
}

/* What this source costs when a command is missing, on the card it is about. */
.card-foot {
  border-top: 1px solid var(--border-subtle);
  padding: var(--space-3) var(--space-4);
  font: var(--type-caption);
  color: var(--fg-tertiary);
}

/* #224: the rules badge, its link when missing, and the "Check again" button
   as one flex row - a flex row rather than inline text left to wrap, the
   same reasoning `.capability-actions` and `.spec-actions` already carry. */
.rules-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.card-body > :last-child {
  margin-bottom: 0;
}

/* #187: an action that submits by fetch and stays on the page shows its own
   refusal here, beside the card rather than on a separate one. */
.card-error {
  padding: var(--space-2) var(--space-4);
  font: var(--type-caption);
  color: var(--status-failed-fg);
}

/* ── The Run page ─────────────────────────────────────────── */

.page-head {
  margin-bottom: var(--space-5);
}

.crumb {
  display: flex;
  align-items: center;
  gap: var(--space-15);
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin: 0 0 var(--space-2);
}

.crumb a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-05);
  color: var(--fg-tertiary);
}

.crumb .sep {
  color: var(--border-strong);
}

.page-title {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
}

.page-title h2 {
  flex: 1;
  min-width: 0;
  font: var(--type-title);
  letter-spacing: var(--tracking-tight);
  margin: 0;
}

/* #298: `flex: none` kept Refresh, Sync, Plan again, Code it again and End
   run to one line, so the last one was cut at the right edge the moment
   they did not fit 412px - the same shape `.spec-actions` above already
   fixed. `.button`'s own `white-space: nowrap` (#290) still keeps a label
   from wrapping into two lines; only the row gives. */
.head-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.badges {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-3) 0 0;
}

/* #275: a source key has no spaces to fall back to, so `overflow-wrap:
   anywhere` is what lets a long one wrap once it no longer fits its line,
   the same reasoning `.dialog h3` already applies to a repository name. */
.badges .branch {
  overflow-wrap: anywhere;
}

/* #275: six links, none of them optional, wider together than a narrow
   viewport - scrolls inside its own box rather than forcing the page wider,
   the site rule every other wide row already follows. */
.tabs {
  display: flex;
  gap: var(--space-4);
  border-bottom: 1px solid var(--border-default);
  margin-top: var(--space-4);
  overflow-x: auto;
}

.tabs a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-15);
  font: var(--type-ui-medium);
  color: var(--fg-tertiary);
  padding: var(--space-2) 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.tabs a:hover {
  color: var(--fg-secondary);
  text-decoration: none;
}

.tabs a[aria-current] {
  color: var(--fg-primary);
  border-bottom-color: var(--accent-solid);
}

.tab-count {
  font: var(--type-code);
  color: var(--fg-tertiary);
}

/* A main column and a rail beside it, which the Run and the Sources pages both
   are. The rail drops under the main column rather than shrinking with it: a
   plan read at 300px is a plan nobody reads, and so is a capability table. */
.run,
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--space-5);
  align-items: start;
}

@media (max-width: 1000px) {
  .run,
  .split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The cards in the main column, which the Run page's `main` does not need
   because its own children carry the gap. */
.split > main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.gate-detail {
  font: var(--type-body);
  color: var(--fg-secondary);
  max-width: var(--prose-max);
  margin: 0 0 var(--space-4);
}

.gate-detail code {
  color: var(--fg-primary);
}

.choices {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.choices form {
  display: flex;
  gap: var(--space-2);
}

.reject input {
  min-width: 15rem;
}

/* ── Machine text ─────────────────────────────────────────── */

.code {
  margin: 0 0 var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  overflow: hidden;
}

.code figcaption {
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  font: var(--type-code);
  color: var(--fg-tertiary);
}

.code pre {
  margin: 0;
  border: none;
  border-radius: 0;
  background: none;
  padding: var(--space-4);
  color: var(--fg-secondary);
}

/* ── Lists ────────────────────────────────────────────────── */

.lines {
  list-style: none;
  padding: 0;
  margin: 0;
}

.lines li {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  font: var(--type-code);
}

.lines li:last-child {
  border-bottom: none;
}

/* One line that clips: a list that reflows while Runs update is unreadable
   exactly while it is changing. */
.lines .what {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-secondary);
}

/* #273: a failed tool result, told apart from a passing one on the same line. */
.lines .what.tone-failed {
  color: var(--tone-fg);
}

.lines .when,
.lines .stage {
  flex: none;
  color: var(--fg-tertiary);
}

.lines .meta {
  flex: none;
  margin: 0;
}

/* ── The rail ─────────────────────────────────────────────── */

.rail .card-body {
  padding: var(--space-3) var(--space-4);
}

.stage-badges {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.rail dl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2) var(--space-3);
  margin: 0;
  font: var(--type-caption);
}

.rail dt {
  color: var(--fg-tertiary);
}

.rail dd {
  margin: 0;
  color: var(--fg-secondary);
  min-width: 0;
  overflow-wrap: anywhere;
}

.rail dd .meta {
  display: block;
  margin-top: var(--space-1);
}

.pipeline {
  list-style: none;
  padding: 0;
  margin: 0;
}

.pipeline li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font: var(--type-code);
  color: var(--tone-fg, var(--fg-tertiary));
}

/* ── Bars and figures ─────────────────────────────────────── */

.bar-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin-bottom: var(--space-15);
}

.track {
  height: var(--space-1);
  border-radius: var(--radius-full);
  background: var(--surface-active);
  overflow: hidden;
}

.track span {
  display: block;
  height: 100%;
  background: var(--tone-fg);
}

.ticks {
  display: flex;
  gap: var(--space-1);
}

.ticks span {
  flex: 1;
  height: var(--space-1);
  border-radius: var(--radius-full);
  background: var(--surface-active);
}

.ticks span.on {
  background: var(--tone-fg);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-05);
}

.stat-label {
  font: var(--type-caption);
  color: var(--fg-tertiary);
}

.stat-value {
  font: var(--weight-semibold) var(--text-3xl) / var(--leading-tight) var(--font-mono);
  color: var(--fg-primary);
}

.stat-unit {
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin-left: var(--space-1);
}

.stat-hint {
  font: var(--type-caption);
  color: var(--fg-tertiary);
}

/* ── Projects ─────────────────────────────────────────────── */

.lede {
  font: var(--type-body);
  color: var(--fg-secondary);
  max-width: var(--prose-max);
  margin: var(--space-2) 0 0;
}

/* Below the flexible column's floor the wrapper scrolls with every column
   intact: a source list with no source name in it is worse than a scrollbar. */
/* A row is a link, and a table painted accent-on-every-cell has no hierarchy
   left to spend. The design makes the whole row clickable and colours none of
   it; a real anchor taking the cell's colour is the closest honest thing a page
   with no script can do. */
.table-wrap td a {
  color: inherit;
}

.table-wrap td a:hover {
  color: var(--accent-fg);
}

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font: var(--type-ui);
}

th {
  text-align: left;
  font: var(--type-label);
  color: var(--fg-tertiary);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  /* #212: a column squeezed by `table-layout: fixed` bled its label into the
     next one - `td` already clips this way, and a header naming a column too
     narrow to hold it needs the same rather than a table only `td` behaves on. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

td {
  padding: 0 var(--space-4);
  height: var(--row-height);
  border-bottom: 1px solid var(--border-subtle);
  color: var(--fg-secondary);
  /* One line that clips. A row that reflows while Runs update is unreadable
     exactly while it is changing. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

tbody tr:last-child td {
  border-bottom: none;
}

th.right,
td.right {
  text-align: right;
}

/* #208: `flex` rather than `inline-flex` - the row's own ellipsis needs a
   definite width to truncate against, which only the block-level box gets
   from the `td` around it. Without it a long name hard-clipped mid-word
   instead of reading "…". */
.cell {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.cell .mono {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.note {
  border-top: 1px solid var(--border-subtle);
}

.note .meta {
  margin: 0 0 var(--space-3);
}

/* ── Dialog ───────────────────────────────────────────────── */

/* The one component that casts a real shadow. Everything else on dark uses a
   raised surface and a hairline, so the shadow is what says this covers the
   page rather than sitting on it. */
.dialog {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  background: var(--surface-overlay);
  color: var(--fg-primary);
  box-shadow: var(--shadow-overlay);
  padding: var(--space-5);
  width: min(28rem, calc(100vw - var(--space-8)));
  /* #219: `dialog` stays where it is in the DOM even though `showModal()`
     paints it in the top layer, so a `<dialog>` sitting inside a `td.right`
     still *inherits* from it - `white-space: nowrap`, there so a row clips
     to one line while it updates rather than reflowing, and `text-align:
     right`, there for a number column - both followed it in, and a title
     naming a long repository rendered right-aligned as one unbroken line
     past the box `width` above had already correctly shrunk to. Every
     dialog opens from some row or card, so this resets both inheritances
     here rather than chasing them at each call site. */
  white-space: normal;
  text-align: left;
}

.dialog::backdrop {
  background: var(--scrim);
}

/* A repository name has no spaces to fall back to, so `overflow-wrap:
   anywhere` is what lets a long one break at all once `white-space: normal`
   above allows breaking in the first place. */
.dialog h3 {
  font: var(--type-subheading);
  margin: 0;
  overflow-wrap: anywhere;
}

.dialog-lede {
  font: var(--type-caption);
  color: var(--fg-tertiary);
  margin: var(--space-2) 0 var(--space-5);
  overflow-wrap: anywhere;
}

.dialog-close {
  float: right;
}

.dialog-close button {
  background: none;
  border: none;
  color: var(--fg-tertiary);
  padding: 0;
  height: auto;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

.field label {
  font: var(--type-label);
  color: var(--fg-secondary);
}

/* Two commands side by side, one line each. They are read together: what a
   checkout needs before anything runs, and what decides the change afterwards. */
.commands {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

@media (max-width: 700px) {
  .commands {
    grid-template-columns: 1fr;
  }
}

.commands .field {
  margin-bottom: 0;
}

.commands input {
  width: 100%;
}

/* Left, where the design puts it: the card is the thing being edited, so the
   action sits under the fields rather than in the corner of a dialog. */
.toggle {
  display: flex;
  align-items: start;
  gap: var(--space-2);
}

.toggle input {
  height: auto;
  margin-top: var(--space-05);
  accent-color: var(--accent-solid);
}

.toggle label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font: var(--type-ui);
  color: var(--fg-primary);
}

.form-foot {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* The switches stack, and the label under them is a field like any other. */
form > .toggle + .toggle {
  margin-top: var(--space-3);
}

form > .toggle + .field {
  margin-top: var(--space-4);
}

.field select {
  font: var(--type-ui);
  color: var(--fg-primary);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  height: var(--control-height-md);
  padding: 0 var(--space-2);
}

.hint {
  font: var(--type-caption);
  color: var(--fg-tertiary);
}

/* Cancel first, the committing action last. */
.dialog-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

/* ── Prose ────────────────────────────────────────────────── */

/* A plan, rendered. Only what `components/markdown.ts` can emit is styled here:
   anything else on the page would be a construct the parser does not produce. */
.prose {
  max-width: var(--prose-max);
  color: var(--fg-secondary);
}

.prose > :first-child {
  margin-top: 0;
}

.prose > :last-child {
  margin-bottom: 0;
}

.prose h3,
.prose h4,
.prose h5 {
  margin: var(--space-6) 0 var(--space-3);
  color: var(--fg-primary);
}

.prose p {
  margin: 0 0 var(--space-4);
}

.prose ul,
.prose ol {
  margin: 0 0 var(--space-4);
  padding-left: var(--space-6);
}

.prose li {
  margin-bottom: var(--space-2);
}

.prose code {
  font: var(--type-code);
  color: var(--fg-primary);
}

/* The fenced blocks a plan carries: the same surface as `.code`, without the
   caption, because a fence inside a plan has no filename to name. */
.prose pre {
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  overflow-x: auto;
}

/* A stage reports its commits as a table, and a work item's body carries them
   too. Scrolls inside itself: the column is 68ch and a table is not. */
.prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--space-4);
  font: var(--type-caption);
  display: block;
  overflow-x: auto;
}

.prose th,
.prose td {
  border-bottom: 1px solid var(--border-subtle);
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
  text-align: left;
  vertical-align: top;
}

.prose th {
  color: var(--fg-primary);
  white-space: nowrap;
}

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

.prose pre code {
  color: var(--fg-secondary);
}

/* ── Sign in ──────────────────────────────────────────────── */

/* The one page outside the shell, so it centres itself: `body` keeps its
   `--content-max` column here, and the column is wider than anything on it.
   #211: `body`'s own vertical margin sat outside this block, so the block's
   own centre landed `--space-7` below the viewport's the moment `.signin`
   filled it - cleared here the same reason `body:has(.shell)` clears it,
   because it is not this block's own spacing, which stays on `.signin`
   itself below. */
body:has(.signin) {
  margin-top: 0;
  margin-bottom: 0;
}

.signin {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-7) var(--space-4);
  text-align: center;
  min-height: 100vh;
}

/* `dvh` excludes a mobile browser's own chrome from the viewport unit, which
   `vh` above counts and leaves the block short of centred by exactly that
   much. Behind a feature query rather than the same property declared twice
   relying on the browser to skip the one it does not understand: the
   fallback is what a browser without `dvh` keeps either way, and this way
   the lint that catches an actual duplicate stays on. */
@supports (height: 100dvh) {
  .signin {
    min-height: 100dvh;
  }
}

/* The sidebar's mark at the sidebar's size, which is also the favicon's ratio. */
.signin .mark {
  width: 22px;
  height: 22px;
  border-radius: var(--radius-sm);
  background: var(--accent-solid);
}

/* #211: `align-items: center` does not shrink a child to its container - a
   centred flex item wider than what is left overflows one side instead, and
   42ch is wider than body's own padding leaves at 412px. `min()` is the cap
   `.signin`'s own centring needs; the block still reads at the width the
   design picked everywhere wider than that. */
.signin-lede {
  font: var(--type-body);
  color: var(--fg-secondary);
  max-width: min(42ch, 100%);
  margin: 0 0 var(--space-2);
}

/* Stacked, because the first way in is the one most people want and a row of
   equal buttons says they are equally likely. */
.signin-ways {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 15rem;
}

.signin-ways .button {
  justify-content: center;
}

.signin-problem {
  font: var(--type-caption);
  color: var(--fg-secondary);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-1);
}

/* ── Empty ────────────────────────────────────────────────── */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--fg-tertiary);
}

.empty-title {
  font: var(--type-ui-medium);
  color: var(--fg-secondary);
  margin: 0;
}

.empty-body {
  font: var(--type-caption);
  max-width: 42ch;
  margin: 0;
}

/* #198: the one empty state with something to do about it reads at heading
   size, same rule `body:has(.shell)` already uses rather than a variant prop
   threaded through every other emptyState() caller. */
.empty-state:has(.empty-actions) .empty-title {
  font: var(--type-heading);
}

.empty-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.empty-actions form {
  display: contents;
}

/* #204: the create-first-project form is a real row, not the loose buttons
   `.empty-actions` flattens everything else into - the higher specificity
   here is what wins it back from the rule above. `text-align` resets
   `.empty-state`'s own centring, which is what put the label text in the
   middle of a field wider than it, over a left-anchored input. */
.empty-actions .first-project-form {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  align-items: end;
  column-gap: var(--space-3);
  text-align: left;
}

.first-project-form .field {
  margin-bottom: 0;
}

.first-project-form .hint {
  grid-column: 2;
  margin: var(--space-1) 0 0;
}

/* ── Runners ──────────────────────────────────────────────── */

/* 220px is the design's own floor: narrower than that and the slot ticks wrap
   under their caption, which reads as two controls instead of one. */
.runner-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.runner-grid .card {
  margin-bottom: 0;
}

.runner-grid .card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Label left, value right. `.rail dl` is the same idea in a two-column grid,
   and stays that way: there the values are long enough to wrap. */
.pairs {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-caption);
}

.pairs dt {
  color: var(--fg-tertiary);
}

.pairs dd {
  margin: 0;
  color: var(--fg-secondary);
  text-align: right;
}

.held {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  background: var(--surface-inset);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  font: var(--type-caption);
  text-decoration: none;
}

.held:hover {
  border-color: var(--border-strong);
}

.held .mono {
  flex: none;
  color: var(--fg-tertiary);
}

/* One line that clips: a title long enough to wrap would change the card's
   height while the grid is laid out beside other cards. */
.held .what {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-secondary);
}

.held-none {
  margin: 0;
  font: var(--type-caption);
  color: var(--fg-tertiary);
}

.secret {
  margin: 0;
  padding: var(--space-3);
  background: var(--surface-inset);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  font: var(--type-code);
  color: var(--fg-primary);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* The title takes whatever the fixed columns leave and clips rather than wraps:
   a row that grows to two lines while the list refreshes is a list that moves
   under the cursor. */
.run-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

.run-title > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-title .meta {
  flex: none;
}

/* The one native control on these pages, painted to match the buttons beside
   it. `appearance: none` takes the platform's chrome off; the arrow is drawn
   here because a select with none reads as a text field. */
.filter {
  margin-bottom: var(--space-4);
}

.filter select {
  appearance: none;
  font: var(--type-ui);
  color: var(--fg-secondary);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-7) var(--space-2) var(--space-3);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--fg-tertiary) 50%),
    linear-gradient(135deg, var(--fg-tertiary) 50%, transparent 50%);
  background-position:
    right var(--space-4) center,
    right var(--space-3) center;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}

.filter select:hover {
  border-color: var(--border-strong);
  color: var(--fg-primary);
}

/* The state cell is the one allowed to grow. A second badge appears there only
   when something is wrong — a Job queued behind another, a runner gone — and
   widening the column for every row to fit the rare one pays that width always.
   Wrapping makes the row taller exactly when it should be catching the eye. */
.table-wrap td:has(.badge + .badge) {
  white-space: normal;
  line-height: 1.8;
}

/* #218: both sized here rather than through `table()`'s own inline `style`,
   like every other column - an inline style only yields to a stylesheet rule
   marked `!important`, and reading a second, narrower width for the 720-
   1000px range below is simpler as a second plain rule than an `!important`
   override of an inline one.

   #289: the action column widened at this, the desktop width, with the room
   #288's Labels removal freed - it was clipping the state badge to "lanned"
   because #288 put "Code it" in the same cell as "Plan it" and the badge,
   three things sharing what used to fit one badge and one button. 23rem is
   measured, not guessed: the widest real row (the "Waiting to be planned"
   badge plus both buttons, the only state that ever shows all three at
   once) needs 351px unclipped, checked by cloning that cell's own markup
   into an unconstrained probe element and reading its natural width - 23rem
   is 368px, a 17px margin.

   Source stays at its own `table-layout: fixed` remainder rather than
   taking a second, larger share: widening it to show a whole repository
   name at this width came at Title's expense, measured at 998px total for
   this table - the same three fixed columns plus the action column above
   left Title only 118px, clipped to "Bump t…" on every row. Title is the
   column a person reads and keeps at least the 294px it had on `main`
   before this issue, and Source - already truncating with its own ellipsis
   before this change, on a repository name as ordinary as "caju-
   beneficios/ai-orchestrator" - is what gives the room back, the same way
   it already does below 1000px. Left alone below 1000px, where Labels was
   already hidden and so never had width to give back. */
.spec-col-source {
  width: 13rem;
}

.spec-col-action {
  width: 23rem;
}

/* #251: only ever "Run now" or nothing, never a badge, so this does not need
   `.spec-col-action`'s 15rem - sized for that button alone, with room to
   spare, so the column never shifts between a row that has the action and
   one that does not. */
.runs-col-action {
  width: 7rem;
}

/* #218: at 1000px there still is not room for Source, Author, the action
   column and a title worth reading. Source stays and truncates with its own
   ellipsis - the one thing telling two rows from the same Project apart
   when they come from different repositories - Author is what a person can
   act on least at this width, the same reasoning the phone rule below
   applies to it and Source both. Below 720px it joins Source in going, and
   title wraps instead of truncating. Labels is gone from the table
   entirely, #289, so there is nothing left to name here. */
@media (max-width: 1000px) {
  .spec-col-author {
    display: none;
  }
}

/* #218: a `min-width` on the title column itself did nothing - table-layout:
   fixed does not treat a cell's own `min-width` as a floor, only the width
   of the table (and so the box `.table-wrap` scrolls) resolving past its
   container does. Measured at 750px with no floor: the title computed to
   52px, its header clipped to "T…" and every row to its number alone.

   Between 720 and 1000px Labels and Author are already gone (the rule
   above); Source and the action column are narrower here too, 9rem and
   10rem rather than their 11rem and 15rem above 1000px - enough for "Plan
   it" and the shorter state badges, a longer one accepted to clip here
   rather than solved, which is what leaves the title its full 12rem rather
   than the first word or two of it. 31rem (9 + 10 + 12) is the width below
   which the table itself, not just the title, has to give: `.table-wrap`
   starts scrolling with the title still readable and "Plan it" reachable
   rather than either vanishing. Below 720px the phone rule takes over and
   this stops applying, Source having joined Labels and Author in going. */
@media (min-width: 721px) and (max-width: 1000px) {
  .spec-col-source {
    width: 9rem;
  }

  .spec-col-action {
    width: 10rem;
  }

  .specs-table {
    min-width: 31rem;
  }
}

/* ── Phone layout, #212 ──────────────────────────────────────
   Below 720px `.shell` is one column. Kept last in the file on purpose: most
   of these rules share a selector with one defined earlier (`.first-project-
   form`'s grid columns, `.page-title`'s wrap), and two rules of equal
   specificity resolve by source order, not by which one sits in a media
   query.

   `.sidebar-bar` is new and holds only the switcher - `.menu` is its sibling,
   not its child, because `.sidebar` is already the column flex box the wide
   layout relies on, and a `<details>` next to a switcher growing to fill a
   row starves it of width, confining its body to a sliver instead of the
   full bar. Stacking them as siblings costs nothing above 720px either: it
   is the same switcher-then-groups order the wide layout always rendered.

   `.menu-body` is a sibling of `.menu` too, not its child: closing a
   `<details>` hides everything but `summary` through a mechanism no author
   stylesheet can override, so nothing that has to stay always-visible above
   720px can live inside one. `.menu[open] ~ .menu-body` reads the attribute
   as a plain CSS selector instead, which a `display` rule below 720px can
   read and one above it can simply ignore. */
.sidebar-bar {
  display: flex;
  align-items: center;
}

.menu > summary {
  list-style: none;
  cursor: pointer;
}

.menu > summary::-webkit-details-marker {
  display: none;
}

/* Closed is `.menu`'s real default, with nothing turning it on for the first
   render - the collapsed drawer this attribute selector reads is the native
   state of a `<details>` nobody has opened yet, not a rule undone later. */
.menu-body {
  display: none;
  flex: 1;
  flex-direction: column;
}

.menu[open] ~ .menu-body {
  display: flex;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border-subtle);
  z-index: 100;
}

/* Present for a reader who cannot see the icon, gone for one who can -
   `.menu > summary` never gets a text label of its own on screen. */
.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;
}

@media (min-width: 721px) {
  /* The wide layout never had a toggle, and `.menu` is nothing else now that
     `.menu-body` moved out of it - hiding the whole element rather than just
     `summary` costs nothing, and forcing `.menu-body` on ignores whatever
     `[open]` happens to be, rather than depending on it staying unset. */
  .menu {
    display: none;
  }

  .menu-body {
    display: flex;
  }
}

@media (max-width: 720px) {
  .shell {
    position: relative;
    display: block;
    height: auto;
    min-height: 100vh;
  }

  .sidebar {
    position: relative;
    width: 100%;
    border-right: none;
    border-bottom: 1px solid var(--border-subtle);
    --topbar-reserve: 6.5rem;
  }

  /* `overflow: hidden` clips a descendant against its own box regardless of
     what that descendant is positioned against - `.topbar` renders above
     `.well`'s own top edge now, and stayed clipped there until this. */
  .well {
    overflow: visible;
  }

  /* `--topbar-reserve` is what `.topbar` needs beside the menu toggle -
     the dot-and-number badge, the gap, the one-word theme control and its
     own padding - kept a token so the two rules that need it agree. */
  .switcher,
  div.switcher {
    flex: 1;
    min-width: 0;
    padding-right: calc(var(--topbar-height) + var(--topbar-reserve));
    border-bottom: none;
  }

  /* #239: with two or more Projects, `summary` also holds the swap icon after
     `.switcher-text`. `.switcher-text`'s own `flex: 1` stretches its box to
     the row's full width regardless of how short the name is, which pushes
     the icon past it into the gutter `--topbar-reserve` set aside for the
     topbar, not beside the name a person taps. Scoped to `summary`, which
     only the two-or-more-Projects markup has - the single-Project `div`
     never gets an icon to push. */
  .switcher > summary .switcher-text {
    flex: 0 1 auto;
  }

  /* Pulled out of the column `.menu` sits in and pinned to `.sidebar`'s own
     corner instead - `.menu` renders after `.sidebar-bar` in the flow (its
     body needs the full-width row that gives it, not a share of the bar's),
     so without this the toggle would open a whole row down from the bar it
     is meant to read as part of. */
  .menu > summary {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--topbar-height);
    height: var(--topbar-height);
    color: var(--fg-primary);
  }

  .menu > summary:hover {
    background: var(--surface-hover);
  }

  /* The line between the bar and whatever `.menu-body` is showing - drawn
     open or closed, since a hidden box's own border never renders either
     way. */
  .menu-body {
    border-top: 1px solid var(--border-subtle);
  }

  /* Out of `.well`'s own flow and pinned into the sidebar's row instead, so
     the mark, the switcher, the runners chip, the theme control and the menu
     toggle all read as one bar. Shrinks to its own content rather than
     spanning the row - `right` alone, no `left`, is what keeps it out of the
     switcher's text instead of stretching back over it. */
  .topbar {
    position: absolute;
    top: 0;
    right: var(--topbar-height);
    height: var(--topbar-height);
    padding: 0 var(--space-3);
    border-bottom: none;
    background: none;
  }

  .topbar .spacer {
    flex: none;
    width: var(--space-2);
  }

  /* #217: scoped to the top bar - unscoped, this hid every `.badge` on the
     page below 720px, not just the runners chip the swap is about: a
     source's capability badges, "no credential", Done and Pending all
     vanished with it. The full sentence swaps for the dot-and-number
     reading the switcher's own reserve was sized for - the two badges share
     a tone and a dot, and never both show at once. */
  .topbar .badge:not(.badge-compact) {
    display: none;
  }

  .topbar .badge-compact {
    display: inline-flex;
  }

  /* Already the switcher's own line - repeating the name and the source
     count here is the wide layout's redundancy, not something worth the
     room at this width. The count is `.crumb-project`'s own sibling rather
     than its child, so hiding one never hid the other. */
  .crumb-project,
  .topbar > .meta {
    display: none;
  }

  .content {
    padding: var(--space-4);
  }

  /* Chips and buttons drop under the title rather than fighting it for a
     row that is not wide enough for both. `flex-wrap` alone does not force
     it: `min-width: 0` on the title lets it shrink arbitrarily far instead,
     which the row read as "still fits" and never wrapped. `flex-basis: 100%`
     claims the whole first line outright, so there is no room left for
     whatever comes after to do anything but drop to the next one. */
  .page-title,
  .card-head {
    flex-wrap: wrap;
  }

  .page-title h2,
  .card-head > div:first-child {
    flex-basis: 100%;
  }

  /* #214: `column-gap` never carried a row's worth of spacing, which one
     column has nothing else to fall back on for. */
  .empty-actions .first-project-form {
    grid-template-columns: 1fr;
    justify-items: stretch;
    row-gap: var(--space-3);
  }

  .first-project-form .hint {
    grid-column: 1;
  }

  /* #214: the hint sits after the button in the markup - #204 placed it
     under the Repository column by an explicit `grid-column`, which said
     nothing about row order and left source order as the fallback the wide
     layout never had to use. One column falls back to it, and it read the
     hint under the button it was never about. Ordering the button after
     the hint, rather than the hint before the button, is what a one-column
     `grid-auto-flow` actually reads: an item's `order` moves *where it
     lands* in auto-placement, and a negative order on the hint would only
     have pulled it ahead of the Repository field it explains, too. */
  .first-project-form button {
    order: 1;
  }

  /* `table-layout: fixed`'s explicit widths (`Provider` 6rem, `Prepare` and
     `Verify` 15rem each, ...) already add up past what this width has, so the
     one column with none - `Source`, the repository name, the one somebody
     came to read - was squeezed to nothing rather than sharing the loss.
     `auto` sizes every column by its own content instead, and the table
     scrolling wider than the card is `.table-wrap`'s job either way. */
  table {
    table-layout: auto;
  }

  /* #218: the Specs table scrolled sideways inside its card, so "Plan it"
     sat off the edge with nothing saying so. Source and Author are what a
     person can act on least - the source is already this page's own
     heading for a one-source Project - so they go, leaving the number, the
     title and the action on screen without a scroll. The wide layout above
     720px is untouched. Labels is gone from the table entirely, #289, and
     the filter above it still knows labels regardless of column width. */
  .spec-col-source,
  .spec-col-author {
    display: none;
  }

  /* The global `td` and `.run-title` rules clip to one line each; freed of
     the three columns beside it, the title has the room to wrap instead. */
  .spec-col-title {
    overflow: visible;
    white-space: normal;
    text-overflow: clip;
  }

  .spec-col-title .run-title > span:last-child {
    overflow: visible;
    white-space: normal;
    text-overflow: clip;
  }

  /* The action column reserves 23rem above 720px, #289 - sized for the
     state badge, "Code it" and "Plan it" sharing one cell - kept reserving
     it even with the columns beside it gone, leaving the title less of the
     freed room than it could have had. `auto` lets it size to whatever the
     row actually holds instead; equal specificity and this rule coming
     after the plain one above is what lets it win, no `!important` needed. */
  .spec-col-action {
    width: auto;
  }
}
