/*
 * AVC — design tokens.
 *
 * Near-monochrome cool slate. The instrument is grey; colour is a labelling device and is
 * spent only on data — direction, level, verdict, organelle identity — never on chrome.
 *
 * Elevation is luminance, not glow. Each --surface step is a real, measurable lift over
 * the one below, so a card reads as raised without a halo. The previous theme drew cards
 * at #0D1117 on a #060B12 ground behind an accent-tinted 8%-alpha border, which is a
 * 2-point luminance difference: invisible on any display that is not in a dark room.
 *
 * The legacy --ink-* / --bright / --glow-* names are kept as aliases at the end of this
 * block. Roughly a thousand declarations reference them; re-pointing the aliases restyles
 * all of them at once, and the names retire as each file is rewritten.
 */
:root {
  /* Elevation ladder — measurable steps, coolest at the bottom. */
  --surface-0: #0A0C11;   /* app ground */
  --surface-1: #10131A;   /* rails, bars */
  --surface-2: #171B24;   /* cards */
  --surface-3: #1F242F;   /* raised, hover */
  --surface-4: #29303C;   /* popovers, tooltips */

  /* Hairlines. Neutral, not accent-tinted: a border is structure, not decoration. */
  --line: rgba(180, 197, 224, 0.10);
  --line-strong: rgba(180, 197, 224, 0.18);
  --line-focus: rgba(122, 172, 214, 0.55);

  /* Text ramp. */
  --text-0: #E7ECF3;      /* primary */
  --text-1: #A9B4C4;      /* secondary */
  --text-2: #717E90;      /* muted, labels */
  --text-3: #4C5666;      /* faint, disabled */

  /* One restrained accent for interaction. Desaturated steel blue rather than the old
     saturated mint, which fought every data colour on the screen for attention. */
  --accent: #6BA8D6;
  --accent-hover: #86BCE4;
  --accent-dim: rgba(107, 168, 214, 0.16);
  --accent-faint: rgba(107, 168, 214, 0.07);
  --accent-ink: #0A0C11;  /* text on a filled accent */
  /* Channels, for the places that need a computed alpha — a heat scale, say — and so cannot
     use the pre-mixed --accent-dim / --accent-faint. */
  --accent-rgb: 107, 168, 214;

  /* Semantic data colours. Muted enough to sit on a grey ground without vibrating. */
  --up: #DD8A6C;          /* increased — warm */
  --down: #6EA3D4;        /* decreased — cool */
  --gain: #7FB08A;
  --cost: #CE8A6E;
  --caution: #C6A96B;
  --danger: #D2777A;

  --verdict-harmful: #D2777A;
  --verdict-protective: #7FB08A;
  --verdict-ambiguous: #C6A96B;
  --verdict-neutral: #717E90;

  /* Type. Sans for prose and headings; mono for data, identifiers and figures only. */
  --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-heading: 'Inter', ui-sans-serif, system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;

  --radius: 10px;
  --radius-sm: 6px;
  --transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);

  /* Shadows do the work the glows used to. Dark, tight, no colour. */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow-2: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-3: 0 12px 32px rgba(0, 0, 0, 0.55);

  /* ── Legacy aliases ── */
  --ink-950: var(--surface-0);
  --ink-900: var(--surface-1);
  --ink-850: var(--surface-2);
  --ink-800: var(--surface-3);
  --ink-700: var(--surface-4);
  --ink-600: var(--text-3);
  --ink-500: var(--text-2);
  --ink-400: var(--text-2);
  --ink-300: var(--text-1);
  --ink-200: var(--text-1);
  --ink-100: var(--text-0);

  --edge: var(--line);
  --edge-hover: var(--line-strong);
  --edge-active: var(--line-focus);

  --brand: var(--accent);
  --bright: var(--accent);
  --paper: var(--text-0);

  --glow-cyan: var(--accent);
  --glow-blue: var(--accent);
  --glow-purple: var(--accent);
  --glow-cyan-dim: var(--line);
  --glow-blue-dim: var(--line);
  --glow-purple-dim: var(--line);

  /* "Glass" in name only, and deliberately so: --glass-bg is an opaque surface, so the
     backdrop-filters that used to accompany it blurred a backdrop nobody could see. They are
     gone; see the note by #detail-panel.expanded for why they were worse than merely useless. */
  --glass-bg: var(--surface-2);
  --glass-border: var(--line);
  --glass-hover-border: var(--line-strong);

  color-scheme: dark;
}

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

/*
 * The UA's `[hidden] { display: none }` is a plain element-level rule, so *any* class that sets
 * `display` outranks it — and most of the things this UI hides and shows are flex or grid
 * containers. The failure is silent and looks like a data bug rather than a style one: the cell
 * legend kept announcing "99 findings" through a replay that had reset the cell to empty, because
 * `.cell-legend { display: flex }` beat the `hidden` the code had just set.
 *
 * `!important` because that is the whole point — `hidden` has to be the last word on visibility.
 */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--ink-100);
  /* A single static vignette, lightest at the top edge where the chrome sits. It replaces
     three blurred, drifting colour orbs that were the largest continuously-repainting
     layer on the page and tinted every surface behind them, so no two cards read as the
     same elevation. It belongs on the root rather than on an overlay element: an opaque
     fixed overlay paints above the unpositioned flex items and hid the whole app. */
  background: radial-gradient(120% 80% at 50% 0%, #11151D 0%, var(--surface-0) 62%) fixed;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--edge); border-radius: 9999px; }
::-webkit-scrollbar-thumb:hover { background: var(--edge-hover); }

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

/* ── Layout ── */

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100vw;
}

#top-bar {
  display: flex;
  align-items: center;
  height: 52px;
  padding: 0 16px;
  background: var(--ink-900);
  border-bottom: 1px solid transparent;
  border-color: var(--line);
  flex-shrink: 0;
  gap: 16px;
  z-index: 100;
}

#control-bar {
  display: flex;
  align-items: center;
  height: 40px;
  padding: 0 16px;
  background: var(--ink-950);
  border-bottom: 1px solid var(--edge);
  flex-shrink: 0;
  gap: 4px;
}

#main-area {
  display: flex;
  flex: 1;
  min-height: 0;
}

/* Width through a variable so the drag handle can write it without an inline declaration that
   would outrank the narrow-viewport rule at the foot of this file. Default matches `state.js`. */
#run-gallery {
  position: relative;
  width: var(--gallery-width, 240px);
  flex-shrink: 0;
  background: var(--ink-900);
  border-right: 1px solid var(--edge);
  /* `.run-list` is the scrolling surface, not the gallery: the drag handle is a child of this
     element, and on a scrolling parent it would slide out of reach with the run list. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

#view-frame {
  flex: 1;
  min-width: 0;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* A flex item, not an overlay. As an absolutely-positioned layer it sat *under* the activity
   dock, so opening the dock hid the bottom 240px of the cell — and Chromium clipped the
   canvas's composited layer to the un-overlapped region, dropping an equal band off the top.
   In flow the canvas gets exactly the space the dock leaves, the ResizeObserver re-fits the
   camera to it, and nothing overlaps anything. `relative` because the label layer, the
   controls and the legend are all absolutely positioned inside it. */
#cell-canvas-container {
  position: relative;
  flex: 1;
  min-height: 0;
  display: none;
  z-index: 0;
  /* The colour `scene.background` clears to in cell3d.js, kept in step by hand.
     The renderer is created with `alpha: true`, so a drawing buffer that has just been
     reallocated is *transparent* until something draws into it — and what showed through was
     the page's own vignette, which is lighter than the scene near the top of the viewport.
     One composite of that is a pale flash in the shape of the canvas. Painting the same colour
     underneath makes the gap indistinguishable from a drawn frame. */
  background: #080B11;
}
#cell-canvas-container.visible { display: block; }
#cell-canvas-container canvas { width: 100% !important; height: 100% !important; }

/*
 * The scene's box inside the host (built in `cell3d.js`, see `mountStage`).
 *
 * Identical to the host at rest — this is the whole workspace, and the cell should fill it. The
 * one thing it exists for is that a replay can inset it: the rails need real gutters, and a
 * gutter the renderer does not know about is just an overlay with better manners. No transition
 * on the insets. Every step of an animated one is a container resize, and each resize
 * reallocates the composer's twenty render targets.
 */
.cell-stage {
  position: absolute;
  inset: 0;
}

#panel-container {
  flex: 1;
  overflow-y: auto;
  padding: 20px 24px;
}
#panel-container.hidden { display: none; }

#detail-panel {
  position: relative;
  width: 44px;
  flex-shrink: 0;
  background: var(--ink-900);
  border-left: 1px solid var(--edge);
  overflow-y: auto;
  transition: width var(--transition);
}
/* Nothing to drag while the panel is a 44px rail — its width is fixed and the handle would sit
   over the rail's own click target. */
#detail-panel:not(.expanded) .resize-handle { display: none; }

/* The width is a variable so the drag handle can write it without an inline declaration that
   would outrank the collapsed rule above. The fallback is the default in `state.js`. */
#detail-panel.expanded {
  width: var(--detail-width, 360px);
  padding: 16px;
  /* The panel itself no longer scrolls: `.detail-scroll` does. See `detail-panel.js` — the
     controls stay put, and the drag handle stays where the pointer left it. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.detail-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Bleed back out to the panel's edges so a hovered row's background reaches them, then put
     the padding back on the inside. */
  margin: 0 -16px;
  padding: 0 16px;
}

#status-bar {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 16px;
  background: var(--ink-900);
  border-top: 1px solid var(--edge);
  flex-shrink: 0;
  gap: 12px;
  font-size: 11px;
  color: var(--ink-500);
  overflow: hidden;
}
#status-bar.running {
  border-top: 1px solid transparent;
  border-color: var(--line);
  animation: breathe 3s ease-in-out infinite;
}

/* ── Top Bar ── */

.top-logo {
  font-family: var(--font-heading);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--bright);
}

.top-divider {
  width: 1px;
  height: 20px;
  background: var(--edge);
  flex-shrink: 0;
}

.header-field {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.header-field .eyebrow {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bright);
  line-height: 1;
}

.header-field .field-value {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-100);
  white-space: nowrap;
}

.top-status {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}
.status-dot.running { background: var(--bright); animation: pulse-dot 2s ease-in-out infinite; }
.status-dot.completed { background: var(--gain); }
.status-dot.failed { background: var(--cost); }
.status-dot.pending { background: var(--caution); }

.top-btn {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  padding: 5px 14px;
  border-radius: 6px;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-300);
  cursor: pointer;
  transition: all var(--transition);
  /* The class is worn by an `<a>` as well as a `<button>` — the link out to how-it-works.html. */
  display: inline-block;
  text-decoration: none;
  line-height: 1.45;
}
.top-btn:hover { border-color: var(--edge-hover); color: var(--ink-100); }
.top-btn.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
/* Was #00b871 — a mint green left over from the previous palette, which the button flashed
   on hover and nothing else in the app ever used. */
.top-btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* ── Control Bar (View Tabs) ── */

.tab-group {
  display: flex;
  align-items: center;
  gap: 2px;
}

.tab-group-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-600);
  padding: 0 8px 0 4px;
}

.tab-divider {
  width: 1px;
  height: 20px;
  background: var(--edge);
  margin: 0 8px;
  flex-shrink: 0;
}

.tab-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: 6px;
  border: 1px solid transparent;
  background: transparent;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-500);
  cursor: pointer;
  transition: all var(--transition);
  white-space: nowrap;
}
.tab-btn:hover { background: var(--ink-900); color: var(--ink-200); }
.tab-btn.active {
  background: var(--glass-bg);
  color: var(--bright);
  border-color: var(--edge-hover);
  box-shadow: var(--shadow-1);
}
.tab-btn.disabled { opacity: 0.3; pointer-events: none; }
.tab-btn svg { width: 14px; height: 14px; }

/* ── Lens switch (A / B / Δ) ──
 *
 * Deliberately heavier than the view tabs beside it. The tabs choose which question is on
 * screen; this chooses which of two universes the answer is about, and a reader who misreads it
 * misreads every number in the app. The mark (A, B, Δ) carries it at a glance and the two-line
 * label spells it out for anyone meeting the app for the first time.
 */

.lens-switch {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--ink-900);
  flex-shrink: 0;
}

/* Pinned to Δ because the view *is* the comparison. Shown rather than hidden, so the reader can
   see which reading they are on and hover to find out why it is the only one. */
.lens-switch.fixed { border-style: dashed; cursor: help; }
.lens-switch.fixed .lens-btn:disabled { opacity: 0.3; }
.lens-switch.fixed .lens-btn.active:disabled { opacity: 1; }

.lens-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 9px;
  border-radius: 6px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-500);
  cursor: pointer;
  transition: all var(--transition);
  text-align: left;
  white-space: nowrap;
}
.lens-btn:hover:not(:disabled) { background: var(--glass-bg); color: var(--ink-200); }
.lens-btn.active {
  background: var(--glass-bg);
  border-color: var(--accent);
  color: var(--bright);
  box-shadow: var(--shadow-1);
}
.lens-btn:disabled { opacity: 0.28; cursor: default; }

.lens-mark {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  color: var(--ink-500);
  transition: color var(--transition);
}
.lens-btn.active .lens-mark { color: var(--accent); }

.lens-text { display: flex; flex-direction: column; gap: 1px; line-height: 1.15; }
.lens-label { font-size: 11.5px; font-weight: 600; }
.lens-sub {
  font-size: 9.5px;
  color: var(--ink-600);
  letter-spacing: 0.02em;
}

@media (max-width: 1500px) {
  /* The mark alone still says which universe is on screen; the gloss is what goes. */
  .lens-text { display: none; }
  .lens-btn { padding: 4px 10px; }
}

/* ── Run Gallery (Left Sidebar) ── */

.gallery-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-bottom: 1px solid var(--edge);
  flex-shrink: 0;
}

.gallery-header h3 {
  font-family: var(--font-heading);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-200);
}

.gallery-search {
  flex: none;
  padding: 6px 10px;
  margin: 8px 12px;
  border-radius: 6px;
  border: 1px solid var(--edge);
  background: var(--ink-950);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-200);
  outline: none;
  width: calc(100% - 24px);
}
.gallery-search:focus { border-color: var(--edge-hover); }

.run-list { flex: 1; min-height: 0; overflow-y: auto; }

.run-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--transition);
}
.run-item:hover { background: var(--ink-850); }
.run-item.active { background: var(--accent-faint); border-left: 2px solid var(--bright); }

.run-item .run-status-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.run-item .run-info {
  flex: 1;
  min-width: 0;
}

.run-item .run-drug {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-100);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-item .run-meta {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-500);
  margin-top: 1px;
}
.demo-badge {
  display: inline-block;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.5px;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--accent-faint, rgba(99,179,237,0.15));
  color: var(--bright, #63b3ed);
  vertical-align: middle;
  margin-left: 4px;
}

.run-item .run-time {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-600);
  white-space: nowrap;
}

/*
 * Watch a finished run happen.
 *
 * Always drawn, never hover-only: a control that appears under the pointer is one you have to
 * already know is there, which is no use to a first-time viewer and worse to a presenter working
 * from muscle memory. Quiet at rest (a dim triangle in a row of them does not compete with the
 * run's own name) and lit on hover. The demo run is the exception — it is the one anyone is meant
 * to press, so it carries the accent from the start.
 */
.run-replay-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
  opacity: 0.62;
  transition: opacity var(--transition), color var(--transition), border-color var(--transition);
}
.run-item:hover .run-replay-btn,
.run-card:hover .run-replay-btn,
.run-replay-btn:focus-visible { opacity: 1; }
.run-replay-btn:hover { color: var(--accent); border-color: var(--line-focus); }
.run-replay-btn.is-demo {
  opacity: 1;
  color: var(--accent);
  border-color: var(--line-focus);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
}

.run-card-time {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ── Detail Panel (Right) ── */

.detail-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding-top: 14px;
}

.detail-rail-label {
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-600);
  user-select: none;
}

.detail-close {
  background: none;
  border: none;
  color: var(--ink-500);
  cursor: pointer;
  font-size: 16px;
  padding: 4px;
  border-radius: 4px;
}
.detail-close:hover { color: var(--ink-200); background: var(--ink-850); }

.detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.detail-header h3 {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* ── Status Bar ── */

.status-counter {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.status-counter .count-value {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-200);
  font-variant-numeric: tabular-nums;
}

.status-counter .count-label {
  font-size: 10px;
  color: var(--ink-600);
}

.status-sep {
  width: 1px;
  height: 14px;
  background: var(--edge);
  flex-shrink: 0;
}

/* ── Console Panel (Cards) ── */

.console-panel {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 20px;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.console-panel:hover { border-color: var(--glass-hover-border); box-shadow: var(--shadow-2); }

.console-panel h3 {
  font-family: var(--font-heading);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-bottom: 12px;
}

/* ── Stat Cards ── */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

.stat-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 18px;
  text-align: center;
  transition: border-color var(--transition), transform var(--transition);
}
.stat-card:hover {
  border-color: var(--glass-hover-border);
  transform: translateY(-2px);
}

.stat-card .stat-value {
  font-family: var(--font-heading);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.stat-card .stat-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-500);
  margin-top: 4px;
}

/* ── Eyebrow ── */

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bright);
}

.section-eyebrow {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-500);
  margin-bottom: 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--edge);
}

/* ── Pill ── */

.pill {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.06em;
  border-radius: 9999px;
  padding: 2px 10px;
  border: 1px solid var(--edge);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}

.pill-bright { border-color: var(--bright); color: var(--bright); }
.pill-gain { border-color: var(--gain); color: var(--gain); }
.pill-cost { border-color: var(--cost); color: var(--cost); }
.pill-caution { border-color: var(--caution); color: var(--caution); }

/* ── Badge ── */

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ── Tables ── */

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.data-table thead { position: sticky; top: 0; z-index: 1; }

.data-table th {
  background: var(--ink-900);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-500);
  padding: 8px 10px;
  text-align: left;
  border-bottom: 1px solid var(--edge);
  cursor: pointer;
  user-select: none;
}
.data-table th:hover { color: var(--ink-300); }

.data-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.data-table tr:hover td { background: var(--accent-faint); }

/* ── Form Elements ── */

.form-group { margin-bottom: 12px; }

.form-group label {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-500);
  margin-bottom: 4px;
}

.form-input {
  width: 100%;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--edge);
  background: var(--ink-950);
  color: var(--ink-100);
  font-family: var(--font-sans);
  font-size: 13px;
  outline: none;
  transition: border-color var(--transition);
}
.form-input:focus { border-color: var(--bright); box-shadow: 0 0 0 3px var(--accent-dim); }

select.form-input {
  appearance: none;
  cursor: pointer;
}

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

textarea.form-input { resize: vertical; line-height: 1.5; min-height: 60px; }

.label-hint {
  font-family: var(--font-sans);
  font-size: 10px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
  margin-left: 6px;
}

/* Cell line + its "Look up" action share a line: the lookup acts on the field beside it, and
   putting it anywhere else made it read as a form-level button. */
.input-with-action { display: flex; gap: 6px; }
.input-with-action .form-input { flex: 1 1 auto; min-width: 0; }

.duration-field { display: grid; grid-template-columns: 1fr 1.15fr; gap: 6px; }

.btn-quiet {
  flex: 0 0 auto;
  font-family: var(--font-sans);
  font-size: 12px;
  padding: 0 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--edge);
  background: var(--surface-3);
  color: var(--text-1);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--transition), color var(--transition);
}
.btn-quiet:hover:not(:disabled) { border-color: var(--edge-hover); color: var(--text-0); }
.btn-quiet:disabled { opacity: 0.5; cursor: default; }

/* The previous Start button was a full-bleed saturated slab — the loudest object on a page
   whose subject is data. An outlined button in the accent reads as primary without shouting. */
.btn-primary {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--accent);
  background: var(--accent-dim);
  color: var(--accent-hover);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.btn-primary:hover:not(:disabled) { background: var(--accent); color: var(--accent-ink); }
.btn-primary:disabled {
  border-color: var(--edge);
  background: transparent;
  color: var(--text-3);
  cursor: default;
}
.btn-block { display: block; width: 100%; margin-top: 14px; }

.form-error {
  margin-top: 10px;
  padding: 8px 10px;
  border-left: 2px solid var(--danger);
  background: rgba(210, 119, 122, 0.09);
  font-size: 12px;
  color: var(--text-1);
}

/* ── Cell-line review card ── */

.context-card {
  margin-top: 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-1);
}
.context-card.pending { padding: 14px; }

.context-pending-text { font-size: 12px; color: var(--text-2); }
.context-pending-text b { color: var(--text-0); font-weight: 500; }

.context-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
}
.context-title { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.context-name { font-size: 14px; font-weight: 600; color: var(--text-0); }

/* Everything on this card came out of a model. Saying so once, in the header, is the whole
   reason the card is editable at all. */
.context-flag {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}
.context-head-right { display: flex; align-items: center; gap: 8px; }

.context-conf {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  padding: 2px 8px;
  border: 1px solid currentColor;
  border-radius: 999px;
  opacity: 0.9;
}

.context-warn {
  margin: 12px 14px 0;
  padding: 8px 10px;
  border-left: 2px solid var(--caution);
  background: rgba(198, 169, 107, 0.09);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-1);
}

.context-summary {
  margin: 12px 14px 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-2);
}

.context-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px 14px; }
.context-chip {
  font-size: 11px;
  color: var(--text-1);
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 10px;
}
.chip-key {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-right: 6px;
}

/* Collapsed by default: the chips answer "did it get this right", and only an operator who
   wants to change something needs eleven inputs and a mutation table. */
.context-body { display: none; padding: 0 14px 14px; }
.context-card.open .context-body { display: block; }

.ctx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}
.ctx-field.wide { grid-column: 1 / -1; }
.ctx-field label {
  display: block;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 3px;
}
.ctx-field .req { color: var(--caution); margin-left: 3px; }
.ctx-field .form-input { padding: 6px 9px; font-size: 12px; }

.ctx-section { margin-top: 14px; }
.ctx-none { font-size: 12px; color: var(--text-3); margin: 4px 0 0; }

.mut-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.mut-row { display: flex; align-items: center; gap: 6px; }
.mut-row .form-input { padding: 5px 8px; font-size: 12px; font-family: var(--font-mono); }
.mut-gene { flex: 0 0 96px; }
.mut-variant { flex: 1 1 auto; min-width: 0; }
.mut-tag {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.06em;
  color: var(--text-3);
  white-space: nowrap;
}
.mut-remove {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-3);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.mut-remove:hover { color: var(--text-0); border-color: var(--edge-hover); }

.ctx-caveats { margin: 6px 0 0; padding-left: 16px; }
.ctx-caveats li { font-size: 11.5px; line-height: 1.55; color: var(--text-2); margin-bottom: 4px; }

/* ── Dashboard ── */

.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* The form and Overview size to their content; the run list takes everything left over.
     Without the second track the whole grid packed into the top 60% of a 950px frame and the
     page simply stopped, with two hundred empty pixels below the last card. */
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 100%;
  gap: 20px;
  /* Panels size to their content. Stretch is the grid default, and it gave Overview — four
     stat cards — the full height of the form beside it, which is most of a 600px void. */
  align-items: start;
  animation: fadeIn 400ms ease;
}
.dashboard-grid .full-width {
  grid-column: 1 / -1;
  /* The one panel that does stretch: it owns the bottom of the page, and its card grid scrolls
     inside it rather than pushing the page taller. */
  align-self: stretch;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.dashboard-grid .full-width > .run-cards-grid,
.dashboard-grid .full-width > .empty-state {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Cards keep their own height inside a container that is now taller than they are. */
  align-content: start;
}

/* ── Dot Grid Background ── */

.dot-grid {
  background-image: radial-gradient(circle, var(--line) 1px, transparent 1.4px);
  background-size: 22px 22px;
}

/* ── Green Glow ── */

.green-glow {
  box-shadow: var(--shadow-3);
}

/* ── Verdict Cards ── */

.verdict-grid {
  display: grid;
  /* 200px left "Functional Capacity" wrapping under its own verdict badge and the outcome
     node ids breaking mid-token. These cards carry identifiers, so they need the width. */
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

/*
 * `--vc` is the card's own verdict colour, set inline by the renderer; `#717E90` is the neutral
 * it falls back to when a card is rendered without one. The wash is faint on purpose — thirty of
 * these sit in a grid, and a saturated tile per domain would be a chart of nothing. What it buys
 * is that the domains which moved are findable without reading thirty badges.
 */
.verdict-card {
  --vc: #717E90;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--vc) 9%, var(--glass-bg)) 0%,
      var(--glass-bg) 62%);
  border: 1px solid var(--glass-border);
  border-left: 3px solid color-mix(in srgb, var(--vc) 72%, transparent);
  border-radius: var(--radius);
  padding: 14px;
  cursor: pointer;
  transition: all var(--transition);
}
.verdict-card:hover {
  border-color: color-mix(in srgb, var(--vc) 46%, var(--glass-hover-border));
  border-left-color: var(--vc);
  box-shadow: var(--shadow-2), 0 0 0 1px color-mix(in srgb, var(--vc) 18%, transparent);
  transform: translateY(-1px);
}

.verdict-card .verdict-domain {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: -0.01em;
  margin-bottom: 8px;
  text-transform: capitalize;
}

.verdict-card .verdict-badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #fff;
  margin-bottom: 8px;
}

.confidence-meter { display: flex; gap: 2px; height: 4px; margin: 8px 0; }
.confidence-meter .seg { flex: 1; border-radius: 2px; background: var(--ink-800); }
.confidence-meter .seg.filled { background: var(--bright); }

.verdict-meta {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-500);
}

/* ── Validation / Experiment Cards ── */

.validation-group { margin-bottom: 24px; }

.validation-group h4 {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-500);
  margin-bottom: 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--edge);
}

.experiment-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 8px;
  transition: border-color var(--transition);
}
.experiment-card:hover { border-color: var(--glass-hover-border); }

.experiment-card .exp-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.experiment-card .exp-technique {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: -0.01em;
}

.experiment-card .exp-detail {
  font-size: 12px;
  color: var(--ink-400);
  margin: 3px 0;
  line-height: 1.5;
}
.experiment-card .exp-detail strong { color: var(--ink-200); }

/* ── Graph View ── */

/* Takes the height left below the filter bar. It used to subtract a hardcoded 60px, which is
   the filter bar's height only until something in it wraps. */
.graph-container {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 400px;
}

/* Pinned to the container rather than sized at `height: 100%`. The percentage resolved against
   `.view-fill`, whose own height comes from its content, so it fell back to `auto` and the canvas
   sat at its attribute height — 634px inside a 788px box, with the shortfall showing as dead space
   under the graph. Against a positioned ancestor the percentage has a definite box to resolve to. */
.graph-container canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  background: var(--ink-950);
}

/* ── Graph key ──
 *
 * On the picture, not beside it. The colour encoding is a choice the reader makes from the
 * filter bar, so a key printed anywhere else would be describing one of six schemes and wrong
 * about the other five. `pointer-events: none` so it never intercepts a pan that starts in the
 * corner — except on its own rows, which carry the hover explanations. */
.graph-key {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 20;
  padding: 9px 12px 10px;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  background: var(--glass-bg);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-2);
  pointer-events: none;
  max-width: 210px;
}

.graph-key-head {
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 3px;
}

/*
 * Every row is a filter.
 *
 * They are `<button>`s rather than styled divs because that is what they now do: one click
 * isolates the class the row names. Buttons carry the keyboard and the accessibility tree for
 * free — tab order, Enter and Space, and an `aria-pressed` a screen reader announces — none of
 * which a div with a click handler has. The reset below is the price of getting them.
 */
.graph-key-row,
.graph-key-mag,
.graph-key-clear {
  appearance: none;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 120ms ease, background 120ms ease;
}

.graph-key-row {
  display: flex;
  align-items: center;
  gap: 7px;
  width: calc(100% + 8px);
  margin-left: -4px;
  padding: 1px 4px;
  border-radius: 4px;
}

.graph-key-row:hover,
.graph-key-mag:hover { background: var(--surface-3); }

.graph-key-row:focus-visible,
.graph-key-mag:focus-visible,
.graph-key-clear:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/*
 * Selected reads as "this one", muted as "not this one". Both are needed: highlighting alone is
 * easy to miss on a panel this small, and the muted rows are what say the picture behind the key
 * is a subset rather than the whole run.
 */
.graph-key-row.selected,
.graph-key-mag.selected {
  background: var(--surface-3);
  color: var(--text-1);
  font-weight: 600;
}

.graph-key-row.muted,
.graph-key-mag.muted { opacity: 0.4; }

.graph-key-label { overflow-wrap: anywhere; }

.graph-key-dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
}

.graph-key-mags {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 7px;
  font-size: 9.5px;
  color: var(--text-3);
}

.graph-key-mag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 3px;
  border-radius: 4px;
}

/* The way out. A selection made by clicking a row is cleared by clicking it again, which is not
   discoverable — and after switching the colouring there may be no selected row on screen to
   click. This is always there while anything is filtered, and never there when nothing is. */
.graph-key-clear {
  margin-top: 8px;
  padding: 3px 7px;
  width: 100%;
  border: 1px solid var(--glass-border);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--text-2);
}

.graph-key-clear:hover {
  background: var(--surface-3);
  color: var(--text-1);
}

/* No backdrop-filter: this floats directly over the WebGL canvas and follows the cursor, so
   it is the worst possible place to force a per-frame backdrop snapshot. See the note by
   `#detail-panel.expanded`. */
.node-popup {
  position: absolute;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 12px;
  max-width: 280px;
  z-index: 50;
  pointer-events: none;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}

/*
 * A view whose last element is a scroller — a long table, a canvas — should be handed the
 * height that is left rather than pick one. Wrapping such a view in `.view-fill` makes it a
 * column that fills the panel; the child marked `flex: 1` takes the remainder.
 *
 * `height`, not `min-height`: with a minimum only the column still grows to fit its content, so
 * `flex: 1` on the scroller caps nothing and a long table runs hundreds of pixels past the bottom
 * of the panel instead of scrolling inside it. Every view using this ends in its own scroller, so
 * bounding the column here is what makes that scroller the one that moves.
 */
.view-fill {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/*
 * Empty states. A view with nothing in it still owes the reader an explanation: whether the
 * run has not got there yet or genuinely produced nothing is the difference between waiting
 * and investigating. Centred in its container rather than top-packed, so an empty view reads
 * as deliberate instead of as a rendering failure.
 */

.view-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 24px;
  text-align: center;
  pointer-events: none;
}

.view-empty-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-300);
}

.view-empty-body {
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-500);
  max-width: 340px;
}

.graph-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-500);
}

/* ── DEG View ── (the rest of this page lives in `deg.css`) */

/* What the chart's axes mean. The signature has no fold change and no p-value, and a reader who
   knows the field will assume both unless the chart says otherwise, so the caption is part of
   the chart rather than decoration on it. */
.chart-caption {
  flex: none;
  max-width: 78ch;
  margin-bottom: 16px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-400);
}

/* Scrolls to the bottom of the panel rather than stopping at an arbitrary 400px with empty
   space beneath it. */
.gene-table-wrapper {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
}

/* ── Filter Bar ── */

.filter-bar {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 14px;
  flex-wrap: wrap;
}

.filter-bar .form-input { width: auto; min-width: 140px; }

/* ── Menu select ──
   A button and a popover drawn over a hidden native `<select>`, which stays as the state.
   See assets/controls.js for why it is an enhancer rather than a component. */

.msel { position: relative; display: inline-block; }

/* The native control is the state and the accessibility target of last resort; it must not be
   `display: none`, which would take it out of the form and out of the a11y tree entirely. */
.msel-native {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(50%);
}

.msel-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  min-width: 132px;
  padding: 4px 26px 5px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  color: var(--text-0);
  font-family: var(--font-sans);
  font-size: 12.5px;
  line-height: 1.25;
  cursor: pointer;
  text-align: left;
  position: relative;
  transition: border-color var(--transition), background var(--transition);
}

.msel-btn:hover:not(:disabled) { border-color: var(--line-strong); background: var(--surface-2); }
.msel-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
.msel-btn:disabled { opacity: 0.45; cursor: default; }

/* The subject of the control, always visible — which is the whole reason a bar of ten of these
   is now readable without opening any of them. */
.msel-group {
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.msel-value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 190px; }

.msel-caret {
  position: absolute;
  right: 9px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 9px;
  color: var(--text-3);
}

/* Not the default: this filter is doing something. */
.msel.active .msel-btn { border-color: var(--accent); background: var(--accent-dim); }
.msel.active .msel-group { color: var(--accent); }
.msel.open .msel-btn { border-color: var(--line-focus); }

.msel-menu {
  display: none;
  position: absolute;
  z-index: 60;
  top: calc(100% + 4px);
  left: 0;
  /* Wide enough that a one-sentence hint is two lines rather than six. A menu the width of its
     button turns every explanation into a column of three-word fragments, which is harder to
     read than no explanation at all. */
  min-width: max(100%, 270px);
  max-width: 340px;
  max-height: 380px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}

.msel.open .msel-menu { display: block; }
.msel.open.up .msel-menu { top: auto; bottom: calc(100% + 4px); }

.msel-opt {
  padding: 6px 9px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 12.5px;
  color: var(--text-1);
}

.msel-opt:hover { background: var(--surface-3); color: var(--text-0); }
.msel-opt.selected { background: var(--accent-dim); color: var(--text-0); }
.msel-opt-label { display: block; }

/* What an option means, which a native `<option title>` shows on no platform this runs on. */
.msel-opt-hint {
  display: block;
  margin-top: 2px;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--text-2);
}

/* ── Segmented control ──
 *
 * For a handful of exclusive choices worth seeing all at once, which a popover hides.
 *
 * Named `segctl`, not `seg`: `.seg` was already taken twice over — by the confidence meter's
 * pips and by the cell view's quality switch — and a third meaning of the same class name is
 * how a stylesheet starts silently restyling things nobody was looking at.
 */

.segctl {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-1);
}

.segctl-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 4px 11px 5px;
  border: none;
  border-right: 1px solid var(--line);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 12.5px;
  line-height: 1.25;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.segctl-btn:last-child { border-right: none; }
.segctl-btn:hover:not(:disabled):not(.active) { background: var(--surface-2); color: var(--text-0); }
.segctl-btn.active { background: var(--accent-dim); color: var(--text-0); }
.segctl-btn:disabled { opacity: 0.35; cursor: default; }
.segctl-label { white-space: nowrap; }

.segctl-sub {
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.segctl-btn.active .segctl-sub { color: var(--accent); }

/* ── Toggle ── */

/*
 * A toggle's "on" state used to be a solid fill of the brand colour. Four of these sit in
 * the cell view and all four default to on, so the corner of the viewport was four
 * saturated slabs — which is why they read as blank boxes rather than as labelled
 * controls. On is now a tint plus a leading dot: unmistakable, and quiet enough that four
 * of them at once are still just controls.
 */
.toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.005em;
  cursor: pointer;
  transition: background var(--transition), color var(--transition),
              border-color var(--transition);
}

.toggle-btn::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.30;
  transition: opacity var(--transition);
}

.toggle-btn:hover {
  background: var(--surface-3);
  border-color: var(--line-strong);
  color: var(--text-1);
}

.toggle-btn.active {
  background: var(--accent-dim);
  border-color: var(--line-focus);
  color: var(--accent-hover);
}

.toggle-btn.active::before { opacity: 1; }

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

/* ── Cell View Controls ── */

.cell-controls {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 10px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  /* Opaque, not translucent. This panel floats over a moving 3D scene; anything
     see-through means the label text is read against whatever organelle drifts behind
     it, which is exactly the condition under which a control stops being readable. */
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}

/* Folded away so the scene has the frame to itself; the stub below brings it back. */
.cell-controls.folded { display: none; }

.cell-controls-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.cell-controls-fold {
  border: none;
  background: transparent;
  color: var(--text-3);
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
  transition: color var(--transition);
}
.cell-controls-fold:hover { color: var(--text-0); }
.cell-controls-fold:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
}

.cell-controls-title {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 0 3px 2px;
}

/*
 * What the governor is spending, when it is spending less than everything. Hidden the rest of
 * the time — see `markCellQuality`, which is also the only thing that writes the text.
 */
.cell-quality-note {
  max-width: 152px;
  padding: 0 3px;
  font-size: 10px;
  line-height: 1.35;
  color: var(--text-3);
}
.cell-quality-note[hidden] { display: none; }

/*
 * The stub, mirroring `.cell-atlas-toggle` on the other side of the scene. Same corner the panel
 * occupies, so folding and unfolding are the same gesture in the same place.
 */
.cell-scene-toggle {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 11px;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.cell-scene-toggle:hover { color: var(--text-0); border-color: var(--line-strong); }
.cell-scene-toggle[hidden] { display: none; }
.cell-scene-toggle-label {
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 10px;
}

/*
 * Quality is one setting with two values, so it is a segmented control rather than two
 * toggles — a pair of independent switches would allow "both off", which is not a state
 * the renderer has.
 */
.cell-quality-switch {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-0);
}

.seg-btn {
  flex: 1;
  padding: 4px 10px;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--text-3);
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.seg-btn:hover { color: var(--text-1); }

.seg-btn.active {
  background: var(--surface-3);
  color: var(--text-0);
}

.seg-btn:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 1px;
}

/* ── Cell View Legend ──
 *
 * Bottom-left, opposite the layer controls, because it describes the scene rather than
 * operating on it. Hidden entirely when the effect layer has nothing to explain.
 */
.cell-legend {
  position: absolute;
  bottom: 14px;
  left: 14px;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 240px;
  padding: 10px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
  pointer-events: none;
}

.cell-legend-title {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-1);
  letter-spacing: 0.01em;
}

.cell-legend-rows {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}

/*
 * The swatches are buttons: each one lights its own class of finding and nothing else. The reset
 * is what a `<button>` costs; the keyboard reachability and the announced pressed state are what
 * it buys, neither of which a span with a click handler has.
 */
.cell-legend-row,
.cell-legend-step,
.cell-legend-clear {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  transition: opacity 120ms ease, background 120ms ease;
}

.cell-legend-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 1px 4px;
  margin: -1px -4px;
  border-radius: 4px;
  font-size: 11px;
  color: var(--text-2);
  text-transform: capitalize;
}

.cell-legend-row i {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex: none;
}

.cell-legend-row:hover,
.cell-legend-step:hover { background: var(--surface-3); }

.cell-legend-row:focus-visible,
.cell-legend-step:focus-visible,
.cell-legend-clear:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/* Selected says "this one"; muted says the cell behind the panel is a subset. Both are needed —
   a highlight alone is easy to miss on a panel this small and this far from the eye's centre. */
.cell-legend-row.selected,
.cell-legend-step.selected {
  background: var(--surface-3);
  color: var(--text-1);
  font-weight: 600;
}

.cell-legend-row.muted,
.cell-legend-step.muted { opacity: 0.4; }

.cell-legend-clear {
  margin-top: 2px;
  padding: 3px 7px;
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-2);
}

.cell-legend-clear:hover {
  background: var(--surface-3);
  color: var(--text-1);
}

.cell-legend-note {
  font-size: 10px;
  color: var(--text-3);
}

/*
 * The panel stays transparent to the mouse so that dragging across this corner still orbits the
 * camera, but the swatches themselves take pointer events — every one carries the schema's gloss
 * as a title and is the filter for its own class, and neither a hover nor a click reaches a
 * panel the pointer passes straight through.
 */
.cell-legend-row,
.cell-legend-step,
.cell-legend-clear,
.cell-legend-key { pointer-events: auto; }

.cell-legend-key {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-3);
  cursor: help;
}

.cell-legend-ramp {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}

.cell-legend-step {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 4px;
  margin: -1px -4px;
  border-radius: 4px;
  font-size: 10px;
  color: var(--text-3);
}

.cell-legend-step i {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex: none;
}

/* ── Cell View Atlas ──
 *
 * Right-hand side, opposite the scene controls: the index of what the run modelled, by level and
 * by domain. Scrolls internally rather than growing — thirty domains do not fit beside a cell,
 * and a panel that overflows the viewport would push the legend off the bottom of it.
 */
.cell-atlas {
  position: absolute;
  top: 14px;
  right: 14px;
  bottom: 14px;
  z-index: 10;
  width: var(--atlas-width, 300px);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  /* The head and the search box stay put; only the tree below them scrolls. Searching a
     thirty-domain tree whose own search box has scrolled off the top is searching blind. */
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}

/* Folded away so the scene has the frame to itself; the stub below brings it back. */
.cell-atlas.folded { display: none; }

.cell-atlas-toggle {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 11px;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.cell-atlas-toggle:hover { color: var(--text-0); border-color: var(--line-strong); }
.cell-atlas-toggle-label {
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 10px;
}

.atlas-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 4px 4px;
  flex: none;
}

.atlas-fold {
  border: none;
  background: transparent;
  color: var(--text-3);
  font-size: 13px;
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--transition), background var(--transition);
}
.atlas-fold:hover { color: var(--text-0); background: var(--surface-3); }

/* Run-wide totals, and the two controls that act on the whole tree. */
.atlas-summary {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  padding: 0 4px 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-2);
}

.atlas-expand-all {
  margin-left: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.04em;
  padding: 2px 6px;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.atlas-expand-all:hover { color: var(--text-0); border-color: var(--line-strong); }

.atlas-search-wrap { flex: none; padding: 0 2px 6px; }

.atlas-search {
  width: 100%;
  padding: 5px 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-0);
  color: var(--text-0);
  font-family: var(--font-sans);
  font-size: 11.5px;
}
.atlas-search::placeholder { color: var(--text-3); }
.atlas-search:focus { outline: none; border-color: var(--line-focus); }

/* The only part that scrolls, so the search box above it cannot be scrolled out of reach. */
.atlas-tree {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.atlas-title {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.atlas-lens {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-2);
  cursor: help;
}

.atlas-row {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  padding: 5px 6px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.atlas-row:hover { background: var(--surface-2); color: var(--text-0); }

.atlas-row:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: -2px;
}

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

.atlas-sub {
  font-size: 10px;
  color: var(--text-3);
  flex: none;
}

.atlas-count {
  flex: none;
  min-width: 22px;
  padding: 1px 5px;
  border-radius: 8px;
  background: var(--surface-3);
  color: var(--text-1);
  font-family: var(--font-mono);
  font-size: 10px;
  text-align: center;
}

/* A zero is information — the agent ran and wrote nothing — so it is shown, but quietly. */
.atlas-count-zero { background: transparent; color: var(--text-3); }

.atlas-moved {
  flex: none;
  margin-left: 4px;
  padding: 1px 5px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 10px;
}

.atlas-level-head {
  font-weight: 600;
  color: var(--text-1);
  margin-top: 4px;
}

/* Three short names, and the one that matters most — "Compartmental" — is the longest. Let it
   take its natural width and push the agent count instead of being clipped by it. */
.atlas-level-head .atlas-name {
  flex: 0 1 auto;
  margin-right: auto;
}

/* The level's colour as a spine down the left of its row, matching the halo hue in the scene. */
.atlas-bar {
  flex: none;
  width: 3px;
  height: 15px;
  border-radius: 2px;
}

.atlas-dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
}

.atlas-caret {
  flex: none;
  font-size: 9px;
  color: var(--text-3);
  transition: transform var(--transition);
  padding: 0 2px;
}

.atlas-level.open > .atlas-level-head .atlas-caret,
.atlas-struct.open > .atlas-struct-head .atlas-caret { transform: rotate(90deg); }

.atlas-body { padding-left: 6px; }

.atlas-struct-body {
  padding-left: 16px;
  /* Hangs the domains off their structure so the nesting reads without a second indent level. */
  border-left: 1px solid var(--line);
  margin-left: 10px;
}

.atlas-struct.selected > .atlas-struct-head {
  background: var(--surface-3);
  color: var(--text-0);
}

.atlas-domain { font-size: 11px; }

/* Dimmed rather than hidden: absence of findings is a result, and one worth being able to see. */
.atlas-domain.empty { color: var(--text-3); }
.atlas-domain.empty .atlas-caret { display: none; }
.atlas-domain-group.open > .atlas-domain .atlas-caret { transform: rotate(90deg); }

/* The escape hatch to the full node list. Faint until the row is hovered, because the row's own
   action — list the findings here — is the one most readers want. */
.atlas-open {
  flex: none;
  padding: 0 3px;
  font-size: 11px;
  line-height: 1;
  color: var(--text-3);
  opacity: 0;
  transition: opacity var(--transition), color var(--transition);
}
.atlas-row:hover .atlas-open,
.atlas-open:focus-visible { opacity: 1; }
.atlas-open:hover { color: var(--accent); }

/* ── Findings inside a domain ── */

.atlas-node-list {
  padding-left: 8px;
  margin-left: 8px;
  border-left: 1px solid var(--line);
}

.atlas-node {
  font-size: 11px;
  padding: 4px 6px;
  align-items: baseline;
}
.atlas-node .atlas-dot { align-self: center; width: 7px; height: 7px; }

/* Name over observable, because one gene routinely carries four nodes and the name alone does
   not say which claim this is. */
.atlas-node .atlas-name {
  display: flex;
  flex-direction: column;
  gap: 1px;
  white-space: normal;
  overflow: visible;
}

.atlas-node-obs {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--text-3);
  line-height: 1.2;
}

.atlas-node-mag,
.atlas-node-change {
  flex: none;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-3);
}
.atlas-node-change { font-weight: 600; }

.atlas-node.selected {
  background: var(--accent-dim);
  color: var(--text-0);
  box-shadow: inset 2px 0 0 var(--accent);
}

.atlas-more {
  width: 100%;
  margin-top: 2px;
  padding: 4px 6px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 9.5px;
  text-align: left;
  cursor: pointer;
}
.atlas-more:hover { background: var(--surface-2); color: var(--text-0); }

.atlas-note {
  margin-top: 8px;
  padding: 6px 4px 2px;
  border-top: 1px solid var(--line);
  font-size: 10px;
  line-height: 1.45;
  color: var(--text-3);
}

.cell-tooltip {
  position: absolute;
  /* Moved with `transform`, like the labels — animating `left`/`top` would put a layout pass
     in the frame loop for every pixel the cursor travels. These pin the transform's origin. */
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--surface-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  padding: 8px 11px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-0);
  line-height: 1.45;
  white-space: nowrap;
  box-shadow: var(--shadow-2);
}
.cell-tooltip-name { font-weight: 600; }
.cell-tooltip-meta { font-size: 11px; color: var(--text-3); }

/* ── Cell Labels ──
 *
 * Placed by cell3d-labels.js in screen space; everything here is presentation only. The
 * layer has no background of its own and does not take pointer events — the canvas
 * underneath must stay fully draggable, including the pixels a label happens to cover.
 */

.cell-label-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 8;
  overflow: hidden;
}

.cell-label-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.cell-label-line {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1;
  /* The leader is an aid, not a feature. It should be findable when you look for it and
     invisible when you are not — the previous version drew opaque white polylines that
     read as scratches across the cell. */
  opacity: 0.55;
  transition: opacity var(--transition);
}
.cell-label-line.is-far { opacity: 0.22; }

.cell-label {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-1);
  color: var(--text-1);
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.01em;
  white-space: nowrap;
  box-shadow: var(--shadow-1);
  /* transform is set per frame, so it must not be transitioned — a transition here would
     make every label lag the camera by its own duration. */
  transition: opacity var(--transition), border-color var(--transition);
}

.cell-label.is-far {
  opacity: 0.45;
}

.cell-label.is-active {
  opacity: 1;
  border-color: var(--line-focus);
  color: var(--text-0);
}

/*
 * The cell is isolating something else. The plate stays legible if you look for it — where the
 * other structures are is still part of reading where this one sits — but it stops competing
 * with the one thing that was asked for. Matches the geometry's own ghost.
 */
.cell-label.is-muted {
  opacity: 0.26;
}
.cell-label-line.is-muted {
  opacity: 0.12;
}

/*
 * A label with nowhere to go: its anchor has turned behind the cell, or the collision solver
 * ran out of slots that do not sit on top of something else.
 *
 * It fades rather than being switched off. These come and go constantly while the cell
 * rotates, and a dozen elements blinking in and out is perceived as the whole scene
 * stuttering — the frames were arriving on time all along, the labels just looked like they
 * were not. Last of the three, so it outranks both `is-far` and `is-active`, which are
 * equally specific and would otherwise win on source order alone.
 */
.cell-label.is-off,
.cell-label-line.is-off {
  opacity: 0;
}

.cell-label-chip {
  display: inline-flex;
  align-items: center;
  width: 34px;
  height: 4px;
  border-radius: 2px;
  background: var(--surface-3);
  overflow: hidden;
}

.cell-label-chip i {
  display: block;
  height: 100%;
  border-radius: 2px;
  transition: width var(--transition);
}

/*
 * ── Colony chips ──
 *
 * One per group of findings in the cell, placed by cell3d-clusters.js. Smaller and lighter than
 * the structure plates on purpose: the structure names are the frame the cell is read in and
 * these are the data drawn on it, so they must not out-shout the fourteen labels that give the
 * picture its anatomy. The layer takes no pointer events; the chips themselves do, because
 * clicking one is how a group is opened.
 */

.cell-cluster-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 9;
  overflow: hidden;
}

.cell-cluster {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 2px 6px 2px 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* Translucent rather than solid: a chip sits over the cell, and fourteen opaque pills would
     punch fourteen holes in the thing they are annotating. No backdrop blur — it is a
     full-screen filter pass per chip over a live WebGL canvas, and this view is already
     spending its frame budget on the cell. */
  background: rgba(16, 19, 26, 0.82);
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0.01em;
  white-space: nowrap;
  cursor: pointer;
  pointer-events: auto;
  /* transform is written every frame; transitioning it would make every chip lag the camera. */
  transition: opacity var(--transition), border-color var(--transition),
              background var(--transition), color var(--transition);
}

.cell-cluster:hover {
  border-color: var(--line-focus);
  color: var(--text-0);
  background: rgba(23, 27, 36, 0.92);
}

.cell-cluster:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
}

.cell-cluster-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: none;
}

.cell-cluster-count {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 9px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-2);
}

/* The group being read. It has the cell to itself — the cloud is dimmed to it — so the chip
   says which group that is without the reader having to remember what they clicked. */
.cell-cluster.is-active {
  border-color: var(--line-focus);
  background: var(--surface-2);
  color: var(--text-0);
}
.cell-cluster.is-active .cell-cluster-count {
  background: var(--accent-dim);
  color: var(--text-0);
}

/* On the far side of the cell: still worth reading — it says what you are looking through —
   but not competing with the groups actually facing the reader. */
.cell-cluster.is-far {
  opacity: 0.4;
}

/* Nowhere to sit this frame. Faded rather than switched off, for the reason the structure
   labels are: a dozen elements blinking as the cell turns reads as the scene stuttering. */
.cell-cluster.is-off {
  opacity: 0;
}

/* ── Domain / Node lists ── */

.domain-item {
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.domain-item:last-child { border-bottom: none; }

.domain-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
}

.domain-item .domain-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: -0.01em;
  flex: 1;
  min-width: 0;
}

/* The escape hatch out of the panel and into the full node list for this agent. */
.domain-open {
  flex: none;
  padding: 1px 5px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-3);
  font-size: 11px;
  line-height: 1.2;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--transition), color var(--transition), border-color var(--transition);
}
.domain-item:hover .domain-open,
.domain-open:focus-visible { opacity: 1; }
.domain-open:hover { color: var(--accent); border-color: var(--line-strong); }

.domain-item .domain-meta {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-500);
  display: flex;
  gap: 8px;
  align-items: center;
}

.node-list { margin-top: 8px; }

/*
 * A finding, as a way into its record.
 *
 * This was a `<div>` for as long as the panel existed, which is the whole reason the twenty
 * findings under an organelle were unreachable: nothing about the row said it could be clicked
 * and nothing happened when it was. A button, a hover state and a chevron say the opposite.
 */
.node-list-item {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 6px;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: transparent;
  color: var(--text-1);
  font-family: var(--font-sans);
  font-size: 12px;
  text-align: left;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.node-list-item:hover { background: var(--surface-2); color: var(--text-0); }
.node-list-item:hover .node-chevron { color: var(--accent); transform: translateX(2px); }
.node-list-item:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }
.node-list-item[disabled] { cursor: default; }
.node-list-item[disabled]:hover { background: transparent; color: var(--text-1); }

.node-list-item .node-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
}

/* Name over observable: one gene routinely carries four nodes, and a row printing only the name
   tells the reader which molecule but not which claim. */
.node-list-item .node-entity {
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-weight: 500;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.node-list-item .node-observable {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 400;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.node-list-item .node-change {
  flex: none;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
}

.node-list-item .node-mag {
  flex: none;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-3);
}

.node-list-item .node-cite {
  flex: none;
  min-width: 16px;
  padding: 1px 4px;
  border-radius: 8px;
  background: var(--surface-3);
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 9px;
  text-align: center;
}

.node-list-item .node-chevron {
  flex: none;
  color: var(--text-3);
  font-size: 14px;
  line-height: 1;
  transition: color var(--transition), transform var(--transition);
}

/* Shared by every capped list in the panel — findings, evidence, connections. */
.list-more {
  width: 100%;
  margin-top: 4px;
  padding: 5px 6px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 10px;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.list-more:hover { color: var(--text-0); border-color: var(--line-strong); }
.list-more[disabled] { cursor: default; opacity: 0.6; }

/* ── Expandable ── */

.expandable-header {
  display: flex;
  align-items: center;
  gap: 5px;
  cursor: pointer;
  padding: 4px 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-500);
  user-select: none;
}
.expandable-header:hover { color: var(--ink-300); }

.expandable-content { display: none; padding: 6px 0 6px 14px; }
.expandable-content.open { display: block; }

/* ── Event Log (in status bar region) ── */

.event-stream {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--bright);
  white-space: nowrap;
}

.event-stream .stream-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--bright);
  animation: pulse-dot 2s ease-in-out infinite;
  flex-shrink: 0;
}

/* ── Empty State ── */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 48px 24px;
  text-align: center;
}

.empty-state .empty-icon {
  font-size: 36px;
  margin-bottom: 12px;
  opacity: 0.2;
}

.empty-state p {
  font-size: 13px;
  max-width: 320px;
  line-height: 1.6;
  color: var(--ink-500);
}

.empty-state .heading {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink-300);
  margin-bottom: 6px;
}

/* ── Session gate ── */

#key-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(6, 11, 18, 0.92);
  backdrop-filter: blur(10px);
}

#key-overlay.hidden { display: none; }

.key-card {
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--surface-2);
}

.key-eyebrow {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--bright);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.key-title {
  margin: 0 0 6px;
  font-family: var(--font-heading);
  font-size: 19px;
  font-weight: 600;
  color: var(--ink-100);
}

.key-label {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-500);
}

.key-input {
  padding: 9px 11px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.35);
  color: var(--text-0);
  font-family: var(--font-mono);
  font-size: 12px;
}

.key-input:focus { outline: none; border-color: var(--bright); }

.key-hint {
  margin: 0;
  font-size: 11px;
  line-height: 1.6;
  color: var(--ink-600);
}

.key-hint code { font-family: var(--font-mono); color: var(--ink-400); }

.key-error {
  margin: 0;
  padding: 8px 10px;
  border: 1px solid var(--danger);
  border-radius: 6px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--danger);
}

.key-card .key-submit { margin-top: 6px; }
.key-card .key-dev { width: 100%; }

/* ── Identity chip ── */

.identity-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 9px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  max-width: 190px;
}

.identity-name {
  font-size: 11px;
  color: var(--ink-400);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.identity-signout {
  display: flex;
  padding: 3px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--ink-600);
  cursor: pointer;
}

.identity-signout:hover { color: var(--danger); background: rgba(0, 0, 0, 0.3); }

/*
 * The empty state that is not empty — a list the API refused to hand over. Coloured, because the
 * distinction from "you have no runs" has to survive a glance.
 */
.empty-state.runs-error .heading { color: var(--danger); }

.empty-state.runs-error .runs-error-detail {
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-600);
  word-break: break-word;
}

.empty-state.runs-error code {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-400);
}

.empty-state.runs-error .gallery-retry-btn { margin-top: 14px; }

/* ── Progress Sweep ── */

.progress-sweep {
  height: 2px;
  width: 30%;
  background: var(--bright);
  border-radius: 2px;
  animation: sweep 1.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

/* ── Utilities ── */

.hidden { display: none !important; }
.mt-8 { margin-top: 8px; }
.mt-12 { margin-top: 12px; }
.mt-16 { margin-top: 16px; }
.mb-8 { margin-bottom: 8px; }
.mb-12 { margin-bottom: 12px; }
.mb-16 { margin-bottom: 16px; }

/* ── Animations ── */

@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* Was a pulsing cyan halo on the live-run header. A glow that grows and shrinks reads as
   decoration; a border that brightens reads as "this is live" and costs no attention. */
@keyframes breathe {
  0%, 100% { border-color: var(--line); }
  50% { border-color: var(--line-strong); }
}

@keyframes sweep {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(420%); }
}

@keyframes manifest-in {
  from { opacity: 0; filter: blur(6px); }
  to { opacity: 1; filter: blur(0); }
}

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ── Activity Dock ── */

.activity-dock {
  /* In flow at the foot of the view frame — see the note on #cell-canvas-container. */
  position: relative;
  flex-shrink: 0;
  background: var(--ink-900);
  border-top: 1px solid var(--edge);
  z-index: 20;
  transition: height var(--transition);
  height: var(--dock-height, 240px);
  display: flex;
  flex-direction: column;
}
.activity-dock.collapsed { height: 36px; overflow: hidden; }
/* Opened by a replay on a short screen: see `setDockOpen`. Capped rather than fixed, so an
   operator who has dragged it *smaller* than this keeps their height. */
.activity-dock.lean { height: min(var(--dock-height, 240px), 188px); }
/* Nothing to resize when there is nothing showing, and the strip would sit over the header's
   own click target. */
.activity-dock.collapsed .resize-handle { display: none; }

.activity-dock-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 12px;
  height: 36px;
  flex-shrink: 0;
  cursor: pointer;
  border-bottom: 1px solid var(--edge);
}
.activity-dock-header:hover { background: var(--ink-850); }

.activity-dock-left {
  display: flex;
  align-items: center;
  gap: 10px;
}

.activity-expand-btn {
  background: none;
  border: none;
  color: var(--ink-500);
  cursor: pointer;
  padding: 4px;
  border-radius: var(--radius-sm);
}
.activity-expand-btn:hover { color: var(--ink-200); }
.activity-expand-btn svg { display: block; }

/* The columns scroll individually, so the body must not become a scroller of its own. */
.activity-dock-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.activity-columns {
  display: flex;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.activity-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--edge);
  overflow: hidden;
}
.activity-col:last-child { border-right: none; }
.activity-col-wide { flex: 2; }

.activity-col-header {
  padding: 4px 10px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-600);
  border-bottom: 1px solid var(--edge);
  flex-shrink: 0;
}

.agent-board {
  flex: 1;
  overflow-y: auto;
  padding: 6px 8px;
}

.agent-level-group { margin-bottom: 6px; }

.agent-level-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 3px;
}

.agent-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 3px 6px;
  border-radius: 4px;
  font-size: 10px;
  color: var(--ink-400);
  margin-bottom: 2px;
}
.agent-card.agent-active { background: var(--accent-faint); color: var(--ink-200); }

.agent-name {
  text-transform: capitalize;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
}

.agent-stats {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  flex-shrink: 0;
  margin-left: 6px;
}

.agent-empty {
  font-size: 10px;
  color: var(--ink-600);
  text-align: center;
  padding: 12px;
}

.activity-ticker {
  flex: 1;
  overflow-y: auto;
  padding: 4px 8px;
}

.ticker-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-size: 10px;
  color: var(--ink-400);
  animation: manifest-in 0.2s ease;
}

.ticker-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  flex-shrink: 0;
}

.ticker-time {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  flex-shrink: 0;
}

.ticker-kind {
  font-weight: 500;
  color: var(--ink-300);
  text-transform: capitalize;
  flex-shrink: 0;
}

.ticker-msg {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
}

.activity-node-stream {
  display: flex;
  gap: 4px;
  padding: 4px 8px;
  overflow-x: auto;
  border-top: 1px solid var(--edge);
  flex-shrink: 0;
}

.node-stream-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 8px;
  border-radius: 9999px;
  border: 1px solid var(--edge);
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-200);
  cursor: pointer;
  white-space: nowrap;
  animation: manifest-in 0.3s ease;
  flex-shrink: 0;
}
.node-stream-pill:hover { border-color: var(--edge-hover); }
.node-stream-pill.fading { opacity: 0; transition: opacity 0.3s; }

.node-pill-entity { font-weight: 500; }

.node-pill-dir {
  font-size: 9px;
  font-weight: 600;
  text-transform: uppercase;
}
.node-pill-dir.up { color: #ff4d6a; }
.node-pill-dir.down { color: #4da6ff; }
.node-pill-dir.present { color: #2ecc71; }

.activity-last-event {
  font-size: 10px;
  color: var(--ink-400);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Diff Cards ── */

.diff-cards {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.diff-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
}
.diff-card:hover { border-color: var(--edge-hover); }

/* ── Keyboard Help ── */

.kb-help-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.kb-help-card {
  background: var(--ink-900);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: 20px;
  min-width: 320px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

.kb-help-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
}

.kb-help-body { display: flex; flex-direction: column; gap: 6px; }

.kb-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--ink-300);
}

.kb-row kbd {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 4px;
  border: 1px solid var(--edge);
  background: var(--ink-950);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-200);
  min-width: 24px;
  text-align: center;
}

/* The resize handles are defined once, further down — this was an earlier copy of the same
   three rules, overridden in full by the later block and so only a trap for the next edit. */

/* ── Coverage View ── */

.coverage-table {
  width: 100%;
  border-collapse: collapse;
}

.coverage-table th,
.coverage-table td {
  padding: 6px 8px;
  border: 1px solid var(--edge);
  font-size: 11px;
}

.coverage-table th {
  background: var(--ink-900);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-500);
  text-align: left;
}

.coverage-cell {
  text-align: center;
  cursor: pointer;
  transition: background var(--transition);
}
.coverage-cell:hover { border-color: var(--edge-hover); }

/* The dock's own box is defined once, further up. What was here was an earlier version of the
   same six rules, and being later it won the cascade — its `.activity-dock-body` padding and
   `overflow-y: auto` wrapped the column layout in a second scroller. */

.activity-last-event {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-500);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Stream Pill ── */

.stream-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 9999px;
  border: 1px solid var(--edge);
  white-space: nowrap;
  color: var(--ink-500);
}
.stream-pill.connected { color: var(--bright); border-color: var(--line-focus); }
.stream-pill.reconnecting { color: var(--caution); border-color: rgba(210, 153, 34, 0.3); }
.stream-pill.stalled { color: var(--cost); border-color: rgba(219, 109, 40, 0.3); }
/* A finished run is a resting state, not a fault: readable, but with none of the urgency
   the caution/cost hues carry. */
.stream-pill.finished { color: var(--ink-400); border-color: var(--edge); }
.stream-pill.disconnected { color: var(--ink-600); }
/* A recording, playing. Deliberately not the live hue: the whole point of the label is that a
   replay cannot be mistaken for work being done right now. */
.stream-pill.replaying { color: var(--caution); border-color: rgba(210, 153, 34, 0.35); }

.stream-pill-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.stream-pill.connected .stream-pill-dot { animation: pulse-dot 2s ease-in-out infinite; }
.stream-pill.replaying.playing .stream-pill-dot { animation: pulse-dot 2s ease-in-out infinite; }

/* ── Stage Bar ── */

.activity-stage-bar {
  display: flex;
  align-items: center;
  gap: 0;
  padding: 6px 0;
  border-bottom: 1px solid var(--edge);
}

.stage-segment {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-600);
  transition: all var(--transition);
}
.stage-segment.active { color: var(--bright); background: var(--accent-faint); }
.stage-segment.completed { color: var(--ink-400); }
/* Not reached, and never will be on this run — mechanism discovery only runs for an unknown
   molecule. Dimmed and struck through, so it reads as "not applicable", not "still pending". */
.stage-segment.skipped { color: var(--ink-600); }
.stage-segment.skipped .stage-label { text-decoration: line-through; }

.stage-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 2px solid var(--ink-600);
  background: var(--ink-700);
  flex-shrink: 0;
  transition: all var(--transition);
}
.stage-segment.active .stage-dot { border-color: var(--bright); background: var(--bright); animation: pulse-dot 2s ease-in-out infinite; }
.stage-segment.completed .stage-dot { border-color: var(--ink-400); background: var(--ink-400); }

.stage-connector {
  width: 20px;
  height: 2px;
  background: var(--ink-700);
  flex-shrink: 0;
}

/* ── Activity Columns ── */

.activity-columns {
  display: flex;
  gap: 12px;
  flex: 1;
  min-height: 0;
}

.activity-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.activity-col-wide { flex: 2; }

.activity-col-header {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-600);
  margin-bottom: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--edge);
}

/* ── Agent Board ── */

.agent-board {
  flex: 1;
  overflow-y: auto;
}

.agent-level-group { margin-bottom: 8px; }

.agent-level-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin-bottom: 4px;
}

.agent-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  border-left: 2px solid transparent;
  margin-bottom: 2px;
}
.agent-card.agent-active { background: var(--accent-faint); border-left-color: var(--bright); }

.agent-name {
  color: var(--ink-300);
  text-transform: capitalize;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.agent-stats {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  white-space: nowrap;
}

.agent-empty {
  font-size: 11px;
  color: var(--ink-600);
  padding: 12px 0;
}

/* ── Ticker ── */

.activity-ticker {
  flex: 1;
  overflow-y: auto;
  font-size: 11px;
}

.ticker-item {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 2px 0;
  border-bottom: 1px solid var(--line);
}

.ticker-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 4px;
}

.ticker-time {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  white-space: nowrap;
}

.ticker-kind {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-500);
  text-transform: capitalize;
  white-space: nowrap;
}

.ticker-msg {
  color: var(--ink-400);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Node Stream ── */

.activity-node-stream {
  display: flex;
  gap: 6px;
  padding: 6px 0;
  overflow-x: auto;
  flex-shrink: 0;
  scrollbar-width: none;
}
.activity-node-stream::-webkit-scrollbar { display: none; }

.node-stream-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 9999px;
  border: 1px solid var(--edge);
  font-size: 10px;
  white-space: nowrap;
  cursor: pointer;
  transition: all var(--transition);
  animation: pill-slide-in 300ms ease-out;
  flex-shrink: 0;
}
.node-stream-pill:hover { border-color: var(--edge-hover); background: var(--accent-faint); }
.node-stream-pill.fading { opacity: 0; transition: opacity 300ms ease-out; }

.node-pill-entity {
  font-weight: 500;
  color: var(--ink-200);
}

.node-pill-dir {
  font-family: var(--font-mono);
  font-size: 8px;
  font-weight: 600;
  text-transform: uppercase;
  padding: 1px 4px;
  border-radius: 3px;
}
.node-pill-dir.up { color: #ff4d6a; background: rgba(255, 77, 106, 0.15); }
.node-pill-dir.down { color: #4da6ff; background: rgba(77, 166, 255, 0.15); }
.node-pill-dir.present { color: #2ecc71; background: rgba(46, 204, 113, 0.15); }
.node-pill-dir.altered { color: #f59e0b; background: rgba(245, 158, 11, 0.15); }

/* ── Detail Panel Enhancements ── */

/* The panel is a flex column; every one of its pinned children keeps its natural height rather
   than being squeezed when the tab content is long. */
#detail-panel.expanded > .detail-header,
#detail-panel.expanded > .detail-stats,
#detail-panel.expanded > .detail-tabs,
#detail-panel.expanded > .detail-filter,
#detail-panel.expanded > .console-panel { flex: none; }

.detail-stats {
  display: flex;
  gap: 12px;
  margin-bottom: 12px;
}

.detail-stat {
  font-family: var(--font-heading);
  font-size: 16px;
  font-weight: 700;
  color: var(--ink-200);
}

.detail-stat.moved { color: var(--caution); }

.detail-stat-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  color: var(--ink-600);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* One filter over the whole structure — findings, evidence and connections all answer to it. */
.detail-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}

.detail-search {
  flex: 1;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  color: var(--text-0);
  font-family: var(--font-sans);
  font-size: 11px;
}
.detail-search::placeholder { color: var(--text-3); }
.detail-search:focus { outline: none; border-color: var(--line-focus); background: var(--surface-2); }

.detail-filter-count {
  flex: none;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-3);
  white-space: nowrap;
}

.detail-empty {
  margin: 8px 0 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-3);
}

.detail-hint {
  margin: 4px 0 8px;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--text-3);
}

.detail-tabs {
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--edge);
  padding-bottom: 0;
}

.detail-tab {
  padding: 6px 12px;
  border: none;
  background: none;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  color: var(--ink-500);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: all var(--transition);
}
.detail-tab:hover { color: var(--ink-200); }
.detail-tab.active { color: var(--bright); border-bottom-color: var(--bright); }

.detail-tab-content { margin-top: 12px; }

/* Pinned to the foot of the panel: the routes out of it should not depend on how far down a
   two-hundred-row list the reader has scrolled. */
.detail-open-in {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  flex: none;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}

.open-in-btn {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  padding: 4px 10px;
  border-radius: 4px;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
  transition: all var(--transition);
}
.open-in-btn:hover { border-color: var(--edge-hover); color: var(--bright); }

/* ── Evidence ── */

.evidence-group { margin-bottom: 12px; }

.evidence-group-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.evidence-item {
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}

.evidence-item-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

/* The cited finding, as a way back into its record. */
.evidence-entity {
  padding: 0;
  border: none;
  background: transparent;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  color: var(--text-0);
  text-align: left;
  cursor: pointer;
  transition: color var(--transition);
}
.evidence-entity:hover { color: var(--accent); text-decoration: underline; }
.evidence-entity:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px; }
.evidence-entity[disabled] { cursor: default; }
.evidence-entity[disabled]:hover { color: var(--text-0); text-decoration: none; }

.evidence-domain {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-3);
  text-transform: capitalize;
}

.evidence-item-head .ni-obs-link { margin-left: auto; }

.evidence-source {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--text-3);
  margin-top: 2px;
}

/* ── Connections ── */

/*
 * A row is a disclosure: the two ends and the strength on the summary line, the agent's written
 * mechanism inside. The mechanism is the part worth reading and the part most likely to be wrong,
 * but three lines of prose per edge across sixty edges is not a list anyone scans.
 */
.connection-item {
  border-bottom: 1px solid var(--line);
  font-size: 11px;
}

.connection-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
  transition: background var(--transition);
}
.connection-head::-webkit-details-marker { display: none; }
.connection-head:hover { background: var(--surface-2); }
.connection-item[open] > .connection-head { background: var(--surface-2); }

.conn-node {
  max-width: 42%;
  padding: 2px 6px;
  border: none;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  color: var(--text-0);
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 500;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.conn-node:hover { background: var(--accent-dim); color: var(--text-0); }
.conn-node:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }

/* An end the snapshot does not hold. Reads as a label, not as a control that does nothing. */
.conn-node.unknown {
  background: transparent;
  border: 1px dashed var(--line);
  color: var(--text-3);
  cursor: help;
}

.conn-arrow {
  flex: none;
  font-family: var(--font-mono);
  font-size: 9px;
  white-space: nowrap;
}

.conn-strength {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-3);
}

.connection-body {
  padding: 2px 8px 8px 10px;
  border-left: 2px solid var(--line);
  margin: 0 0 6px 6px;
}

/* ── Verdict View ── */

/*
 * The page holds a column, and the column is wide.
 *
 * Without a bound the view inherits the workspace — 1400px on a desktop — while everything in it
 * that is prose is capped near 78ch, so the text sat against the left edge with half the page
 * empty beside it. The cap is a reading measure, not a layout: the sections below use the whole
 * of this width (the synthesis in two columns, the cards in a grid), and the width itself is held
 * where a 13px line stays comfortable rather than stretched to whatever monitor is in the room.
 */
.verdict-view { max-width: 1320px; }

/* ── The headline ── */

/*
 * `--vc` is the verdict's own colour, set inline by the renderer and used for every accent in
 * this banner. Nothing here mixes a second hue: the page's one job at the top is to say which
 * way the run went, and it says it in the colour the rest of the page already uses for that.
 */
.verdict-headline {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 28px;
  margin-bottom: 16px;
  padding: 18px 22px 14px;
  border: 1px solid color-mix(in srgb, var(--vc) 30%, var(--edge));
  border-left: 4px solid var(--vc);
  border-radius: var(--radius);
  background:
    linear-gradient(104deg,
      color-mix(in srgb, var(--vc) 15%, var(--glass-bg)) 0%,
      color-mix(in srgb, var(--vc) 4%, var(--glass-bg)) 46%,
      var(--glass-bg) 78%);
  box-shadow: var(--shadow-2);
}

.verdict-headline-main { min-width: 0; flex: 1; }

.verdict-headline-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 4px;
}

.verdict-headline-word {
  font-family: var(--font-heading);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-transform: capitalize;
  color: var(--vc);
  text-shadow: 0 0 26px color-mix(in srgb, var(--vc) 34%, transparent);
}

.verdict-headline-scope {
  font-size: 13px;
  color: var(--ink-300);
}

.verdict-headline-subject {
  margin-bottom: 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--ink-500);
}

.verdict-headline-bar { height: 8px; border-radius: 4px; }

.verdict-headline .verdict-legend-row { margin-bottom: 0; }

/* The qualifiers, held to the right where they read as conditions on the word rather than as
   part of it. They are the difference between a finding and a number. */
.verdict-headline-stats {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-left: 20px;
  border-left: 1px solid var(--edge);
}

.verdict-headline-stat { display: flex; flex-direction: column; gap: 2px; white-space: nowrap; }

.verdict-headline-stat-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-600);
}

.verdict-headline-stat-value {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--ink-200);
}

/* Under about a thousand pixels the qualifiers stop being a margin and start being a second
   column competing with the finding. They go below it instead. */
@media (max-width: 1000px) {
  .verdict-headline { flex-direction: column; gap: 14px; }
  .verdict-headline-stats {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 18px;
    padding: 12px 0 0;
    border-left: 0;
    border-top: 1px solid var(--edge);
  }
}

/* ── Synthesis ── */

.verdict-synthesis { display: grid; gap: 10px; }

@media (min-width: 1080px) {
  .verdict-synthesis {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    gap: 28px;
    align-items: start;
  }
  /* The right column is the shorter one and it is the one people read first — the named targets
     and what the run could not settle. Separated by a rule rather than by whitespace alone, so
     the two halves do not read as one wrapped paragraph. */
  .verdict-synthesis-side {
    padding-left: 28px;
    border-left: 1px solid var(--edge);
  }
  .verdict-synthesis-side .verdict-targets { margin-top: 0; }
}

.verdict-level-group { margin-bottom: 20px; }

.verdict-level-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  font-family: var(--font-heading);
  font-size: 13px;
}

.verdict-level-count { font-family: var(--font-mono); font-size: 10px; color: var(--ink-500); }

/* No measure of its own any more: the synthesis column above is the measure, and a second cap
   inside it only put the text back against the left edge of a column already narrow enough. */
.verdict-summary {
  margin: 8px 0 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-300);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  overflow: hidden;
}
.verdict-summary-wrap.open .verdict-summary {
  -webkit-line-clamp: unset;
  line-clamp: unset;
  overflow: visible;
}

.verdict-more {
  margin-top: 6px;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-500);
  cursor: pointer;
}
.verdict-more:hover { color: var(--bright); }

.verdict-targets { margin-top: 14px; }

.verdict-target { padding: 7px 0; border-top: 1px solid var(--edge); }

.target-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}

.target-entity { font-family: var(--font-mono); font-size: 11px; color: var(--ink-200); }
.target-observable { font-size: 11px; color: var(--ink-400); }
.target-domain {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--ink-600);
  margin-left: auto;
}

.target-dir {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.target-dir.up { color: var(--up); }
.target-dir.down { color: var(--down); }
.target-dir.altered, .target-dir.relocated { color: var(--verdict-ambiguous); }

.target-rationale {
  margin: 3px 0 0;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--ink-500);
  max-width: 78ch;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.verdict-open { margin-top: 14px; }

.verdict-question {
  padding: 6px 0 6px 10px;
  border-left: 2px solid var(--edge);
  margin-bottom: 6px;
}
.verdict-question p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink-300); }
.verdict-question .question-reason { margin-top: 3px; font-size: 11.5px; color: var(--ink-600); }

/* Both steps side by side down to the point where the tallies stop fitting. */
.verdict-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
  margin-top: 10px;
}

/* Each step carries the colour of whichever verdict led it, on the edge — which way the treated
   cell went, and which way the untreated line was already going, at a glance and side by side. */
.verdict-step {
  --vc: #717E90;
  padding-left: 14px;
  border-left: 3px solid color-mix(in srgb, var(--vc) 70%, transparent);
}

.verdict-step-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 8px;
}

.verdict-step .verdict-step-name { color: var(--vc); }

.verdict-step-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 13px;
}

.verdict-step-count { font-family: var(--font-mono); font-size: 10px; color: var(--ink-500); }

.verdict-bar {
  display: flex;
  gap: 2px;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
}
.verdict-bar-seg { display: block; min-width: 3px; }

.verdict-legend-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 12px;
}

.verdict-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-400);
}
.verdict-legend-item.muted { color: var(--ink-600); }
.verdict-legend-item b { color: var(--ink-200); font-weight: 600; }
.verdict-legend-dot { width: 7px; height: 7px; border-radius: 50%; }

.verdict-domain-table { width: 100%; border-collapse: collapse; }
.verdict-domain-table tr { border-top: 1px solid var(--edge); }
.verdict-domain-table td { padding: 5px 0; vertical-align: baseline; }

.vd-name { font-size: 12px; color: var(--ink-300); text-transform: capitalize; white-space: nowrap; }
.vd-tally { display: flex; flex-wrap: wrap; gap: 8px; padding-left: 12px !important; }
.vd-pill { font-family: var(--font-mono); font-size: 10px; }
.vd-total {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-600);
  text-align: right;
  width: 2.5ch;
}

.verdict-note {
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-600);
  margin-top: 8px;
}

/* Provenance sits under the meta line, before the fold: whether an agent concluded this or
   the readout inferred it changes what the badge above is worth, so it is never hidden. */
.verdict-provenance {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.verdict-source {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  padding: 1px 6px;
  border-radius: 3px;
  border: 1px solid var(--edge);
  color: var(--ink-600);
}
.verdict-source.assessed { color: var(--ink-300); border-color: var(--ink-600); }
.verdict-source.derived { color: var(--caution); border-color: currentColor; }
.verdict-dissent {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--caution);
}

/* A verdict one assessor could have reversed. Sits directly under the provenance row because
   that row is where a reader goes to weigh the badge, and it is styled as a caution rather than
   an error: the word above is still the best reading available, it just cost more to reach than
   the badge admits. */
.verdict-contested {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-top: 6px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-600);
}
.verdict-contested-badge {
  flex: none;
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 1px 6px;
  border-radius: 3px;
  border: 1px solid currentColor;
  color: var(--caution);
}

.verdict-coverage { margin: 4px 0 0; padding-left: 14px; }
.verdict-coverage li { margin: 2px 0; }

/* What adoption left unsettled. Reads as a record, not a warning banner: these are findings
   a bench experiment resolves, and colouring them like errors would bury them under the
   run's real errors. */
.adoption-step { margin-top: 12px; }
.adoption-step-head {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-600);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--edge);
}
.adoption-group { margin-top: 8px; }
.adoption-group-head {
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-300);
  margin-bottom: 4px;
}
.adoption-list {
  margin: 0;
  padding-left: 16px;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.7;
  color: var(--ink-600);
  word-break: break-all;
}
.adoption-contested {
  border-left: 2px solid var(--caution);
  padding: 4px 0 4px 8px;
  margin-bottom: 6px;
}
.adoption-contested-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 10px;
}
.adoption-round {
  font-family: var(--font-mono);
  color: var(--ink-600);
}
.adoption-route { color: var(--ink-300); }
.adoption-decision {
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  padding: 1px 5px;
  border-radius: 3px;
  border: 1px solid var(--edge);
  color: var(--ink-600);
}
.adoption-decision.reject { color: var(--danger); border-color: currentColor; }
.adoption-decision.accept,
.adoption-decision.accept_edge_only { color: var(--caution); border-color: currentColor; }
.adoption-node {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-600);
  margin-top: 3px;
  word-break: break-all;
}
.adoption-reason {
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-300);
  margin-top: 3px;
}

/* The card head is the click target; the body is what unfolds. */
.verdict-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.verdict-card-head .verdict-domain { margin-bottom: 0; }

.verdict-card-body { display: none; margin-top: 10px; }
.verdict-card.open .verdict-card-body { display: block; }

/*
 * An open card takes the whole row. Closed, these are summary tiles and four across is right;
 * open, the card holds several paragraphs of an assessor's reasoning, and a 268px column sets
 * that argument in a five-word measure beside an empty half-screen.
 */
.verdict-card.open { grid-column: 1 / -1; }

/* Two columns of prose at full width, so a long domain's notes do not run to a metre of
   scrolling — and one column where there is not room for two. */
.verdict-card.open .verdict-assessments {
  columns: 2;
  column-gap: 28px;
}

@media (max-width: 1100px) {
  .verdict-card.open .verdict-assessments { columns: 1; }
}

/* Nothing may be split across the column break: half an argument at the foot of one column
   and its conclusion at the head of the next is worse than an uneven pair of columns. */
.verdict-card.open .assessment {
  break-inside: avoid;
  page-break-inside: avoid;
}

.verdict-card.open .verdict-assessments .section-eyebrow { column-span: all; }

.verdict-nodes { margin-top: 10px; }
.verdict-node-id {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--ink-400);
  background: var(--ink-900, rgba(255, 255, 255, 0.03));
  border-radius: 3px;
  padding: 1px 5px;
  margin: 2px 3px 0 0;
  word-break: break-all;
}

/* ── The assessors' own words ──
 *
 * The one part of a verdict card that was written rather than counted, so it is given prose
 * width and left unclamped: these notes are the argument, and a truncated argument is a claim.
 */
.verdict-assessments { margin-top: 12px; }

.assessment {
  border-left: 2px solid var(--edge);
  padding: 2px 0 2px 10px;
  margin-bottom: 10px;
}

/* An assessor who disagreed with the domain's verdict is marked, not hidden: a five-to-two
   split and a unanimous call cost the same to read here and mean different things at the bench. */
.assessment.dissenting { border-left-color: var(--caution); }

.assessment-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 3px;
}

.assessment-entity {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-200, var(--text-1));
}

.assessment-observable {
  font-size: 10.5px;
  color: var(--ink-400);
}

/* In the node inspector a supporting assessment names the node it actually concluded about, and
   that name is a way to walk to it — the same affordance an edge's neighbour has. */
.assessment-entity[data-open-node] {
  cursor: pointer;
  border-bottom: 1px dotted var(--ink-500);
}

.assessment-entity[data-open-node]:hover { color: var(--bright); border-bottom-color: var(--bright); }

.assessment-compartment {
  font-size: 9.5px;
  color: var(--ink-500);
}

.assessment-verdict {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-left: auto;
}

.assessment-conf {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--ink-500);
}

.assessment-note {
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--ink-300, var(--text-2));
  margin: 0 0 4px;
}

.assessment-note.muted { color: var(--ink-500); font-style: italic; }

.assessment-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--ink-500);
}

/* "No edges cited" is a defect in the evidence, not a neutral fact about the row. */
.assessment-foot .warn { color: var(--caution); }

.assessment-signal {
  padding: 0 4px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.05);
  cursor: help;
}

/* An error signal means the assessment rests on something the graph does not contain. That is
   not a footnote on a page whose numbers book bench time. */
.assessment-signal.error {
  color: var(--danger);
  background: rgba(199, 91, 91, 0.12);
}

/* The nodes' deviation from a normal cell — labelled, because unlabelled it reads as the
   drug's effect and frequently states its opposite. */
.verdict-shape {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  cursor: help;
}

.verdict-shape-label { color: var(--ink-600, var(--text-3)); }
.verdict-shape-value { color: var(--ink-400); }

.verdict-chains { margin-top: 4px; }

.verdict-chain {
  border-left: 2px solid var(--edge);
  padding-left: 10px;
  margin-bottom: 10px;
}

.chain-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  margin-bottom: 4px;
}
.chain-rank { font-weight: 700; color: var(--ink-400); }
.chain-len { margin-left: auto; }

.chain-step {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px;
  font-size: 10px;
  padding: 2px 0;
}
.chain-step code { font-family: var(--font-mono); color: var(--ink-300); word-break: break-all; }
.chain-arrow { color: var(--ink-600); }
.chain-arrow.positive { color: var(--up); }
.chain-arrow.negative { color: var(--down); }
.chain-arrow.biphasic, .chain-arrow.context_dependent { color: var(--verdict-ambiguous); }
.chain-mech { flex-basis: 100%; color: var(--ink-600); line-height: 1.45; }

.verdict-paths { padding-left: 8px; }

.causal-path {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 3px 0;
  font-size: 10px;
}

.path-rank {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--ink-500);
}

.path-chain {
  font-family: var(--font-mono);
  color: var(--ink-300);
  word-break: break-all;
}

.path-conf {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-600);
  white-space: nowrap;
  margin-left: auto;
}

.verdict-open-in {
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 9px;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-500);
  cursor: pointer;
  transition: all var(--transition);
}
.verdict-open-in:hover { border-color: var(--edge-hover); color: var(--bright); }

/* ── Synthesis ── */

.synthesis-row {
  display: flex;
  gap: 8px;
  padding: 4px 0;
  font-size: 12px;
}

.synthesis-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--ink-500);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  min-width: 90px;
}

.synthesis-value { color: var(--ink-200); }
.synthesis-value.mono { font-family: var(--font-mono); font-size: 11px; }

.synthesis-findings { padding-left: 8px; }

.finding-item {
  font-size: 12px;
  color: var(--ink-300);
  padding: 3px 0;
  border-left: 2px solid var(--edge);
  padding-left: 10px;
  margin-bottom: 4px;
}

/* ── Coverage Table ── */

/*
 * The table scrolls to the bottom of the panel, not to an arbitrary 600px. The fixed height
 * cut the last visible row in half and left the space beneath it empty — the reader could see
 * there was more, but not that the view had simply stopped short of its own container.
 */
.coverage-table-wrapper {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
}

.coverage-level-row td {
  padding: 10px !important;
  font-family: var(--font-heading) !important;
  font-size: 12px !important;
  background: var(--ink-950) !important;
  border-bottom: 1px solid var(--edge) !important;
}

.coverage-domain-row { cursor: pointer; transition: background var(--transition); }
.coverage-domain-row:hover { background: var(--accent-faint) !important; }

.coverage-domain-name {
  text-transform: capitalize;
  font-weight: 500;
}

/* A domain the drug never entered is the finding this table exists to surface, so it is marked
   rather than left as an unremarkable zero among the other zeroes in the row. */
.coverage-reached {
  color: var(--bright);
  font-weight: 600;
}

.coverage-unreached {
  color: var(--ink-600);
  border-bottom: 1px dotted var(--ink-600);
  cursor: help;
}

/* ── Effort View ── */

/* One scroller for the whole view, rather than the coverage view's arrangement of a fixed head
   above a table that scrolls on its own. This view is a table *followed* by two panels, and
   under `.coverage-table-wrapper` (`flex: 1; min-height: 0`) the table collapsed to a hairline
   to make room for them — the panels have content height and the wrapper is allowed to shrink
   to nothing, so the one element the page exists for was the one that disappeared. */
.effort-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Grows with the table instead of bounding it; the scrolling happens one level up. */
.effort-table-wrap {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow-x: auto;
}

/* A failed turn is a defect, not an absence — coloured like one rather than greyed out the way
   an unreached domain is on the coverage grid. */
.effort-failed {
  color: var(--danger);
  font-weight: 600;
}

/* ── Diff View ── */

.diff-selector { max-width: 400px; }

.diff-row.diff-changed { border-left: 3px solid #e8a127; }
.diff-row.diff-added { border-left: 3px solid #5bbd74; }
.diff-row.diff-removed { border-left: 3px solid #FF5252; }

.verdict-diff-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
}

.verdict-diff-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 14px;
}

.verdict-diff-domain {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 13px;
  text-transform: capitalize;
  margin-bottom: 6px;
}

.verdict-diff-comparison {
  display: flex;
  align-items: center;
  gap: 8px;
}

.verdict-diff-side {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1;
}

.verdict-diff-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  color: var(--ink-600);
  text-transform: uppercase;
}

.verdict-diff-arrow {
  color: var(--ink-600);
  font-size: 14px;
}

/* ── Resize Handles ── */

/*
 * The bar itself is 5px, but the ::before pushes the hit area out to 11px on either side of it.
 * A 4px target is inside the range where people miss it, grab the panel behind it and conclude
 * the edge is not draggable — which is what the two existing handles read as.
 *
 * `touch-action: none` because a drag on a trackpad or a touchscreen would otherwise be claimed
 * as a scroll by the panel underneath before a single pointermove reached the handle.
 */
.resize-handle {
  position: absolute;
  z-index: 30;
  background: transparent;
  touch-action: none;
  transition: background var(--transition);
}
.resize-handle::before {
  content: '';
  position: absolute;
  inset: -3px;
}
.resize-handle:hover,
.resize-handle:focus-visible { background: var(--accent-dim); }
.resize-handle:focus-visible { outline: none; }
/* Held through the drag, when the pointer has long since left the 5px bar. */
.resize-handle.dragging { background: var(--accent); }

/* A grip, so the edge says it can be dragged before anyone tries. */
.resize-handle::after {
  content: '';
  position: absolute;
  border-radius: 2px;
  background: var(--line-strong);
  opacity: 0;
  transition: opacity var(--transition), background var(--transition);
}
.resize-handle:hover::after,
.resize-handle:focus-visible::after,
.resize-handle.dragging::after { opacity: 1; background: var(--text-2); }

.resize-handle-left,
.resize-handle-right {
  top: 0;
  bottom: 0;
  width: 5px;
  cursor: col-resize;
}
.resize-handle-left { left: 0; }
.resize-handle-right { right: 0; }

.resize-handle-left::after,
.resize-handle-right::after {
  top: 50%;
  left: 50%;
  width: 2px;
  height: 28px;
  transform: translate(-50%, -50%);
}

.resize-handle-top {
  top: 0;
  left: 0;
  right: 0;
  height: 5px;
  cursor: row-resize;
}

.resize-handle-top::after {
  top: 50%;
  left: 50%;
  width: 28px;
  height: 2px;
  transform: translate(-50%, -50%);
}

/*
 * While a drag is live the cursor is the drag cursor everywhere, and nothing selects. Without
 * this, moving a few pixels past the panel edge swaps the cursor back to a text caret and starts
 * highlighting the prose the drag passes over.
 */
body.resizing-col,
body.resizing-col * { cursor: col-resize !important; user-select: none; }
body.resizing-row,
body.resizing-row * { cursor: row-resize !important; user-select: none; }

/*
 * Those transitions exist for the collapse animations. While a resize is live they make the panel
 * trail the input by 150ms, which reads as the edge sticking — and a held arrow key, repeating
 * faster than 160ms, restarts the animation before it lands, so the panel smears instead of
 * stepping. `resizing` is set by both the pointer and the keyboard path; the `-col`/`-row` classes
 * above stay pointer-only, because forcing a drag cursor on someone pressing arrow keys is wrong.
 */
body.resizing #detail-panel,
body.resizing #view-frame,
body.resizing #cell-view-atlas,
body.resizing #run-gallery,
body.resizing .activity-dock { transition: none; }

/* ── Keyboard Shortcut Help ── */

.kb-help-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
}

.kb-help-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 20px 24px;
  max-width: 380px;
  width: 90%;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}

.kb-help-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink-100);
}

.kb-help-body { display: flex; flex-direction: column; gap: 6px; }

.kb-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--ink-300);
}

kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 4px;
  border: 1px solid var(--ink-700);
  background: var(--ink-850);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--ink-300);
}

/* ── Domain Verdict (in detail panel) ── */

.domain-verdict {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}

@keyframes pill-slide-in {
  from { transform: translateX(20px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

/* ── Run Cards (Dashboard) ── */

.run-cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}

.run-card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--transition), transform var(--transition), box-shadow var(--transition);
  animation: fadeInUp 400ms ease both;
}
.run-card:hover {
  border-color: var(--glass-hover-border);
  transform: translateY(-3px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}

.run-card-status {
  height: 3px;
  width: 100%;
}

.run-card-body {
  padding: 14px 16px;
}

.run-card-drug {
  font-weight: 600;
  font-size: 14px;
  color: var(--ink-100);
  margin-bottom: 4px;
}

.run-card-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-400);
}

.run-card-footer {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-600);
}

/* ── Staggered Run Items ── */

.run-item { animation: fadeInUp 300ms ease both; }
.run-item:nth-child(1) { animation-delay: 0ms; }
.run-item:nth-child(2) { animation-delay: 30ms; }
.run-item:nth-child(3) { animation-delay: 60ms; }
.run-item:nth-child(4) { animation-delay: 90ms; }
.run-item:nth-child(5) { animation-delay: 120ms; }
.run-item:nth-child(6) { animation-delay: 150ms; }
.run-item:nth-child(7) { animation-delay: 180ms; }
.run-item:nth-child(8) { animation-delay: 210ms; }
.run-item:nth-child(9) { animation-delay: 240ms; }
.run-item:nth-child(10) { animation-delay: 270ms; }

/*
 * No `backdrop-filter` on either of these. `--glass-bg` is an opaque surface, so the filter
 * blurred nothing visible — but a backdrop-filter at *any* radius still promotes the element to its own
 * compositing layer and makes the compositor re-snapshot everything behind it every frame.
 * Behind both of these is the live WebGL cell. That cost bought zero pixels of difference,
 * and on some GPU/driver pairs the re-snapshot of a canvas mid-draw is what paints the
 * black rectangle over the cell while it is being dragged.
 */

#detail-panel.expanded {
  background: var(--glass-bg);
  border-left: 1px solid var(--glass-border);
}

.activity-dock {
  background: var(--glass-bg);
  border-top: 1px solid var(--glass-border);
}

/* ── Empty State (Enhanced) ── */

.empty-icon-large {
  color: var(--ink-700);
  margin-bottom: 16px;
}

/* ── Responsive ── */

@media (max-width: 1100px) {
  #detail-panel { display: none; }
}

@media (max-width: 900px) {
  #run-gallery { width: 56px; }
  .run-item .run-info, .run-item .run-time, .gallery-search { display: none; }
  .run-item { justify-content: center; padding: 10px; }
  .gallery-header h3 { display: none; }
  .activity-columns { flex-direction: column; }
}

@media (max-width: 600px) {
  #run-gallery { display: none; }
  .dashboard-grid { grid-template-columns: 1fr; }
  .activity-dock { display: none; }
}

/* ══ Node exploration, run log and replay ══════════════════════════════════════════════
 *
 * Three views and one transport control, added so that the three thousand findings a run
 * produces are individually reachable and the run itself is watchable after the fact.
 *
 * All of them are list-shaped and all of them are long, so they share one structural idea:
 * a fixed header and filter bar, then a single scroller that takes the rest of the height.
 * Nothing here paginates — the scrollbar is the honest indicator of how much there is.
 */

/* ── View header, shared by the list views ── */

/* The container scrolls for document-shaped views; the list views scroll their own body
   instead, so that their header and filters stay fixed above a very long list. */
#panel-container.fill {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#panel-container.fill > .filter-bar { flex-shrink: 0; }

.view-header {
  flex-shrink: 0;
  margin-bottom: 14px;
}

.view-title {
  font-family: var(--font-heading);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-0);
  margin: 0 0 4px;
}

.view-subtitle {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-2);
  margin: 0;
  max-width: 70ch;
}

/* ── Node explorer ──
 *
 * Windowed: #nx-spacer carries the full height of the filtered list so the scrollbar tells
 * the truth, and #nx-rows holds only the visible slice, translated into place. Rows come in two
 * heights — 54px for a node, 34px for a group header — and both are set inline by
 * node-explorer.js from the same constants it measures the window with, so the two cannot drift
 * apart the way a duplicated stylesheet number would.
 */

.nx-body {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
}

.nx-spacer { position: relative; }
.nx-rows { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }

.nx-row {
  display: grid;
  grid-template-columns: 4px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 0 14px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--transition);
}
.nx-row:hover { background: var(--surface-2); }
.nx-row:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }

/* The direction stripe. A bar rather than a dot: at 54px rows a dot is lost, and the bar
   doubles as the row's left rule. */
.nx-dir { width: 4px; height: 26px; border-radius: 2px; }

.nx-main { min-width: 0; }

/* The molecule as the agent spelled it, then what was measured about it. Two weights on one
   line rather than two lines, because the second line belongs to the location. */
.nx-name {
  font-size: 13px;
  color: var(--text-0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nx-observable {
  font-size: 11.5px;
  color: var(--text-2);
  margin-left: 6px;
}

/* An endpoint is a node the assessments will rule on, and its polarity says which way is worse.
   Muted rather than accented: about half the list is an endpoint, so this is a property of the
   finding, not a severity scale competing with direction and magnitude. */
.nx-polarity {
  font-size: 9.5px;
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  color: var(--text-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 1px 4px;
  margin-left: 6px;
  vertical-align: 1px;
  white-space: nowrap;
}

/* Where in the cell. The full `entity|observable|compartment` key is the row's tooltip — it is
   for copying, not for reading, and it cost a line that the compartment uses better. */
.nx-where {
  font-size: 10.5px;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nx-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

.nx-chip {
  font-size: 10px;
  font-family: var(--font-mono);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  white-space: nowrap;
}
.nx-chip-mag { color: var(--text-1); }
/* Which specialist agent wrote the finding. Uncoloured on purpose: the palette is spent on
   direction, structure and level, and a fourth scale here would compete with all three. */
.nx-chip-domain { color: var(--text-2); }
.nx-chip-warn { color: var(--caution); background: rgba(198, 169, 107, 0.12); }
/* Added / removed / altered. Outlined in the change colour so it separates from the flat chips
   beside it — it answers a different question than any of them. */
.nx-chip-change {
  background: transparent;
  border: 1px solid var(--line);
  font-weight: 600;
}

/* Group headers. Sticky is deliberately not used: the rows are absolutely positioned inside a
   translated slice, so a sticky header would stick to the slice rather than to the viewport. */
.nx-group {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px;
  box-sizing: border-box;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  border-top: 1px solid var(--line);
}
.nx-group-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.nx-group-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nx-group-count {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-3);
  background: var(--surface-3);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
  flex-shrink: 0;
}

.nx-none { position: absolute; inset: 0; }

/* ── Change view (GRAPH A vs GRAPH B) ──
 *
 * Same windowing contract as the node explorer, with one simplification: group headers here are
 * the same height as node rows, so the slice is found by division rather than by a prefix scan.
 */

.cx-headline {
  display: flex;
  align-items: stretch;
  gap: 10px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

.cx-stats { display: flex; gap: 8px; flex: 1; min-width: 0; }

.cx-stat {
  flex: 1;
  min-width: 92px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
  cursor: pointer;
  transition: all var(--transition);
  text-align: left;
}
.cx-stat:hover { border-color: var(--line-focus); background: var(--surface-2); }
.cx-stat.active { border-color: var(--accent); background: var(--surface-2); }

.cx-stat-value { font-family: var(--font-mono); font-size: 22px; font-weight: 700; line-height: 1; }
.cx-stat-label { font-size: 10.5px; color: var(--text-2); }

/* Held apart from the structural tiles, and read differently: this one is a claim about cause. */
.cx-drug-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  min-width: 200px;
}
.cx-drug-value {
  font-family: var(--font-mono);
  font-size: 22px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
}
.cx-drug-label { font-size: 11px; color: var(--text-1); font-weight: 600; }
.cx-drug-sub { font-size: 10px; color: var(--text-3); }

.cx-note {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-2);
  margin: 0 0 12px;
  padding: 8px 12px;
  border-left: 2px solid var(--line-focus);
  background: var(--surface-1);
  max-width: 90ch;
}

.cx-body {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
}
.cx-spacer { position: relative; }
.cx-rows { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }
.cx-none { position: absolute; inset: 0; }

.cx-group {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px;
  box-sizing: border-box;
  background: var(--surface-2);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.cx-group-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.cx-group-title {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-1);
}
.cx-group-count {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-3);
  background: var(--surface-3);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
}

.cx-row {
  display: grid;
  grid-template-columns: 30px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 0 14px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--transition);
}
.cx-row:hover { background: var(--surface-2); }

/* +, ~, − or · in the change colour. A glyph rather than a word: it is repeated on every row
   and the word is already in the group header above it. */
.cx-change-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-950, #0b0d10);
}

.cx-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cx-line1, .cx-line2 {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cx-name { font-size: 13px; color: var(--text-0); font-weight: 600; }
.cx-observable { font-size: 11.5px; color: var(--text-2); }
.cx-dir { font-size: 11px; font-weight: 600; }
.cx-mag {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--text-2);
  background: var(--surface-3);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
}
.cx-compartment { font-size: 10.5px; color: var(--text-3); }
.cx-sep { color: var(--text-4, var(--text-3)); font-size: 10px; }
.cx-level { font-size: 10.5px; }

/* What actually moved, for an altered node: `direction  up → down`. */
.cx-moved { display: inline-flex; gap: 10px; margin-left: 4px; min-width: 0; }
.cx-moved-field { display: inline-flex; align-items: baseline; gap: 4px; }
.cx-moved-name {
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.cx-moved-from { font-size: 10.5px; color: var(--text-3); text-decoration: line-through; }
.cx-moved-arrow { font-size: 10px; color: var(--text-3); }
.cx-moved-to { font-size: 10.5px; color: var(--text-1); font-weight: 600; }

.cx-attr {
  font-size: 10px;
  font-family: var(--font-mono);
  padding: 2px 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  flex-shrink: 0;
}

/* ── Change view: the mechanism half ──
   Not part of the windowed node list. Only rewritten and lost edges are listed, and both sets
   are small by construction, so this is an ordinary flow list that sits above the scroller. */
.cx-mech {
  margin: 0 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
}
.cx-mech-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-bottom: 1px solid var(--line);
}
.cx-mech-title {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-1);
}
.cx-mech-counts { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); }
.cx-mech-toggle { margin-left: auto; }
.cx-mech-empty { margin: 0; padding: 10px 14px; font-size: 11.5px; color: var(--text-3); }
.cx-mech-list { max-height: 232px; overflow-y: auto; }

.cx-edge {
  display: grid;
  grid-template-columns: 30px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--transition);
}
.cx-edge:last-child { border-bottom: none; }
.cx-edge:hover { background: var(--surface-2); }
.cx-edge-arrow { color: var(--text-3); font-size: 11px; }
.cx-edge-mech {
  font-size: 10.5px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 46ch;
}

@media (max-width: 1400px) {
  .cx-moved { display: none; }
}

/* Which properties of the coupling moved IS the row. A width rule that hides it leaves
   "mek → erk, altered", which is not a finding anyone can act on. Higher specificity than the
   rule above, so it survives regardless of order. */
.cx-edge .cx-moved { display: inline-flex; flex-wrap: wrap; }

@media (max-width: 1400px) {
  /* The chips give way first — the name, what was measured, and where it happened are the
     content. Two survive at any width: the change chip, because it is the reason the row is in
     the list, and an unsourced warning, because a finding written with no observation behind it
     is the one thing a narrow window must not be allowed to hide. */
  .nx-meta .nx-chip:not(.nx-chip-change):not(.nx-chip-warn):nth-child(n+3) { display: none; }
}

/* ── Node inspector (right panel) ── */

.ni-title {
  font-family: var(--font-heading);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-0);
  margin: 0;
}

.ni-id-row {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 8px 0 12px;
}

.ni-id {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--text-1);
  background: var(--surface-0);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 5px 7px;
  /* Wrapped, not truncated: an identifier you cannot read in full is not an identifier. */
  word-break: break-all;
  user-select: all;
}

.ni-copy {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 5px 8px;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.ni-copy:hover { color: var(--text-0); border-color: var(--line-strong); }

.ni-badges { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 14px; }

.ni-section {
  padding-top: 12px;
  margin-top: 12px;
  border-top: 1px solid var(--line);
}

.ni-field {
  display: grid;
  /* minmax(0,…) so a long value wraps inside the panel instead of widening the grid
     and running out past the panel's right edge. */
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 8px;
  align-items: baseline;
  padding: 3px 0;
  font-size: 12px;
}
.ni-field-label { color: var(--text-2); font-size: 11px; }
.ni-field-value { color: var(--text-0); word-break: break-word; }
/* A layer key is one unbreakable token (`perturbation:trametinib@100nm/24h`) and the panel is
   narrow, so the pills wrap as a group and each one breaks internally if it has to. */
.ni-field-value .pill { display: inline-block; white-space: normal; word-break: break-all; }
.ni-field-value.mono { font-family: var(--font-mono); font-size: 11px; }

.ni-count {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-2);
  background: var(--surface-3);
  border-radius: 8px;
  padding: 1px 6px;
  margin-left: 6px;
}

.ni-empty { font-size: 11.5px; line-height: 1.55; color: var(--text-2); margin: 6px 0 0; }

/* The A → B section. The moved rows are the point of it, so the unmoved ones are dimmed rather
   than hidden — seeing that three of four claim fields held is itself the finding. */
.ni-change-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
.ni-hint { font-size: 11px; line-height: 1.5; color: var(--text-2); margin: 6px 0; }

.ni-claim {
  display: grid;
  grid-template-columns: 84px 1fr 14px 1fr;
  align-items: baseline;
  gap: 6px;
  padding: 4px 0;
  border-bottom: 1px solid var(--line);
  font-size: 11.5px;
  opacity: 0.5;
}
.ni-claim.moved { opacity: 1; }
.ni-claim-name {
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ni-claim-from { color: var(--text-2); }
.ni-claim.moved .ni-claim-from { text-decoration: line-through; }
.ni-claim-arrow { color: var(--text-3); text-align: center; }
.ni-claim-to { color: var(--text-0); }
.ni-claim.moved .ni-claim-to { font-weight: 600; color: var(--accent); }

/* ── Evidence ── */

.ni-obs {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin-top: 8px;
}

.ni-obs-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }

.ni-obs-index {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-3);
}

.ni-obs-meta { font-size: 10.5px; color: var(--text-2); }

.ni-obs-link { font-size: 10.5px; color: var(--accent); text-decoration: none; }
.ni-obs-link:hover { text-decoration: underline; }

/* Whether a PMID was checked is the difference between a citable claim and a remembered
   one, so it is stated rather than implied by the presence of a link. */
.ni-verified { font-size: 9.5px; color: var(--gain); }
.ni-unverified { font-size: 9.5px; color: var(--caution); }

.ni-obs-quote {
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--text-1);
  border-left: 2px solid var(--line-strong);
  margin: 6px 0 0;
  padding: 0 0 0 8px;
}

.ni-obs-source { font-size: 10.5px; color: var(--text-2); }

/* The schema's own value, beside the reader's word for it. Small and dim: it is there for the
   person cross-checking the panel against the event log, not for the person reading it. */
.ni-raw {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-3);
  background: var(--surface-1);
  border-radius: 3px;
  padding: 0 3px;
  margin-left: 4px;
}

/* Polarity — whether this node is somewhere a verdict can be read off.
   Two thirds of a run's nodes are mechanism, not result; the ones that are not decide every
   assessment in the readout, so the distinction gets a block rather than a row. */
.ni-polarity {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  padding: 9px 10px;
  margin-bottom: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
}

.ni-polarity.outcome {
  border-color: var(--caution);
  background: color-mix(in srgb, var(--caution) 7%, var(--surface-1));
}

.ni-polarity-mark {
  font-size: 16px;
  line-height: 1.1;
  color: var(--text-2);
  width: 14px;
  text-align: center;
  flex-shrink: 0;
}

.ni-polarity.outcome .ni-polarity-mark { color: var(--caution); }
.ni-polarity-body { min-width: 0; }
.ni-polarity-note { font-size: 12px; font-weight: 600; color: var(--text-0); }
.ni-polarity-hint { font-size: 11px; line-height: 1.5; color: var(--text-2); margin-top: 3px; }

/* ── Inspector: edges ──
   A node's claim is one line; the reason to believe it is the mechanism written on the edges
   around it. Collapsed by default because a well-connected node has twenty, and the headline —
   which neighbour, which way — is what a reader scanning them is after. */

.ni-edge {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  margin-top: 6px;
}

.ni-edge[open] { border-color: var(--line-strong); }

.ni-edge-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  padding: 7px 9px;
  cursor: pointer;
  list-style: none;
  font-size: 11.5px;
}

.ni-edge-head::-webkit-details-marker { display: none; }
.ni-edge-head:hover { background: var(--surface-2); }

/* The neighbour's name inside this row is its own click target — it walks the graph rather than
   expanding the row — so the disclosure needs an affordance of its own, or the two actions are
   indistinguishable and the larger one wins by accident. */
.ni-edge-head::after {
  content: '\25be';
  margin-left: auto;
  color: var(--text-3);
  font-size: 10px;
  transition: transform 120ms ease;
}

.ni-edge[open] > .ni-edge-head::after { transform: rotate(180deg); }

.ni-edge-dir {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.03em;
  white-space: nowrap;
  flex-shrink: 0;
}

/* The neighbour is the click target — following it is how the graph gets walked — so it reads
   as a link inside a row that is itself clickable for a different reason. */
.ni-edge-node {
  color: var(--text-0);
  cursor: pointer;
  border-bottom: 1px dotted var(--line-strong);
  min-width: 0;
  overflow-wrap: anywhere;
}

.ni-edge-node:hover { color: var(--accent); border-bottom-color: var(--accent); }

.ni-edge-flag {
  font-size: 9.5px;
  font-family: var(--font-mono);
  color: var(--caution);
  border: 1px solid var(--caution);
  border-radius: var(--radius-sm);
  padding: 0 4px;
  white-space: nowrap;
}

.ni-edge-body { padding: 0 9px 9px; border-top: 1px solid var(--line); }

.ni-edge-mech {
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--text-1);
  margin: 8px 0;
}

.ni-edge-body .ni-field { padding: 3px 0; }
.ni-edge-obs { margin-top: 8px; }
.ni-edge-signals { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0; }

/* The completeness guarantee: empty when the curated panel above is current, and filled the
   moment the schema gains a field nothing reads. Collapsed, because it is normally empty. */
.ni-other > summary { cursor: pointer; list-style: none; }
.ni-other > summary::-webkit-details-marker { display: none; }
.ni-other > summary:hover { color: var(--text-0); }

/* ── Run log ── */

.lg-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
  position: relative;
}

.lg-row {
  display: grid;
  /* The kind column fits the longest kind there is — `mechanism discovery done` at 24
     characters. Narrower and the two longest ellipsize to the same stub, which is how forty
     `contradiction detected` rows came to read as `contradiction detect…`. */
  grid-template-columns: 62px 152px 130px 1fr 14px;
  gap: 10px;
  align-items: baseline;
  padding: 4px 12px;
  font-size: 11.5px;
  border-bottom: 1px solid var(--line);
}
.lg-row[data-expandable] { cursor: pointer; }
.lg-row[data-expandable]:hover { background: var(--surface-2); }
.lg-row.open { background: var(--surface-2); }

/* The pipeline's own scaffolding is structure, not content — dimmed so the agent turns and
   graph writes between them are what the eye lands on. */
.lg-run { color: var(--text-3); }

/* A retrieval that failed. Deliberately the loudest row in the log after `run_failed`: the log
   is scanned, not read, and a source that answered nothing all run is the one finding here that
   invalidates everything above it. The rail is what makes a run of them visible while scrolling
   past — sixty-seven consecutive marks down the left edge, which no count communicates. */
.lg-row.lg-failed { box-shadow: inset 2px 0 0 var(--danger); }
.lg-row.lg-failed .lg-text { color: var(--danger); }

.lg-time { font-family: var(--font-mono); font-size: 10px; color: var(--text-3); }
.lg-kind { font-family: var(--font-mono); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lg-domain { font-size: 10.5px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lg-text { color: var(--text-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lg-more { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); text-align: center; }
.lg-row.open .lg-more { color: var(--accent); }

.lg-payload {
  margin: 0;
  padding: 10px 12px 12px 74px;
  background: var(--surface-0);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1.55;
  color: var(--text-1);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow-y: auto;
}

@media (max-width: 1400px) {
  .lg-row { grid-template-columns: 62px 152px 1fr 14px; }
  .lg-domain { display: none; }
  .lg-payload { padding-left: 12px; }
}

/* ── Replay transport ──
 *
 * In the dock header, which is the one strip visible in every view and while the dock is
 * collapsed — a replay is watched in the cell view, not in a panel of its own.
 */

.replay-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  max-width: 580px;
  margin: 0 16px;
  min-width: 0;
  /* The header is a click target for collapsing the dock; the transport is not part of it. */
  cursor: default;
}

/*
 * The transport lives in the demo bar now — the same element, re-parented at start-up, so there
 * stays exactly one of it with exactly one set of handlers. It sits at the right-hand end of the
 * bar at its natural width rather than claiming the leftover space, which in the dock header was
 * all there was and here would push the instrumentation off screen.
 */
/*
 * Sized to be operated, not to be read.
 *
 * At 340 wide this held two 22px buttons, a clock, a scrub track and a speed toggle, and every
 * one of them was at the smallest size the design system has — a transport for a demo somebody is
 * *driving*, in front of a room, built to the proportions of a status readout. The two things a
 * presenter reaches for under pressure are the speed and the position, so those are the two that
 * get the room: a track with a thumb you can catch, chapter ticks wide enough to aim at, and a
 * speed control that is a labelled button rather than two characters of mono.
 *
 * The width is taken from the row, which is finite: the card holds at its minimum, the instrument
 * panel is what it is, and the transport takes the rest at three steps. See the card's own
 * `min-width` below 1500, which is where the last of it comes from.
 */
#demo-bar .replay-bar {
  flex: none;
  width: 465px;
  max-width: none;
  margin: 0;
  padding: 9px 18px;
  gap: 11px;
  align-self: center;
}

#demo-bar .replay-btn {
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
}
#demo-bar .replay-btn svg { width: 15px; height: 15px; }
/* Play/pause is the one control that is pressed without looking. */
#demo-bar #replay-toggle { width: 34px; height: 34px; }
#demo-bar #replay-toggle svg { width: 17px; height: 17px; }

#demo-bar .replay-clock {
  font-size: 11px;
  min-width: 78px;
  color: var(--text-1);
}

#demo-bar .replay-track { min-width: 128px; }

#demo-bar .replay-scrub {
  height: 5px;
  border-radius: 3px;
}
#demo-bar .replay-scrub::-webkit-slider-thumb {
  width: 15px;
  height: 15px;
  /* A halo, not a bigger dot: the thumb stays proportionate to a 5px track while the thing the
     pointer has to land on is twice the area. */
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
}
#demo-bar .replay-scrub::-moz-range-thumb { width: 15px; height: 15px; }

/* The chapter ticks are how anyone jumps to a part of the run, which makes them a control and
   not a decoration. Wider target, taller mark, and clear of the thumb's halo. */
#demo-bar .replay-marks { height: 12px; }
#demo-bar .replay-mark { width: 12px; }
#demo-bar .replay-mark::before { height: 9px; }
#demo-bar .replay-mark.is-round::before { height: 6px; }

/*
 * Leaving.
 *
 * The presentation runs itself, and holds its closing shot indefinitely — so the one thing it
 * never does on its own is end. Pause is not an ending either: a paused demo is still a demo,
 * with the rails up, the chrome folded away and a caption over the cell. Without a way out the
 * only exits were reloading the page or stumbling onto one by clicking the scene, which is not
 * something to discover in front of an audience.
 *
 * Last in the row, because that is where a window's close control lives, and tinted rather than
 * ruled off: a separator would cost ten pixels the row does not have at 1340, and the colour
 * says "leaves" more directly than a hairline says "different group". Hidden by visibility and
 * not by display when there is nothing to leave, so the transport keeps one width.
 */
#demo-bar #replay-exit {
  border-color: transparent;
  background: transparent;
  color: var(--text-3);
}
#demo-bar #replay-exit:hover {
  color: #E9A7A9;
  border-color: color-mix(in srgb, #D2777A 46%, transparent);
  background: color-mix(in srgb, #D2777A 13%, var(--surface-2));
}
#demo-bar #replay-exit[data-idle="1"] { visibility: hidden; pointer-events: none; }

#demo-bar .replay-speed {
  min-width: 48px;
  height: 30px;
  padding: 0 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-0);
}

/*
 * The row's width budget, and the reason these four breakpoints sit where they do.
 *
 * `#demo-bar` lays out three things and clips what does not fit: the chapter card (shrinkable
 * down to its minimum), the instrument panel (fixed to its contents) and the transport (fixed
 * to the step below). Clipping falls on the transport, because it is last — so every step has
 * to be checked against the sum, not chosen for how it looks on its own. Measured at 1366:
 * panel 543–557 depending on whether a counter has just gained a digit, and it can only grow.
 *
 *   >= 1600   card 470 + panel 557 + transport 511 = 1538
 *   1500-1599 card 470 + panel 523 + transport 465 = 1458
 *   1440-1499 card 400 + panel 523 + transport 465 = 1388
 *   1340-1439 card 400 + panel 523 + transport 401 = 1324
 *   <= 1339   card 400 + panel 213 + transport 401 = 1014
 *
 * The one that bit was 1366 — the commonest laptop width there is, and one pixel above a
 * breakpoint written as 1365. It carried 400 + 557 + 424 = 1381 and lost fifteen pixels off
 * the right-hand end of the transport.
 *
 * Each transport step is 41 wider than it was: the exit control is a thirtieth button and a gap.
 * The 1500-1599 step paid for it out of the narration column rather than out of the card, which
 * is the only place in that band with 34 pixels that cost nothing (see `.rhud-status` above).
 */
@media (min-width: 1600px) {
  #demo-bar .replay-bar { width: 511px; padding: 9px 22px; gap: 13px; }
}

/* Below 1440 the row cannot carry 424 and leave the card its minimum. The transport tightens
   rather than losing a control: every button is still there and the track is still a hundred
   pixels of aimable scrub. */
@media (max-width: 1439px) {
  #demo-bar .replay-bar { width: 401px; padding: 9px 13px; gap: 9px; }
  #demo-bar .replay-clock { min-width: 68px; }
  #demo-bar .replay-track { min-width: 96px; }
}

/* Narrower still, and the figures go. They are the third copy of themselves on screen — the
   chapter card had them until 1500, and the fleet rail has them per agent throughout — and what
   they cost is the width the narration and the transport both need. */
@media (max-width: 1339px) {
  .rhud-counts { display: none; }
}

.replay-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-1);
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition), background var(--transition);
}
.replay-btn:hover { color: var(--text-0); background: var(--surface-3); border-color: var(--line-strong); }
.replay-bar.playing #replay-toggle { color: var(--accent); border-color: var(--line-focus); }

.replay-speed {
  width: auto;
  padding: 0 7px;
  font-family: var(--font-mono);
  font-size: 10px;
}

/*
 * The scrubber and its round ticks share one box: the ticks are positioned as a percentage of
 * the track, so they have to be measured against exactly the element the thumb travels along.
 */
.replay-track {
  position: relative;
  flex: 1;
  min-width: 60px;
  display: flex;
  align-items: center;
}

.replay-marks {
  position: absolute;
  /* Above the track, clear of the thumb — a tick under the thumb is a tick you cannot click. */
  bottom: 100%;
  left: 0;
  right: 0;
  height: 9px;
  pointer-events: none;
}

.replay-mark {
  position: absolute;
  bottom: 1px;
  /* `left` is the boundary's position along the log; the shift centres the tick on it. */
  transform: translateX(-50%);
  width: 7px;
  height: 7px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  pointer-events: auto;
}
.replay-mark::before {
  content: '';
  display: block;
  width: 1px;
  height: 7px;
  margin: 0 auto;
  background: var(--text-3);
  transition: background var(--transition);
}
.replay-mark:hover::before { background: var(--accent); }
/* A round boundary is navigation; a milestone is the story. Drawn apart so a viewer can aim. */
.replay-mark.is-round::before { height: 5px; opacity: 0.6; }
.replay-mark.is-beat::before { width: 2px; background: var(--text-1); }

.replay-scrub {
  flex: 1;
  min-width: 0;
  height: 3px;
  appearance: none;
  background: var(--surface-3);
  border-radius: 2px;
  cursor: pointer;
  outline: none;
}
.replay-scrub::-webkit-slider-thumb {
  appearance: none;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--accent);
  cursor: grab;
}
.replay-scrub::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: none;
  border-radius: 50%;
  background: var(--accent);
  cursor: grab;
}

.replay-clock {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  /* Fixed, so the track does not shift a pixel every time the second changes. */
  min-width: 66px;
  text-align: right;
}

/*
 * The milestone the playhead has passed, in words. A tick a presenter has to hover is a tick
 * nobody in the room can read, so the current one is spelled out beside the track.
 */
.replay-mark-label {
  font-size: 10px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 190px;
  flex-shrink: 1;
}

.replay-guide {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
  cursor: help;
}
.replay-guide.is-manual { color: var(--caution); }

@media (max-width: 1400px) {
  /* First to go: the label duplicates a tick that is still on screen and still hoverable. */
  .replay-mark-label, .replay-guide { display: none; }
}

@media (max-width: 1100px) {
  /* The scrubber needs room the narrow header does not have; play/pause still does not. */
  .replay-track, .replay-clock { display: none; }
  .replay-bar { flex: 0 0 auto; max-width: none; }
}

/* ── Cell tooltip additions, for the per-node markers ── */

.cell-tooltip-swatch {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  margin-right: 5px;
  vertical-align: middle;
}

.cell-tooltip-hint {
  font-size: 10px;
  color: var(--text-3);
  margin-top: 2px;
}

/* ── The demo bar ─────────────────────────────────────────────────────────────────────────
 *
 * A strip between the view tabs and the workspace, and everything about the replay that is a
 * *reading* lives in it: the chapter card, the run's live instrumentation, and the transport.
 *
 * It used to be one overlay drawn on top of the cell, and that was the whole problem. A panel
 * whose numbers change sixty times a second, laid over the one thing the viewer is trying to
 * watch, means the cell cannot be looked at while it moves — and the numbers themselves read as
 * flicker rather than as instruments, because nothing around them is still. Off the canvas they
 * are a dashboard. On it they were noise.
 *
 * Everything here is a fixed-size element whose text changes. Nothing re-flows on a value change:
 * the counters are tabular figures in fixed columns, the level bars move a `width`, and the
 * chapter card is rewritten only when the chapter changes. That is the layout counterpart to the
 * "nothing is rebuilt" rule in replay-hud.js, and it is why a bar taking three hundred events a
 * second does not shimmer.
 */

#demo-bar {
  flex: none;
  display: flex;
  align-items: stretch;
  gap: 0;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--surface-1) 0%, var(--surface-0) 100%);
  font-family: var(--font-sans);
  overflow: hidden;
}
#demo-bar[hidden] { display: none; }

/* ── The chapter card ── */

.demo-chapter {
  /* A basis rather than `auto`: the card and the instrument panel share one row, and with both
     sized from their content the panel took the room and left the narration ellipsised after
     three words. Measured at 1680: the card asks for 640 and gets whatever is left over besides,
     which is enough for a full title, two lines of body and the run's figures beside them. */
  flex: 1 1 640px;
  min-width: 470px;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 11px 18px;
  border-right: 1px solid var(--line);
  /* Nothing leaves the card. Below the width the row wants, the card is held at its minimum and
     its own contents are what overflow — and an unclipped overflow here does not fall off the
     screen, it lands on the instrument panel next door and draws the run's figures straight
     through the round counter. Clipped, the worst case is a card that shows less. */
  overflow: hidden;
  /* The one coloured surface in the chrome, and it earns it: which of the three acts is on
     screen is the single fact a viewer most needs to keep straight. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--chapter-color, var(--accent)) 12%, transparent) 0%,
    transparent 62%);
}
.demo-chapter[hidden] { display: none; }
.demo-chapter.tone-a { --chapter-color: var(--down); }
.demo-chapter.tone-b { --chapter-color: var(--up); }
.demo-chapter.tone-diff { --chapter-color: var(--accent); }
.demo-chapter.is-empty { --chapter-color: var(--text-3); }

.demo-chapter-rail {
  flex: none;
  width: 52px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.demo-chapter-index {
  display: flex;
  align-items: baseline;
  gap: 3px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  color: var(--chapter-color, var(--text-1));
}

.demo-chapter-of {
  font-size: 9px;
  font-weight: 400;
  color: var(--text-3);
}

.demo-chapter-progress {
  height: 2px;
  border-radius: 1px;
  background: rgba(180, 197, 224, 0.10);
  overflow: hidden;
}

/*
 * The one moving part of the card. A width driven from the playhead rather than a CSS keyframe:
 * a keyframe would run at its own rate, would be wrong at 0.5× and at 4×, and would keep running
 * through a pause. The transition only carries the step a dropped frame leaves behind.
 */
.demo-chapter-progress-fill {
  height: 100%;
  width: 0;
  border-radius: 1px;
  background: var(--chapter-color, var(--accent));
  transition: width 140ms linear;
}

.demo-chapter-text {
  /* Basis zero, not `auto`. With `auto` the block's basis is its own body copy laid out on one
     line — several thousand pixels — so it took every pixel of the row and the stats beside it
     were shrunk to a clipped word. At zero the stats take their natural width and the text gets
     all the rest, which is the priority the card actually wants. */
  flex: 1 1 0;
  min-width: 300px;
}

.demo-chapter-eyebrow {
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--chapter-color, var(--text-2));
  margin-bottom: 3px;
}

.demo-chapter-title {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-0);
  /* One line, always. A title that wraps to two changes the height of the whole bar, which
     resizes the WebGL viewport underneath it — a full scene re-layout, mid-chapter. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.demo-chapter-body {
  margin-top: 2px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-1);
  /* Two lines at most, for the same reason the title is one. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.demo-chapter-stats {
  flex: 0 0 auto;
  min-width: 0;
  max-width: 44%;
  display: flex;
  gap: 18px;
  padding-left: 4px;
  overflow: hidden;
}

/*
 * The figures are the first thing to go.
 *
 * The row is the card, the instrument panel (about 560, plus 316 of level meters above 1700) and
 * the transport (424, and 470 above 1500). The stats take up to 44% of whatever the card gets, so
 * they only genuinely fit once the card is around 750 wide — which, after the transport was given
 * the width it needs to be usable, is a window wider than any laptop. Below that they were being
 * drawn outside the card, on top of the round counter. They are a repeat of numbers the panel is
 * already showing; the narration is not, so the narration keeps the card.
 */
@media (max-width: 2090px) {
  .demo-chapter-stats { display: none; }
}

/*
 * Below 1500 the row is the card's minimum, the instrument panel and the transport, and it adds
 * up to more than the window. The card gives: 400 still carries the index rail, the eyebrow, a
 * full title and two clamped lines of body, which is the whole of what it is for. The alternative
 * was a transport at 340, and a control nobody can hit is worth less than forty pixels of prose.
 */
@media (max-width: 1499px) {
  .demo-chapter { min-width: 400px; }
  /* And the instrument panel tightens with it — 44px of padding and gaps, taken from the spaces
     between the figures rather than from any of the figures. The narration column gives 34 more:
     it is clamped to two lines either way, so all that changes is where they wrap. */
  .rhud-bar { padding: 11px 14px; gap: 16px; }
  .rhud-status { width: 176px; }
  .rhud-counts { gap: 10px; }
}

/* Between 1500 and 1599 the card keeps its 470 and the panel keeps its gaps; the narration column
   gives its 34 anyway, because the transport gained an exit control and the row's spare at 1500
   was eight pixels. The column is clamped to two lines either way, so all that changes is where
   they wrap. */
@media (min-width: 1500px) and (max-width: 1599px) {
  .rhud-status { width: 176px; }
}

.demo-stat { text-align: right; }

.demo-stat-value {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-0);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.demo-stat-label {
  margin-top: 1px;
  font-family: var(--font-mono);
  font-size: 8px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-2);
  white-space: nowrap;
}

/* ── The instrumentation ── */

/*
 * Shrinkable, and it clips rather than shoves. The row is the chapter card, this panel and the
 * transport, and this is the only one of the three whose width comes from its contents — a count
 * going from three digits to four used to widen it, and what came off the end of the row was the
 * right-hand edge of the transport, because the bar clips its overflow. The panel is also the one
 * of the three that repeats itself: every figure here is also on the rails. So it gives first.
 */
.rhud-bar {
  /* It does not shrink — a clipped counter is worse than a shorter narration, and the card is
     the one element in the row that degrades gracefully. `overflow: hidden` is a guard, not a
     mechanism: below 1280 the counters are dropped outright rather than squeezed. */
  flex: 0 0 auto;
  min-width: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 11px 18px;
  border-right: 1px solid var(--line);
}
.rhud-bar[hidden] { display: none; }

.rhud-status {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 210px;
}

.rhud-status-badge {
  align-self: flex-start;
  padding: 2px 7px;
  border-radius: 3px;
  border: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-1);
}
.rhud-status-badge[hidden] { display: none; }

/* Which of the two cells is being built, in the one place a viewer will look for it. */
.rhud-bar.tone-a .rhud-status-badge {
  color: var(--down);
  border-color: color-mix(in srgb, var(--down) 45%, transparent);
}
.rhud-bar.tone-b .rhud-status-badge {
  color: var(--up);
  border-color: color-mix(in srgb, var(--up) 45%, transparent);
}
.rhud-bar.tone-diff .rhud-status-badge {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
}

.rhud-status-line {
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.35;
  color: var(--text-1);
  /* Fixed to two lines. The line is derived from the fold and changes at the rate of the run;
     letting it change the bar's height would resize the scene beneath it. */
  height: 27px;
  overflow: hidden;
}

.rhud-counts {
  display: flex;
  gap: 16px;
}

.rhud-count { text-align: right; }

.rhud-count-value {
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: 500;
  line-height: 1.1;
  color: var(--text-0);
  /* Tabular figures, so a counter climbing through 999 → 1,000 does not shove its label
     sideways sixty times a second. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.rhud-count-label {
  margin-top: 1px;
  font-family: var(--font-mono);
  font-size: 8px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-2);
  white-space: nowrap;
}

.rhud-levels {
  display: flex;
  gap: 14px;
}

.rhud-level { width: 96px; }

.rhud-level-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 4px;
}

.rhud-level-name {
  font-family: var(--font-mono);
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--level-color, var(--text-2));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rhud-level-count {
  font-family: var(--font-mono);
  font-size: 8.5px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.rhud-level-track {
  height: 3px;
  border-radius: 2px;
  background: rgba(180, 197, 224, 0.09);
  overflow: hidden;
}

.rhud-level-fill {
  height: 100%;
  width: 0;
  border-radius: 2px;
  background: var(--level-color, var(--accent));
  /* The director already decays the value smoothly, frame by frame. This is only here to carry
     the step a dropped frame leaves behind, so it is short enough not to lag the run. */
  transition: width 120ms linear;
}

/* ── The evidence rails, over the scene ───────────────────────────────────────────────────
 *
 * Two things stay on the canvas, and only two, because both are about the cell rather than about
 * the run's totals: the fleet, and the findings.
 *
 * The fleet rail is the answer to the question the old overlay could not answer — three level
 * meters cannot distinguish thirty agents from one, and a viewer watching them has to be *told*
 * that a fleet is working. A row per domain, lit while its turn is open, shows it instead.
 *
 * Both are `pointer-events: none`, so orbiting, zooming and clicking a node all pass straight
 * through. The viewer's hand on the cell always wins; nothing here is a control.
 */

.rhud-rails {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 11;
  font-family: var(--font-sans);
}
.rhud-rails[hidden] { display: none; }

/*
 * The scene's own furniture stands down for the length of a replay.
 *
 * The atlas and the write stream both claim the right of the scene, and the atlas is an opaque
 * card — so with both up the stream was drawn straight over a scrolling list of thirty domains and
 * neither could be read. The rails win because they are saying the same thing better: the atlas is
 * an index of what the run modelled, and the rails are that index *arriving*. Its fold stub goes
 * too, since a control for un-hiding a panel that is not hidden is just another thing in a corner.
 *
 * The legend goes for the same reason on the other side: it sits in the left gutter, which is where
 * the fleet is, and two translucent panels in one gutter read as neither. It is a key to a picture
 * the rails are already naming aloud.
 *
 * The scene controls do *not* go. They used to, and that was the bug: the switches for the name
 * plates, the finding halos and the render quality are the only way to change what the cell looks
 * like, and a replay is exactly when somebody is looking hardest at it. Hiding them meant a viewer
 * who found the labels too busy, or the halos distracting, had nowhere to go. So instead of
 * standing down they step out of the fleet's gutter and into the scene, and `syncCellControlsToReplay`
 * folds them to the stub on the way in — present, quiet, one click from open.
 */
#cell-canvas-container.rhud-on .cell-atlas,
#cell-canvas-container.rhud-on .cell-atlas-toggle,
#cell-canvas-container.rhud-on .cell-legend { display: none; }

/* Clear of the fleet rail, tracking the same widths as the stage inset below (rail + the 14px the
   panel keeps from any edge). Both the panel and its stub move, since only one is ever up. */
#cell-canvas-container.rhud-on .cell-controls,
#cell-canvas-container.rhud-on .cell-scene-toggle { left: 194px; }
#cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-controls,
#cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-scene-toggle { left: 350px; }

/*
 * ── The gutters ───────────────────────────────────────────────────────────────────────────
 *
 * For the length of a replay the cell view is three regions, not one picture with three overlays:
 * the fleet down the left, the write stream down the right, the narration along the bottom, and
 * the scene in what is left. The rails are still absolute over the host — that is how they keep
 * their own geometry — but the scene no longer extends underneath them, so there is no width at
 * which text and organelles are drawn through each other.
 *
 * These numbers are the rails' own widths and have to be kept in step with them: `.rhud-agents`
 * (180), `.rhud-writes` (306) and the notes band (62). The classes come from `fitRails`, which
 * mirrors the rails' layout onto the host precisely so this side can read them; the cascade below
 * follows the same order it does — short changes the fleet's shape, narrow drops the stream,
 * tiny drops the narration — and each gutter closes as its rail stands down.
 */
#cell-canvas-container.rhud-on .cell-stage {
  left: 180px;
  right: 306px;
  bottom: 62px;
}
/* Short: the fleet folds into two columns. The narration keeps its height — it is one card tall
   however wide the view is, and a row of three is the one part of this that cannot be folded. */
#cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-stage { left: 336px; }
/* Narrow: no stream, so no right gutter. */
#cell-canvas-container.rhud-on.rhud-narrow .cell-stage { right: 0; }
/* Tiny: no narration band either. */
#cell-canvas-container.rhud-on.rhud-tiny .cell-stage { bottom: 0; }
/* The closing frame has no rails at all, and the container has already been held off the left
   for the verdict column — a second inset on top of that would strand the cell in a corner. */
#view-frame.demo-finale #cell-canvas-container .cell-stage { inset: 0; }
/* — and with no fleet on the left, the controls come back to the corner they live in. */
#view-frame.demo-finale #cell-canvas-container .cell-controls,
#view-frame.demo-finale #cell-canvas-container .cell-scene-toggle { left: 14px; }

/* ── The fleet ── */

.rhud-agents {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 180px;
  padding: 12px 10px 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
  /* A column, not a scrim. It used to fade to transparent on its inner edge because it was lying
     over the scene and had to let the cell through; it has its own gutter now (see `.cell-stage`),
     so the honest thing is a flat surface with a hairline where the scene begins — the eye reads
     a boundary far more easily than it reads a gradient, and the roster stops competing with
     whatever is lit behind it, because there is nothing behind it. */
  background: linear-gradient(to right, rgba(12, 17, 25, 0.96) 0%, rgba(9, 12, 19, 0.90) 100%);
  border-right: 1px solid rgba(148, 176, 214, 0.11);
}

/*
 * The roster, inside the panel.
 *
 * The fade used to be on the panel itself, which faded the surface and the hairline with it: the
 * wall between the roster and the scene stopped two thirds of the way down and the last few agent
 * rows were left standing on the cell. That is the whole of what "the rails overlap the scene"
 * looks like — not a box in the wrong place, a box whose edge dissolves before it gets to the
 * bottom. So the mask belongs to the list, and the panel it sits in is a solid wall, full height,
 * with one hairline down its inner edge from top to bottom.
 */
.rhud-agent-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  mask-image: linear-gradient(to bottom, #000 0, #000 90%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 90%, transparent 100%);
}

.rhud-rail-head {
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: 6px;
}


.rhud-agent-group { margin-bottom: 7px; }

.rhud-agent-group-name {
  font-family: var(--font-mono);
  font-size: 7.5px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--level-color, var(--text-3));
  opacity: 0.75;
  margin-bottom: 2px;
}

.rhud-agent {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 13px;
  font-family: var(--font-mono);
  font-size: 9px;
  line-height: 13px;
  /* Legible at rest, not just when lit: a roster whose idle rows cannot be read is a roster
     nobody can count, and the count is the point. The contrast with `is-working` is carried by
     the level colour and the dot's glow, which is plenty without dimming the text to a hint. */
  color: var(--text-2);
  /* Only the colour moves. Nothing here changes size, so thirty rows switching on and off over
     a round costs no layout at all. */
  transition: color 180ms linear;
}

.rhud-agent-dot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.45;
  transition: opacity 180ms linear, box-shadow 180ms linear;
}

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

.rhud-agent-count {
  flex: none;
  font-size: 8.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}

/* A turn is open. The single most convincing thing on screen: a dozen of these lit at once, in
   three groups, is a fleet working — and no caption has to say so. */
.rhud-agent.is-working {
  color: var(--level-color, var(--accent));
}
.rhud-agent.is-working .rhud-agent-dot {
  opacity: 1;
  box-shadow: 0 0 7px var(--level-color, var(--accent));
}
.rhud-agent.is-working .rhud-agent-count { color: var(--text-1); }

/* ── The write stream ── */

.rhud-writes {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  /* A fixed column, not a fraction of the scene: it is one wall of the cell view's gutter and the
     stage is inset by exactly this much, so a percentage here would make the scene's width depend
     on the scene's width. The point at which 306 stops fitting is handled by `is-narrow`, which
     takes the stream down altogether — and closes the gutter with it — rather than squeezing it. */
  width: 306px;
  padding: 12px 14px 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
  /* The fleet's surface, mirrored, and for the same reason: this is a wall of the stage now, not
     a film over it. The hairline is what tells a viewer the stream is beside the cell rather than
     on it — and, like the fleet's, it runs the full height. It used to stop at `max-height: 62%`
     with a mask taking the top third of the surface away, which left the newest write sitting on
     the scene with nothing behind it and a third of the gutter as bare background above. Two
     walls of the same height either side of the cell is the difference between a frame and three
     overlays that happen not to intersect. */
  background: linear-gradient(to left, rgba(12, 17, 25, 0.96) 0%, rgba(9, 12, 19, 0.90) 100%);
  border-left: 1px solid rgba(148, 176, 214, 0.11);
}

/* The stream fills its panel from the bottom, so the newest write is always on the same line and
   the eye has somewhere fixed to rest. The fade is the list's, not the panel's: it trims the
   oldest row out of frame rather than dissolving the wall. */
.rhud-write-list {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 1px;
  overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 100%);
}

/*
 * Three columns on a common grid, not a right-justified paragraph.
 *
 * These rows used to hang off the right edge, which meant every one of them started at a
 * different x — a domain name is anywhere from six to twenty-one characters, so the left edge
 * ragged by a hundred pixels and the ellipses landed in a different place on every line. Read
 * quickly, as this stream is meant to be, that is noise. Ruled columns turn the same rows into
 * a table: the domain always here, the finding always there, the direction always last.
 */
.rhud-write {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.9);
  /* Opacity only. The rows used to slide in from the right, which at a hundred and twenty a
     second is a column of text permanently in motion — the exact thing that read as blinking. */
  animation: rhud-write-in 240ms ease-out;
}

@keyframes rhud-write-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.rhud-write-domain {
  /* Fixed, because it is a column. Long names are cut at a common edge, which reads as a margin
     rather than as damage — and the fleet rail opposite carries every name in full anyway, so
     nothing is actually lost by clipping one here. */
  flex: none;
  width: 72px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-2);
  font-size: 8.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.rhud-write-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Full-strength ink. These rows are the evidence — the findings themselves, arriving — and at
     `--text-1` over a lit scene they read as a watermark rather than as output. */
  color: var(--text-0);
}

.rhud-write-dir {
  /* The third column. Fixed width so the six direction words share a left edge and the labels
     beside them share a right one; wide enough for `relocated`, the longest of the six. `em` here
     is against this element's own 8.5px, not the parent's. */
  flex: none;
  width: 6em;
  text-align: left;
  font-size: 8.5px;
  color: var(--text-2);
}

/* The same six direction colours the markers and the legend use — a row in this stream and the
   dot that appears in the cell for it are the same finding, and should not be two colours. */
.rhud-write.dir-up .rhud-write-dir { color: #DD8A6C; }
.rhud-write.dir-down .rhud-write-dir { color: #6EA3D4; }
.rhud-write.dir-present .rhud-write-dir { color: #7FB08A; }
.rhud-write.dir-absent .rhud-write-dir { color: #5B6472; }
.rhud-write.dir-altered .rhud-write-dir { color: #C6A96B; }
.rhud-write.dir-relocated .rhud-write-dir { color: #C097B8; }

/* ── The narration notes ──────────────────────────────────────────────────────────────────
 *
 * What the fleet is doing this second, in words. The write stream to the right of it is read as a
 * rate — rows going past too fast to follow, which is the point of it — and the fleet rail to the
 * left is read as a state. Neither says what the *work* is, and the work is most of a run: a
 * search against PubMed, a walk of the shared graph, a question put to another domain.
 *
 * Sits between the two rails along the bottom of the scene, which is the one band of it that
 * neither claims, and is paced by `NOTE_MIN_MS` rather than by the log — see `replay-hud.js`.
 */

/*
 * The narration: the last three notes, side by side along the bottom of the stage.
 *
 * Two earlier versions of this were wrong in opposite directions. It began as a stack of cards
 * floating over the scene's lower left — good to look at, but it was over the cell, and on a
 * laptop it was over the cell *and* within a few pixels of the write stream. Turning it into a
 * gutter fixed the overlap and broke the thing itself: a column of cards needs a hundred and
 * fifty pixels to show three, the scene cannot spare that, and at 84px — 52 on a short view —
 * what the band actually held was one note. A row holds all three in the height of one.
 *
 * So it is a row, oldest to newest, left to right, the two older ones standing down in opacity.
 * It costs the scene sixty-two pixels instead of a hundred and fifty, the cards keep the width
 * they need to be read, and the region still tiles with the two side rails rather than competing
 * with them: the stage stops above it (`.cell-stage`, `bottom: 62px`) and the rails beside it.
 */
.rhud-notes {
  position: absolute;
  left: 180px;
  right: 306px;
  bottom: 0;
  height: 62px;
  padding: 0 16px 9px;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: flex-end;
  gap: 10px;
  overflow: hidden;
  /* The third wall, and the same surface as the other two. The fleet and the stream each draw a
     hairline on the edge they share with the scene; without one here the canvas simply stops
     part-way down the view, which reads as a crop rather than as a frame. The surface matters as
     much as the line: three regions of the same material around three sides of the cell is a
     frame, and the cards sit *in* it — a band of bare background with cards floating on it is
     three more things over the scene. */
  background: linear-gradient(to top, rgba(12, 17, 25, 0.96) 0%, rgba(9, 12, 19, 0.90) 100%);
  border-top: 1px solid rgba(148, 176, 214, 0.11);
}

.rhud-note {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 7px 11px 7px 9px;
  border-radius: 9px;
  border: 1px solid rgba(148, 176, 214, 0.16);
  border-left: 2px solid var(--note-color, var(--accent));
  background: linear-gradient(100deg,
    color-mix(in srgb, var(--note-color, var(--accent)) 13%, rgba(9, 12, 19, 0.86)) 0%,
    rgba(9, 12, 19, 0.84) 46%);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(10px) saturate(1.05);
  -webkit-backdrop-filter: blur(10px) saturate(1.05);
  /* Opacity and a short lift, once, on entry. Nothing here loops: a card that keeps moving in the
     corner of a scene is the thing that made the old overlay unwatchable. */
  animation: rhud-note-in 320ms cubic-bezier(0.2, 0, 0, 1);
  /*
   * Three across a band as wide as the stage. They share it evenly and cannot grow past a
   * readable measure — a single note stretched to eleven hundred pixels is a strip of text with
   * its accent edge a foot away from the words it belongs to. `min-width: 0` because both lines
   * inside ellipsise, and a flex item will not shrink below its content without it.
   */
  flex: 1 1 0;
  min-width: 0;
  max-width: 400px;
}

/* The older notes stand down rather than disappear, so the row reads as a history of the last
   few seconds — left to right, oldest to newest — instead of as three equal claims. */
.rhud-note:first-child { opacity: 0.42; }
.rhud-note:nth-child(2) { opacity: 0.72; }
.rhud-note:last-child { opacity: 1; }

@keyframes rhud-note-in {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}

.rhud-note-kind {
  flex: none;
  margin-top: 1px;
  font-family: var(--font-mono);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--note-color, var(--accent));
  /* Fixed, so the sentences to its right start on a common edge rather than ragging by however
     long the verb happens to be. */
  width: 62px;
  text-align: right;
}

.rhud-note-body { min-width: 0; flex: 1 1 auto; }

.rhud-note-source {
  display: block;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rhud-note-detail {
  display: block;
  font-family: var(--font-mono);
  font-size: 9.5px;
  line-height: 1.4;
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One colour per kind of work, and the same colours the rest of the interface already uses for
   these things: the level palette for building, the accent for retrieval, caution for a
   reconciliation, bright for a milestone the run will not repeat. */
.rhud-note.kind-retrieve { --note-color: #6EA3D4; }
.rhud-note.kind-graph { --note-color: #C097B8; }
.rhud-note.kind-ask { --note-color: #C6A96B; }
.rhud-note.kind-build { --note-color: #7FB08A; }
.rhud-note.kind-propose { --note-color: #8FA8CE; }
.rhud-note.kind-check { --note-color: #D2777A; }
.rhud-note.kind-round { --note-color: #93A7C4; }
.rhud-note.kind-milestone { --note-color: #E4B072; }

/* ── Fitting the three rails into the scene they are drawn on ─────────────────────────────
 *
 * The classes are set from the measured scene box — see `fitRails` in `replay-hud.js` — because
 * what the rails have to fit in is the canvas, and the canvas is what is left after the dock, the
 * gallery and the detail panel have taken theirs. On a 1366×768 laptop that is around three
 * hundred pixels of height: the fleet was being cut off mid-list, and both bottom rails were
 * piling into the same band across the middle of the cell.
 *
 * Each rule below drops or re-flows exactly one thing, at the width or height where it stops
 * fitting. Nothing is scaled: small type over a lit scene is not read, it is squinted at.
 */

/* Short: the fleet reads across two columns instead of running off the bottom of the frame. */
/*
 * Short *and* wide: the roster folds into two columns so all thirty agents fit the height.
 *
 * Not when the view is also narrow. The fold trades width for height, and it is a gutter now —
 * 336px of a thousand-pixel view is a third of the cell given to a roster that reads perfectly
 * well as one column with its tail faded out. Under `is-narrow` the stream has already stood
 * down, so the single column keeps the scene as wide as the view can make it.
 */
.rhud-rails.is-short:not(.is-narrow) .rhud-agents {
  width: 336px;
  padding-right: 14px;
}
/* Only the roster folds. The panel around it keeps its surface and its hairline — a fold is a
   change of column count, not a licence to let the wall fade into the cell. */
.rhud-rails.is-short:not(.is-narrow) .rhud-agent-list {
  columns: 2;
  column-gap: 16px;
}
/* A group split across the fold reads as two fleets. */
.rhud-rails.is-short:not(.is-narrow) .rhud-agent-group { break-inside: avoid; }
/* The band starts after the second column, not after the first. It keeps its height and all
   three of its notes: a row costs the same sixty-two pixels whatever the view is doing. */
.rhud-rails.is-short:not(.is-narrow) .rhud-notes { left: 336px; }

/* Narrow: three columns will not fit, so the write stream stands down. What it says is still on
   screen — the dock's event feed and node stream carry the same writes. */
.rhud-rails.is-narrow .rhud-writes { display: none; }
.rhud-rails.is-narrow .rhud-notes { right: 0; }

/* Tiny: the fleet alone, and a single narrow column of it — a view this small has none of the
   width the folded roster costs, and `is-narrow` has already ruled the fold out. */
.rhud-rails.is-tiny .rhud-notes { display: none; }

/* ── The simulated pointer and the spotlight ──────────────────────────────────────────────
 *
 * A demo in which pages change by themselves is a slideshow. One in which a cursor travels to a
 * tab, presses it, and *then* the page changes is a product being used — which is the difference
 * between showing a viewer a set of screens and showing them software.
 *
 * Fixed over the whole window rather than inside the app, and `pointer-events: none` throughout,
 * so a presenter can reach every real control straight through it and take the wheel mid-demo.
 */

.demo-layer {
  position: fixed;
  inset: 0;
  z-index: 400;
  pointer-events: none;
}
.demo-layer[hidden] { display: none; }

.demo-cursor {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  /* Transform only — the travel runs on the compositor, which matters on a page already
     spending every millisecond it has on a WebGL scene. */
  transition: transform 620ms cubic-bezier(0.33, 0, 0.15, 1);
  will-change: transform;
}
.demo-cursor[hidden] { display: none; }
/* A cursor arriving from off screen must not glide in from a stale position: the travel is the
   thing being shown, and a slide from wherever it last was reads as a drag. */
.demo-cursor.no-travel { transition: none; }

.demo-cursor svg {
  position: absolute;
  top: -3px;
  left: -3px;
  /* Two shadows: a hard dark one so the white arrow keeps an edge over the pale panels, and a
     coloured bloom so it keeps one over the near-black scene. A single drop shadow was legible
     on neither — which is what "the cursor is not visible" meant. */
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.9))
          drop-shadow(0 0 10px color-mix(in srgb, var(--accent) 55%, transparent));
  transition: transform 140ms cubic-bezier(0.2, 0, 0, 1);
  transform-origin: 4px 4px;
}

/*
 * The halo. A soft disc of light travelling under the arrow, which is the thing the eye actually
 * catches on a screen where everything else is also moving: an arrow alone is twenty pixels of
 * white on a scene full of white markers, and a viewer three metres from a projector loses it.
 * `screen` so it lifts whatever it crosses rather than tinting it, and it is the only part of the
 * cursor that can be seen through a panel.
 */
.demo-cursor-halo {
  position: absolute;
  top: 0;
  left: 0;
  width: 92px;
  height: 92px;
  margin: -46px 0 0 -46px;
  border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--accent) 46%, transparent) 0%,
    color-mix(in srgb, var(--accent) 16%, transparent) 42%,
    transparent 70%);
  mix-blend-mode: screen;
  opacity: 0.9;
  animation: demo-cursor-breathe 2.6s ease-in-out infinite;
}

/* The ring. A fixed piece of geometry around the hot point, so the exact pixel being pressed is
   stated rather than implied by the tip of an arrow. */
.demo-cursor-ring {
  position: absolute;
  top: 0;
  left: 0;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--accent) 85%, white);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow:
    0 0 0 1px rgba(4, 7, 12, 0.55),
    0 0 18px color-mix(in srgb, var(--accent) 45%, transparent),
    inset 0 0 12px color-mix(in srgb, var(--accent) 22%, transparent);
  transition: transform 140ms cubic-bezier(0.2, 0, 0, 1),
              background-color 140ms linear;
}

@keyframes demo-cursor-breathe {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  50% { transform: scale(1.16); opacity: 1; }
}

.demo-cursor-ripple {
  position: absolute;
  top: 0;
  left: 0;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--accent) 90%, white);
  opacity: 0;
}
.demo-cursor-ripple.go { animation: demo-ripple 700ms cubic-bezier(0.2, 0, 0, 1); }

@keyframes demo-ripple {
  from { transform: scale(0.62); opacity: 1; }
  to { transform: scale(2.9); opacity: 0; }
}

/*
 * The label. It names the control being pressed, so it is read at a glance from across a room —
 * which the old 9.5px mono chip was not. Accent-keyed and offset clear of the arrow.
 */
.demo-cursor-label {
  position: absolute;
  top: 24px;
  left: 22px;
  padding: 5px 11px 5px 10px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
  background: linear-gradient(180deg,
    rgba(16, 22, 33, 0.96), rgba(9, 13, 20, 0.96));
  box-shadow:
    0 10px 26px rgba(0, 0, 0, 0.55),
    0 0 18px color-mix(in srgb, var(--accent) 22%, transparent);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-0);
  white-space: nowrap;
}
.demo-cursor-label:empty { display: none; }
/* A dot on the leading edge, tying the chip back to the ring it belongs to. */
.demo-cursor-label::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
  vertical-align: 1px;
}

/* The press. The ring collapses onto the point and fills; the arrow dips. Together they read as
   a click landing on something rather than as an image being scaled. */
.demo-cursor.is-pressed svg { transform: scale(0.84); }
.demo-cursor.is-pressed .demo-cursor-ring {
  transform: scale(0.74);
  background: color-mix(in srgb, var(--accent) 34%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .demo-cursor-halo { animation: none; }
}

.demo-spot {
  position: absolute;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
  /* A ring and a wash of light *inside* it, not a dim of everything else: the region being
     pointed at is still running, and darkening the rest of the interface to make the ring
     legible would hide the evidence the narration is describing. */
  background: var(--accent-faint);
  box-shadow: 0 0 0 1px rgba(8, 11, 17, 0.5), 0 0 26px color-mix(in srgb, var(--accent) 26%, transparent);
  transition: left 420ms cubic-bezier(0.33, 0, 0.15, 1),
              top 420ms cubic-bezier(0.33, 0, 0.15, 1),
              width 420ms cubic-bezier(0.33, 0, 0.15, 1),
              height 420ms cubic-bezier(0.33, 0, 0.15, 1);
}
.demo-spot[hidden] { display: none; }
.demo-spot.no-travel { transition: none; }

.demo-spot-label {
  position: absolute;
  left: 0;
  bottom: calc(100% + 6px);
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-ink);
  white-space: nowrap;
}
.demo-spot-label[hidden] { display: none; }

/* Set by `placeSpotLabel` when the ring is against the top or the right of the workspace and the
   caption's default corner would be off it. See the note there. */
.demo-spot-label.is-inside { bottom: auto; top: 6px; left: 6px; }
.demo-spot-label.is-flipped { left: auto; right: 0; }
.demo-spot-label.is-inside.is-flipped { right: 6px; }

/* ── The closing shot ─────────────────────────────────────────────────────────────────────
 *
 * The verdicts, read over the cell they were derived from. The scene keeps drawing full-bleed
 * behind the panel and the panel goes translucent over it — the one arrangement in the product
 * where the molecular model and the answer it produced are on screen together, which is exactly
 * the note a demo should end on.
 */

/*
 * The run gallery stands down for the whole replay, not just for the closing frame.
 *
 * It costs 240px of the workspace, it is a list nobody is reading while a run plays — nothing in
 * the script ever points at it, every simulated click lands in the control bar — and those 240px
 * are the difference between a cell view that has room for its gutters and one that does not. On
 * the closing frame it goes for the older reason as well: a list down the left edge is the only
 * thing left on screen that still looks like a page rather than a shot.
 */
#main-area.demo-live #run-gallery,
#main-area.demo-finale #run-gallery { display: none; }

#view-frame.demo-finale #cell-canvas-container {
  position: absolute;
  inset: 0;
  /* Held off the left, which is where the verdict column sits. The camera re-centres and pulls
     back at the same moment (`demoLevers.setFinale`), so the cell sits whole in what is left
     rather than half behind the text. */
  left: 41%;
  z-index: 0;
}

/* The rails go with the run. On the closing frame there is nothing left working, and a roster of
   thirty idle agents smeared under a blur is texture rather than evidence. */
#view-frame.demo-finale .rhud-rails { display: none; }

/*
 * The veil, and it is deliberately thin.
 *
 * The first version was 0.90 → 0.66 over an opaque `--glass-bg` card, which is to say: a cell
 * rendering at full cost behind something nobody could see through. Both layers had to give. The
 * veil drops to a third at the top, and the verdict's own sections go translucent with a blur
 * behind them — so the text sits on frosted glass and the cell reads as the light source under it,
 * which is the whole point of ending here.
 */
#view-frame.demo-finale #panel-container {
  position: relative;
  z-index: 2;
  /* No veil across the container: it spans the full workspace, and a wash over the whole of it
     dimmed the cell as much as the text it was meant to seat. The frosting below is per section,
     so it covers the column and stops where the column stops. */
  background: none;
}

#view-frame.demo-finale #panel-container .console-panel,
#view-frame.demo-finale #panel-container .stat-card,
#view-frame.demo-finale #panel-container .verdict-card {
  background: rgba(9, 12, 19, 0.56);
  border-color: rgba(148, 176, 214, 0.20);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.42);
}

/*
 * The verdict colour survives the frosting.
 *
 * The rule above flattens every border to one grey and every ground to one wash, which is right
 * for the panels and wrong for the two things on this page whose whole job is to say which way
 * the run went. Both keep their tint over the cell; only the opacity changes.
 */
#view-frame.demo-finale #panel-container .verdict-card {
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--vc) 15%, rgba(9, 12, 19, 0.56)) 0%,
      rgba(9, 12, 19, 0.56) 62%);
  border-left-color: color-mix(in srgb, var(--vc) 80%, transparent);
}

#view-frame.demo-finale #panel-container .verdict-headline {
  background:
    linear-gradient(104deg,
      color-mix(in srgb, var(--vc) 22%, rgba(9, 12, 19, 0.58)) 0%,
      rgba(9, 12, 19, 0.58) 66%);
  border-color: color-mix(in srgb, var(--vc) 28%, rgba(148, 176, 214, 0.20));
  border-left-color: var(--vc);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.42);
}

/*
 * The verdict comes off full width for the closing frame.
 *
 * Translucency alone was not enough: the verdict fills the workspace, so a frosted panel from edge
 * to edge is still a wall — the cell was a rumour of light behind text. Held to a column on the
 * left, the answer and the model that produced it are side by side, which is the shot.
 */
#view-frame.demo-finale #panel-container > * {
  max-width: 820px;
  margin-right: auto;
}

/*
 * ── Pages arriving ────────────────────────────────────────────────────────────────────────
 *
 * A replay changes view about a dozen times in two minutes, and each change was an instantaneous
 * swap: the cursor pressed a tab and the next screen was simply *there*, fully formed, in the
 * same frame. That is the single thing that most made a two-minute run of real software read as a
 * deck of slides — not the pace of it, the joins. A short rise-and-fade costs nothing, hides the
 * frame in which a table is laid out, and is the difference between a screen being shown to
 * someone and a screen being opened by someone.
 *
 * Only while a replay is on screen (`demo-live`, set in `showHud`). Someone driving the app
 * themselves is navigating on their own clock and does not need their own clicks explained.
 */
@keyframes demo-page-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to { opacity: 1; transform: none; }
}
@keyframes demo-scene-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

#view-frame.demo-live #panel-container > * {
  animation: demo-page-in 340ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

/* The scene comes back by fading, not by rising: it is a continuous thing that was there before
   the page covered it, and sliding it would say otherwise. */
#view-frame.demo-live #cell-canvas-container.visible {
  animation: demo-scene-in 420ms ease-out;
}

/* The card's text is replaced in place sixteen times; the element itself never changes, so the
   animation is re-armed from `paintChapter` rather than triggered by the swap. */
.demo-chapter-text.is-in {
  animation: demo-page-in 360ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  #view-frame.demo-live #panel-container > *,
  #view-frame.demo-live #cell-canvas-container.visible,
  .demo-chapter-text.is-in { animation: none; }
  .rhud-write { animation: none; }
  .rhud-level-fill,
  .demo-chapter-progress-fill,
  .demo-cursor,
  .demo-spot { transition: none; }
  .demo-cursor-ripple.go { animation: none; }
}

/* Narrow viewports: the chapter card's argument is what has to survive. The stats column and the
   fleet rail are the first to go — the rail is a two-hundred-pixel bite out of a cell that no
   longer has two hundred pixels to give. */
/* The three level meters are the first thing to go, and they go early: the fleet rail over the
   scene already says which levels are working, in more detail and more convincingly, so on any
   screen narrower than a large desktop the 316px they cost is better spent on the narration. */
@media (max-width: 1699px) {
  .rhud-levels { display: none; }
}

@media (max-width: 1200px) {
  .demo-chapter-stats { display: none; }
}

@media (max-width: 900px) {
  .rhud-agents { display: none; }
  .rhud-writes { display: none; }
  .rhud-notes { display: none; }
  /* No rails, no gutters: the scene takes the whole box back. Written against the heaviest of
     the gutter selectors as well as the base one, because a media query adds no weight of its own
     and `rhud-short:not(.rhud-narrow)` would otherwise still be holding 336px on the left. */
  #cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-stage,
  #cell-canvas-container.rhud-on .cell-stage { inset: 0; }
  /* The scene controls were only held off the left to clear the fleet, which has just gone. */
  #cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-controls,
  #cell-canvas-container.rhud-on.rhud-short:not(.rhud-narrow) .cell-scene-toggle,
  #cell-canvas-container.rhud-on .cell-controls,
  #cell-canvas-container.rhud-on .cell-scene-toggle { left: 14px; }
  .rhud-counts { gap: 12px; }
  .demo-chapter-body { -webkit-line-clamp: 1; }
}

/* A domain that has written something, but has nothing open right now. Distinguishing it from
   a domain that has not yet been reached is what makes the board show the roster filling in —
   thirty agents coming online over the first round, rather than a grid that is blank until it
   flickers. */
.agent-card.agent-reported .agent-name { color: var(--text-0); }
.agent-card.agent-reported { background: rgba(180, 197, 224, 0.04); }

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   The activity dock, made operable
   ═══════════════════════════════════════════════════════════════════════════════════════════

   The board, the feed and the stage bar were three lists of text describing a run nobody could
   reach from them: the only way to see what an agent had written was to leave the dock, guess
   which view held it, and filter by hand. They are buttons now, and these rules are what make
   them look like it — appended at the end of the file because `.agent-card` and `.ticker-item`
   each have two earlier declarations and the later of those would otherwise win. */

.agent-card {
  width: 100%;
  font-family: inherit;
  text-align: left;
  border: 0;
  border-left: 2px solid transparent;
  background: transparent;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition);
}
/* An agent that has written nothing has nothing to show, and a card that narrowed every view to
   an empty one would be a control that only ever reports a fault. Left visible — the roster
   filling in is the board's subject — but inert, and saying so. */
.agent-card:disabled { cursor: default; }
.agent-card:not(:disabled):hover { background: rgba(180, 197, 224, 0.09); }
.agent-card:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
}

/* Pressed: this is the one agent every view is showing. Held in the accent rather than in the
   activity tint, because "active" already means "taking a turn right now" two rules up, and the
   two states are independent — a focused agent is usually one that finished long ago. */
.agent-card.agent-focused {
  background: var(--accent-dim);
  border-left-color: var(--accent);
}
.agent-card.agent-focused .agent-name { color: var(--accent-hover); }

.ticker-item {
  width: 100%;
  font-family: inherit;
  font-size: 11px;
  text-align: left;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 3px;
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
  transition: background var(--transition), opacity var(--transition);
}
.ticker-item:hover { background: rgba(180, 197, 224, 0.08); }
.ticker-item:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
/* An agent is singled out and this row is another agent's. Dimmed rather than removed: the feed
   is a chronology, and deleting the rows between the ones that match would make the survivors
   look consecutive. */
.ticker-item.is-muted { opacity: 0.26; }

.stage-segment {
  font-family: var(--font-mono);
  border: 0;
  background: transparent;
  cursor: pointer;
}
.stage-segment:not(:disabled):hover { background: rgba(180, 197, 224, 0.08); }
.stage-segment:focus-visible { outline: 1px solid var(--accent); outline-offset: -2px; }
/* No mark to jump to — a run with no replay loaded, or a stage this run skipped. The bar is
   still the run's progress track, so it stays legible; it simply does not respond. */
.stage-segment:disabled { cursor: default; }

/* The row a feed click landed on, among fifty that look the same. Persistent rather than a
   flash: the reader is reading around it, and a highlight that has already faded by the time
   the eye arrives has marked nothing. */
.lg-row.lg-arrived {
  background: var(--accent-faint);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* ── Reset ──
   One control per page that puts the picture back. Quiet, because it is the way out of a state
   the reader chose and not an action on the run; last in its container, under everything it
   undoes. */
.graph-reset {
  align-self: stretch;
  min-height: 32px;
}
.cell-reset {
  margin-top: 4px;
  padding: 5px 11px;
  height: auto;
  font-size: 12px;
}

/* ── The demo's way out ──
   The control existed and was the hardest thing in the row to find: `--text-3` on a transparent
   ground, in the gap beside a speed button of the same size. Raised to the same weight as the
   controls either side of it and fenced off from them, so the last thing in the row reads as a
   different kind of thing. The separator is drawn in the gap by a pseudo-element, which costs no
   layout width — the row's budget is documented above and has none to give. */
#demo-bar #replay-exit {
  position: relative;
  color: var(--text-1);
  border-color: color-mix(in srgb, #D2777A 26%, transparent);
}
#demo-bar #replay-exit::before {
  content: '';
  position: absolute;
  left: -7px;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--edge);
}
#demo-bar #replay-exit[data-idle="1"]::before { display: none; }
