/* Reactor DevTools — dark theme.
 *
 * S1: layered-DAG render of the topology + an ordered receipt timeline + a
 * scrubber (play/pause/step/jump) that marks which node each receipt hit. The
 * S2 animation hooks (flash / dim-pulse / red / edge-light / sparkline) layer
 * on top of these same classes — colors are defined here as CSS variables so
 * S2 only adds keyframes + a few transient classes. */

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-500.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-700.woff2") format("woff2");
}

:root {
  /* ── Machine surface (OpenProse Run · DESIGN.md §2 machine sub-palette) ──
   * The devtools is the sanctioned dark "Reactor monitor". Flat charcoal,
   * no gradients, single-voice JetBrains Mono. Var NAMES are unchanged from
   * the original dark theme so app.js + every selector keep working. */

  --bg:        #1a1c22;            /* machine — FLAT page ground */
  --bg-grad:   none;              /* design: no gradients (was a radial) */
  --panel:     #23262e;            /* machine-bar — chrome bars (scrubber) */
  --panel-2:   #1e2026;            /* sidebar/panel body, one hair off machine */
  --ink:       #e8e5de;            /* machine-fg — body text */
  --ink-dim:   #9aa1ac;            /* machine-dim — labels / eyebrows */
  --ink-faint: rgba(154,161,172,0.5);  /* faint marginalia (machine-dim @50%) */
  --line:      rgba(255,255,255,0.09); /* machine hairline (~#2b2f37), 1px */
  --line-soft: rgba(255,255,255,0.05); /* fainter inner rules */

  /* disposition colors (shared by graph + timeline). All verified on #1a1c22:
   *   machine-ok  #5bbf7a ≈7.4:1 · machine-gold #caa96b ≈7.6:1
   *   machine-dim #9aa1ac ≈6.5:1 · violet #b39ddb ≈7.0:1 · red #e0635a ≈4.7:1 */
  --rendered:      #5bbf7a;   /* input-wake render = machine-ok GREEN */
  --rendered-deep: #4a9e64;   /* deeper green for fills/underlays */
  --skipped:       #9aa1ac;   /* machine-dim GREY — "correctly did nothing" */
  --failed:        #e0635a;   /* machine red (deeper than design #f87171 for AA) */
  --recovered:     #5bbf7a;   /* recovery resolves to machine-ok GREEN */

  /* The "lit / freshly-rendered" + active-propagation accent. On the machine
   * surface the ACTIVE thing traveling the dark graph (lit path, edge beads,
   * the spend) is machine-gold — the prompt/keyword/spend hue. */
  --lit:      #caa96b;        /* machine-gold — lit path + render head */
  --lit-soft: #d8be8c;        /* a lighter gold for path-node floor / dash */

  /* wake-cause accents (the render-flash hue encodes WHY a node woke).
   * input → green (default render), external → gold, self → labeled violet. */
  --cause-input:    #5bbf7a;  /* machine-ok GREEN */
  --cause-self:     #b39ddb;  /* labeled VIOLET — distinct self-tick */
  --cause-external: #caa96b;  /* machine-gold */

  --entry:       #caa96b;     /* entry/gateway node = machine-gold border */
  --edge:        rgba(255,255,255,0.10);  /* resting dark-lane edge */
  --edge-atomic: rgba(255,255,255,0.16);  /* @atomic lane, a touch brighter */
  --node-fill:   #23262e;     /* machine-bar — resting node box */
  --node-stroke: rgba(255,255,255,0.14);  /* resting node border */
  --fresh:       #caa96b;     /* cost meter: fresh tokens = machine-gold (spend) */
  --reused:      #5bbf7a;     /* cost meter: reused = machine-ok green (used @ ~45%) */
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg);
  color: var(--ink);
  font: 12.5px/1.5 "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  display: grid;
  grid-template-rows: auto 1fr auto;
  height: 100vh;
  overflow: hidden;
}

/* ---- topbar ---- */
#topbar {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);        /* was a linear-gradient — now flat machine-bar */
}
/* three faint window-chrome dots (mirrors RunTerminal.tsx: white/15, 10px) */
.windots { display: inline-flex; align-items: center; gap: 7px; }
.windots i {
  width: 10px; height: 10px; border-radius: 50%;
  background: rgba(255,255,255,0.15);
  display: inline-block;
}
/* wordmark: machine-dim, mono, the single gold accent on "reactor" */
.brand { font-weight: 500; letter-spacing: 0.08em; font-size: 13px; color: var(--ink-dim); text-transform: lowercase; }
.brand .brand-accent { color: var(--lit); }   /* gold "reactor" — the one accent */
.brand-dim { color: var(--ink-dim); }
.legend { display: flex; gap: 14px; color: var(--ink-dim); font-size: 11px; }
.lg { display: inline-flex; align-items: center; gap: 5px; }
.lg i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.lg-rendered i { background: var(--rendered); box-shadow: 0 0 4px var(--lit); }
.lg-skipped i { background: var(--skipped); }
.lg-failed i { background: var(--failed); }
.lg-entry i { background: transparent; border: 1.5px solid var(--entry); border-radius: 50%; }
.status { margin-left: auto; color: var(--ink-dim); font-size: 11px; }

/* ---- stage: graph + sidebar ---- */
#stage {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 320px;
  min-height: 0;
  overflow: hidden;
}
#graphwrap {
  position: relative;
  min-width: 0;
  overflow: hidden;
  padding: 10px 14px;
}
/* The SVG fits the whole DAG into the viewport (viewBox + meet). Node geometry
 * is tuned so that at this fit the labels stay legible; deeper/denser graphs
 * are handled by S5's clustering (out of scope here). */
#graph { display: block; width: 100%; height: 100%; }
.graphhint {
  position: absolute;
  left: 12px;
  bottom: 10px;
  color: var(--ink-faint);
  font-size: 10.5px;
  pointer-events: none;
}

/* The "siblings stayed dark" hero caption — a data-driven annotation that fades
 * in on a selective-wake frame (most fan-out subscribers stayed dark). Sits
 * top-center over the graph; subtle, legible, very Twitter-readable. */
.herocaption {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translate(-50%, -6px);
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: 0 4px 6px -1px rgba(0,0,0,.45), 0 2px 4px -2px rgba(0,0,0,.45);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.28s ease, transform 0.28s ease;
  white-space: nowrap;
}
.herocaption::before {
  content: "";
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--rendered);
  box-shadow: 0 0 4px var(--rendered);
  margin-right: 8px;
  vertical-align: middle;
}
.herocaption.show { opacity: 1; transform: translate(-50%, 0); }

/* ---- graph elements ---- */
/* Base (resting / dark-lane) edges read GENUINELY DARK so an unlit lane recedes
 * into the page — the whole pitch is "the lit path lights, the sibling lanes stay
 * dark". A connector you can barely see when dark is correct: the contrast lands
 * the instant a path-lit edge blazes orange over it. */
.edge {
  fill: none;
  stroke: var(--edge);
  stroke-width: 1.1;
  opacity: 0.32;
}
.edge.facet-atomic { stroke: var(--edge-atomic); stroke-dasharray: none; }
.edge:not(.facet-atomic) { stroke-dasharray: 3 3; }    /* named facet lanes are dashed */

/* THE LIT PATH (review #1/#2, the keystone steady state). An edge on the active
 * propagation drain holds a bright hot-orange connector with a glow — so a PARKED
 * keyframe shows the whole path lit at once (Claude Adapter → Session Ledger →
 * Session Summary → Workstream Index → Dashboard), while the five sibling adapter
 * lanes keep the dark base edge above. This is what turns "boxes change color"
 * into "a path lights up." Steady (no animation) so the still captures it. */
.edge.path-lit {
  stroke: var(--lit);
  stroke-width: 2.6;
  opacity: 1;
  stroke-dasharray: none;
  filter: drop-shadow(0 0 5px rgba(202, 169, 107, 0.85)) drop-shadow(0 0 2px rgba(202, 169, 107, 0.9));
}
.edge-label {
  fill: var(--ink-faint);
  font-size: 9px;
  letter-spacing: 0.02em;
}

.node-box {
  fill: var(--node-fill);
  stroke: var(--line);
  stroke-width: 1.4;
  rx: 7;
  transition: stroke 0.18s ease, fill 0.18s ease;
}
.node.entry .node-box { stroke: var(--entry); }
.node-label {
  fill: var(--ink);
  font-size: 18px;
  font-weight: 500;
  dominant-baseline: middle;
}
/* friendly labels ("Claude Adapter") are proportional and a touch smaller so
 * longer relatable names fit the box without crushing letter-spacing. */
.node-label.labeled {
  font-size: 14.5px;
  font-weight: 600;
  font-family: "JetBrains Mono", ui-monospace, monospace;   /* was system sans */
  letter-spacing: 0.01em;
}
.node-sub {
  fill: var(--ink-dim);
  font-size: 12px;
  letter-spacing: 0.04em;
  dominant-baseline: middle;
}
.node.entry .node-label { fill: #ffe2a8; }
.node.producer-only .node-box { stroke-dasharray: 4 3; opacity: 0.85; }

/* current-hit marker (S1 scrubber): the node the active receipt landed on. A
 * rendered head is the bright lit orange (the `.rendered-now`/`.recovered-now`
 * classes above carry the saturated glow); a skipped head is grey; a failed head
 * is red. */
.node.hit .node-box { stroke: var(--lit); stroke-width: 2.4; fill: #2b2f37; }
.node.hit.skipped .node-box { stroke: var(--skipped); fill: #1e2026; filter: none; }
/* The FAILED head must POP as hard as the orange wake (review #3): a saturated
 * red FILL + a double glow, not a thin outline. A parked still on the fail beat
 * reads "this node failed" instantly at Twitter scale, and the lit-path overlay
 * is suppressed (nothing propagated) so it sits alone, red, on a dark graph. */
.node.hit.failed .node-box {
  stroke: var(--failed);
  stroke-width: 3.2;
  fill: #2e1a1d;
  filter: drop-shadow(0 0 6px rgba(255, 93, 108, 0.75)) drop-shadow(0 0 4px rgba(255, 93, 108, 0.9));
}
.node.hit .node-label { fill: #ffe8d4; }
.node.hit.skipped .node-label { fill: var(--ink-dim); }
.node.hit.failed .node-label { fill: #ffd2d7; }

/* Nodes that haven't fired yet read as GENUINELY DARK so the dark lane is stark
 * on video: when you touch one Claude session and only that path lights, the
 * five sibling adapters that stayed dark must visibly recede. The box fill drops
 * toward the page background and the stroke + text go faint. (The transition
 * makes a node "come alive" smoothly the first time a receipt hits it.) */
.node.untouched .node-box {
  opacity: 0.26;
  fill: #15171c;
  stroke: var(--line-soft);
}
.node.untouched .node-label, .node.untouched .node-sub { opacity: 0.24; }

/* A node that has rendered AT OR BEFORE the scrub head keeps a warm "lit / alive"
 * floor so the lit-vs-dark delta is obvious in a STILL even after the pulse has
 * decayed (review #1/#3). The freshly-rendered node at the head goes brighter
 * still via `.rendered-now` (set by the scrubber, idempotent — survives a park /
 * deep-link, so the keyframe PNG is shot at full brightness, not mid-decay). */
.node.lit-floor .node-box {
  stroke: #3a4252;
  fill: #1e2026;
}
.node.lit-floor .node-label { fill: #aab4c4; }
.node.lit-floor .node-sub { fill: var(--ink-dim); }

/* A node on the ACTIVE LIT PATH (review #1/#2): warmer than the cool lit-floor so
 * the whole propagation chain reads as one continuous lit run, but a notch below
 * the blazing `.rendered-now` head so the freshly-rendered node still leads the
 * eye. Steady state — held on a parked still. */
.node.path-node .node-box {
  stroke: var(--lit-soft);
  stroke-width: 1.8;
  fill: #2b2f37;
  filter: drop-shadow(0 0 4px rgba(202, 169, 107, 0.32));
}
.node.path-node .node-label { fill: #ffd9b8; }
.node.path-node .node-sub { fill: #b48a63; }
/* the head node's own `.rendered-now` / `.recovered-now` glow wins over path-node */
.node.path-node.rendered-now .node-box,
.node.path-node.recovered-now .node-box { fill: #2b2f37; }

/* THE freshly-rendered node at the head — bright saturated orange border + a
 * subtle warm fill glow, matching the "rendered" legend swatch. This is the
 * steady-state "lit" treatment the still is captured on; it reads as hot even
 * with the caption hidden and the motion frozen. Skipped/failed heads keep their
 * own disposition color (handled by `.hit.skipped` / `.hit.failed` below). */
.node.rendered-now .node-box {
  stroke: var(--lit);
  stroke-width: 2.6;
  fill: #2b2f37;
  filter: drop-shadow(0 0 6px rgba(202, 169, 107, 0.55)) drop-shadow(0 0 3px rgba(202, 169, 107, 0.7));
}
.node.rendered-now .node-label { fill: #ffd9b8; }
.node.rendered-now.cause-self .node-box {
  stroke: var(--cause-self);
  fill: #262433;
  filter: drop-shadow(0 0 6px rgba(179, 157, 219, 0.5)) drop-shadow(0 0 3px rgba(179, 157, 219, 0.7));
}
.node.rendered-now.cause-self .node-label { fill: #e6d6ff; }
.node.recovered-now .node-box {
  stroke: var(--recovered);
  stroke-width: 2.6;
  fill: #1c2a22;
  filter: drop-shadow(0 0 6px rgba(91, 191, 122, 0.5)) drop-shadow(0 0 3px rgba(91, 191, 122, 0.7));
}
.node.recovered-now .node-label { fill: #c8f7da; }

/* The held SELF-tick treatment (review #8): a parked still on a self-sourced skip
 * (the audit floor on a quiet world) shows a dim VIOLET self-pulse on the canvas
 * — a lone glow that lights NO edges, distinct from the bright orange render and
 * from a grey memo-skip. */
.node.self-now .node-box {
  stroke: var(--cause-self);
  stroke-width: 2.4;
  fill: #262433;
  filter: drop-shadow(0 0 6px rgba(179, 157, 219, 0.6)) drop-shadow(0 0 4px rgba(179, 157, 219, 0.55));
}
.node.self-now .node-label { fill: #e8ddff; }
.node.self-now .node-halo {
  stroke: var(--cause-self);
  opacity: 0.6;
  stroke-width: 2.2;
  stroke-dasharray: 4 4;
}

/* =====================================================================
 * S2 — the hero shot: flashes, skips, red, edge-lights, meter sparkline.
 * Every animation is TRANSIENT (fire-and-forget): a `step` fires the pulse
 * by adding a class on the next animation frame, and the class self-removes
 * on `animationend`. State (`.hit`/`.untouched`/`.current`) stays idempotent
 * for scrub-safety; only these decaying pulses are the motion.
 * ===================================================================== */

/* The bright "highlight update" box — a node RENDERED with a moved fingerprint.
 * A halo rect (`.node-halo`) sits under the box and pulses outward; the box
 * stroke flares to the disposition color. Hue is set per-wake-cause via a
 * --flash custom property on the <g> (input cyan / self violet / external gold). */
.node-halo {
  fill: none;
  stroke: var(--flash, var(--rendered));
  stroke-width: 2;
  opacity: 0;
  rx: 12;
  pointer-events: none;
}
/* The flash hue is the hot "lit" orange by default (matches the rendered legend
 * swatch + the steady-state `.rendered-now`); a SELF-sourced wake flashes violet
 * so the self-tick reads as its own distinct event. */
.node.flash { --flash: var(--lit); }
.node.flash.cause-input { --flash: var(--lit); }
.node.flash.cause-self { --flash: var(--cause-self); }
.node.flash.cause-external { --flash: var(--lit-soft); }

.node.flash .node-halo { animation: nodeFlashHalo var(--pulse, 700ms) cubic-bezier(0.16, 0.84, 0.44, 1) forwards; }
.node.flash .node-box  { animation: nodeFlashBox var(--pulse, 700ms) ease-out forwards; }
.node.flash .node-label { animation: nodeFlashLabel var(--pulse, 700ms) ease-out forwards; }

@keyframes nodeFlashHalo {
  0%   { opacity: 0.95; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1.14); }
}
@keyframes nodeFlashBox {
  0%   { stroke: var(--flash); stroke-width: 4; filter: drop-shadow(0 0 6px var(--flash)) drop-shadow(0 0 5px var(--flash)); fill: #2b2f37; }
  60%  { stroke: var(--flash); stroke-width: 2.8; filter: drop-shadow(0 0 5px var(--flash)); fill: #2b2f37; }
  /* decays to the cool lit-floor steady state, NOT all the way back to dark — so
   * the node stays visibly "alive" between pulses (review #1/#3) while the FRESH
   * head node (`.rendered-now`) stays blazing gold so the hero contrast holds. */
  100% { stroke: #3a4252; stroke-width: 1.6; filter: none; fill: #1e2026; }
}
@keyframes nodeFlashLabel {
  0%   { fill: #f3fbff; }
  100% { fill: var(--ink); }
}
/* the halo scales about the node's own center, not the SVG origin */
.node-halo { transform-box: fill-box; transform-origin: center; }

/* The dim grey ripple — a node was woken but memo-SKIPPED ("correctly did
 * nothing"). A faint grey halo breathes once; no stroke flare, no glow. This is
 * the shot React DevTools can't take. */
.node.skip-pulse .node-halo { stroke: var(--skipped); animation: nodeSkipHalo var(--pulse, 700ms) ease-out forwards; }
@keyframes nodeSkipHalo {
  0%   { opacity: 0.18; transform: scale(0.99); }
  100% { opacity: 0;    transform: scale(1.05); }
}

/* The SELF-tick pulse (review #8): a lone violet self-pulse on the audit-floor
 * node. It lights NO edges and costs nothing, but unlike a grey memo-skip it
 * glows its own violet so "the reactor audited itself on a quiet world" reads on
 * the CANVAS, not only in the receipts panel. Brighter + a faint box tint so the
 * still captures it. */
.node.self-pulse { --flash: var(--cause-self); }
.node.self-pulse .node-halo { stroke: var(--cause-self); animation: nodeSelfHalo var(--pulse, 700ms) cubic-bezier(0.16, 0.84, 0.44, 1) forwards; }
.node.self-pulse .node-box  { animation: nodeSelfBox var(--pulse, 700ms) ease-out forwards; }
@keyframes nodeSelfHalo {
  0%   { opacity: 0.85; transform: scale(0.98); }
  45%  { opacity: 0.6;  transform: scale(1.06); }
  100% { opacity: 0;    transform: scale(1.12); }
}
@keyframes nodeSelfBox {
  0%   { stroke: var(--cause-self); stroke-width: 2.6; filter: drop-shadow(0 0 6px var(--cause-self)); fill: #262433; }
  100% { stroke: var(--node-stroke); stroke-width: 1.4; filter: none; }
}

/* The red fail flare — a node FAILED; prior truth stands, no edges light. */
.node.fail-pulse { --flash: var(--failed); }
.node.fail-pulse .node-halo { stroke: var(--failed); animation: nodeFailHalo var(--pulse, 700ms) ease-out forwards; }
.node.fail-pulse .node-box  { animation: nodeFailBox var(--pulse, 700ms) ease-out forwards; }
@keyframes nodeFailHalo {
  0%   { opacity: 0.95; transform: scale(1); }
  35%  { opacity: 0.8;  transform: scale(1.05); }
  100% { opacity: 0;    transform: scale(1.12); }
}
@keyframes nodeFailBox {
  0%   { stroke: var(--failed); stroke-width: 4; filter: drop-shadow(0 0 6px var(--failed)) drop-shadow(0 0 6px var(--failed)); fill: #2e1a1d; }
  60%  { stroke: var(--failed); stroke-width: 3.2; filter: drop-shadow(0 0 5px var(--failed)); fill: #2e1a1d; }
  /* decays to the held red fail state (the `.hit.failed` steady style), so the
   * red stays POPPED on a parked still instead of fading back to dark. */
  100% { stroke: var(--failed); stroke-width: 3.2; filter: drop-shadow(0 0 6px rgba(255,93,108,0.75)); fill: #2e1a1d; }
}

/* Per-facet EDGE LIGHT — a receipt propagated producer→subscriber on a moved
 * facet. The lane brightens to the facet color and a dash chases along it
 * (producer→subscriber direction), plus a token bead travels the path. Only the
 * moved facet's lanes light; a hiring-only subscriber stays dark. */
.edge.lit {
  stroke: var(--flow, var(--rendered));
  stroke-width: 2.2;
  opacity: 1;
  filter: drop-shadow(0 0 4px var(--flow, var(--rendered)));
  animation: edgeLit var(--pulse, 700ms) ease-out forwards;
}
.edge.lit.facet-atomic { --flow: var(--lit); }
.edge.lit:not(.facet-atomic) { --flow: var(--lit-soft); }
@keyframes edgeLit {
  0%   { opacity: 1;    stroke-width: 2.6; }
  70%  { opacity: 0.9;  stroke-width: 2; }
  100% { opacity: 0.55; stroke-width: 1.2; filter: none; }
}
/* the chasing dash overlay drawn over a lit lane */
.flow-dash.facet-atomic { --flow: var(--lit); }
.flow-dash:not(.facet-atomic) { --flow: var(--lit-soft); }
.flow-bead { --flow: var(--lit); }
.flow-dash {
  fill: none;
  stroke: var(--flow, var(--rendered));
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 7 220;
  filter: drop-shadow(0 0 5px var(--flow, var(--rendered)));
  pointer-events: none;
  animation: flowDash var(--pulse, 700ms) cubic-bezier(0.4, 0, 0.5, 1) forwards;
}
@keyframes flowDash {
  0%   { stroke-dashoffset: 227; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}
/* a glowing token bead that rides the lane */
.flow-bead {
  fill: var(--flow, var(--rendered));
  filter: drop-shadow(0 0 5px var(--flow, var(--rendered)));
  pointer-events: none;
}

/* a freshly woken subscriber gets a convergent ring that CONTRACTS inward (the
 * diamond single-wake lands here — woken once even via multiple lanes). The
 * inward collapse reads as "the fan-in converged here, once" — distinct from the
 * outward bloom of a render flash. Brighter so the still can capture it. */
.node.woken .node-halo { stroke: var(--lit); animation: nodeWokenHalo var(--pulse, 700ms) cubic-bezier(0.3, 0, 0.2, 1) forwards; }
@keyframes nodeWokenHalo {
  0%   { opacity: 0; transform: scale(1.22); }
  35%  { opacity: 0.9; transform: scale(1.06); }
  100% { opacity: 0; transform: scale(0.97); }
}
/* When the diamond convergence node is the head and parked, give it a held
 * "woken once" ring so the still reads the single-wake. */
.node.woke-once .node-halo {
  stroke: var(--lit);
  opacity: 0.7;
  stroke-width: 2.4;
  stroke-dasharray: 5 4;
}

/* ---- sidebar ---- */
#sidebar {
  border-left: 1px solid var(--line);
  background: var(--panel-2);
  display: grid;
  grid-template-rows: auto 1fr;
  min-height: 0;
}
.panel { padding: 12px 14px; min-height: 0; }
.panel + .panel { border-top: 1px solid var(--line); }
.panel-title {
  margin: 0 0 10px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.dim { color: var(--ink-faint); font-weight: 400; }

/* ---- cost meter (S2: sparkline over the per-frame series + live totals) ---- */
.meterbody { display: grid; gap: 9px; }

/* The cost meter is the VISUAL HERO. When the expensive node finally wakes
 * (beat 8: the single tall fresh spike), the whole meter panel pulses — the big
 * "fresh tokens so far" number scales + glows orange and the sparkline frame
 * lights — so the "cost ticks ONCE off a flat line" reads instantly on camera. */
#meter.spike .meter-total .big {
  color: #ffd9c2;
  text-shadow: 0 0 6px var(--fresh), 0 0 3px var(--fresh);
  animation: meterBigPulse 1s cubic-bezier(0.16, 0.84, 0.44, 1);
}
#meter.spike .spark {
  border-color: var(--fresh);
  box-shadow: 0 0 6px -1px var(--fresh), inset 0 0 12px -6px var(--fresh);
}
@keyframes meterBigPulse {
  0%   { transform: scale(1); }
  22%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}

/* The sparkline: fresh tokens per receipt as a filled area, reused as a faint
 * underlay, broken out by surprise_cause as stacked tick bands. Flat near zero
 * on a quiet stretch; a single tall spike on a surprise = "cost scales with
 * surprise." A moving playhead marker tracks the scrub head. */
.spark-wrap { position: relative; }
.spark {
  display: block;
  width: 100%;
  height: 92px;
  background: var(--panel-2);          /* flat — was a linear-gradient */
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: 0 4px 6px -1px rgba(0,0,0,.45), 0 2px 4px -2px rgba(0,0,0,.45);
  transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.spark-baseline { stroke: var(--line); stroke-width: 1; }
.spark-area-fresh { fill: var(--fresh); fill-opacity: 0.16; stroke: none; }
.spark-line-fresh { fill: none; stroke: var(--fresh); stroke-width: 1.4; }
.spark-bar { stroke-width: 0; }
/* fresh bars are the hot lit orange regardless of cause — the sparkline is the
 * cost story, and cost is cost; the cause dots live in the rows below. A self
 * tick has zero fresh so it never draws a bar (the floor stays flat). */
.spark-bar.c-input { fill: var(--fresh); }
.spark-bar.c-self { fill: var(--cause-self); }
.spark-bar.c-external { fill: var(--fresh); fill-opacity: 0.75; }
.spark-bar.reused { fill: var(--reused); fill-opacity: 0.5; }
.spark-bar.future { opacity: 0.16; }
.spark-bar.spike-bar { filter: drop-shadow(0 0 5px var(--fresh)); }
.spark-head { stroke: var(--lit); stroke-width: 1.4; opacity: 0.9; }
.spark-caption {
  display: flex; justify-content: space-between;
  font-size: 9.5px; color: var(--ink-faint); margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.meter-total { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.meter-total .big {
  font-size: 38px;
  font-weight: 700;
  line-height: 1;
  color: var(--fresh);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  transition: text-shadow 0.18s ease, transform 0.18s ease, color 0.25s ease;
  transform-origin: left center;
}
/* on a quiet window the hero number COOLS to grey so "near zero" reads as calm,
 * not as a hot spend (review #5). It re-warms the instant a fresh tick lands. */
.meter-total .big.quiet { color: var(--ink-dim); }
.meter-total .unit { font-size: 10.5px; color: var(--ink-dim); }
.meter-bar { height: 8px; border-radius: 4px; background: var(--panel-2); overflow: hidden; display: flex; }
.meter-bar .seg-fresh { background: var(--fresh); }
.meter-bar .seg-reused { background: var(--reused); }
.meter-rows { display: grid; gap: 5px; font-size: 11px; }
.meter-row { display: grid; grid-template-columns: 14px 1fr auto; align-items: center; gap: 7px; }
.meter-row .swatch { width: 9px; height: 9px; border-radius: 2px; }
.meter-row .swatch.fresh { background: var(--fresh); }
.meter-row .swatch.reused { background: var(--reused); }
.meter-row .k { color: var(--ink-dim); }
.meter-row .v { color: var(--ink); font-variant-numeric: tabular-nums; }
.meter-causes { margin-top: 4px; display: grid; gap: 3px; font-size: 10.5px; color: var(--ink-dim); }
.meter-cause { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; font-variant-numeric: tabular-nums; }
.meter-cause .cause-name { display: inline-flex; align-items: center; gap: 5px; }
.meter-cause .dot { width: 7px; height: 7px; border-radius: 50%; }
.dot-input { background: var(--cause-input); }
.dot-self { background: var(--cause-self); }
.dot-external { background: var(--cause-external); }

/* ---- timeline ---- */
.timeline { display: grid; grid-template-rows: auto 1fr; min-height: 0; }
.receiptlist {
  margin: 0; padding: 0; list-style: none;
  overflow-y: auto; min-height: 0;
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
.receiptlist::-webkit-scrollbar { width: 8px; }
.receiptlist::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.ritem {
  display: grid;
  grid-template-columns: 30px 6px 1fr auto;
  align-items: center;
  gap: 8px;
  /* extra right padding keeps the cause tag (INPUT/EXTERNAL/SELF) off the
   * scrollbar gutter at narrow widths (critic polish #2: frames 21/53). */
  padding: 4px 12px 4px 6px;
  border-radius: 5px;
  cursor: pointer;
  color: var(--ink-dim);
}
.ritem:hover { background: rgba(255,255,255,0.04); }
.ritem .idx { color: var(--ink-faint); font-variant-numeric: tabular-nums; text-align: right; }
.ritem .tick { width: 6px; height: 6px; border-radius: 50%; justify-self: center; }
.ritem.s-rendered .tick { background: var(--rendered); }
.ritem.s-skipped .tick { background: var(--skipped); }
.ritem.s-failed .tick { background: var(--failed); }
.ritem .name { color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ritem.s-skipped .name { color: var(--ink-dim); }
.ritem .cause { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.04em; }
.ritem .cause.c-input { color: var(--cause-input); }
.ritem .cause.c-self { color: var(--cause-self); }
.ritem .cause.c-external { color: var(--cause-external); }
.ritem.future { opacity: 0.4; }
.ritem.current {
  background: var(--panel);
  box-shadow: inset 2px 0 0 var(--rendered);
}
.ritem.current.s-skipped { box-shadow: inset 2px 0 0 var(--skipped); }
.ritem.current.s-failed { box-shadow: inset 2px 0 0 var(--failed); }

/* FOCUS mode (review #5): on the hero / diamond / recover path beats, gently dim
 * the right-hand receipts timeline so the lit graph path is the brightest thing
 * on screen and pulls the eye. The cost meter stays full-brightness (it's the
 * co-hero); only the dense receipts list recedes. Smooth so it doesn't flicker as
 * the scrubber crosses beats. */
.receiptlist, #sidebar { transition: opacity 0.4s ease; }
body.focus .timeline { opacity: 0.38; transition: opacity 0.4s ease; }

/* ---- scrubber ---- */
#scrubber {
  border-top: 1px solid var(--line);
  padding: 9px 16px 11px;
  background: var(--panel);
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
}
.transport { display: flex; align-items: center; gap: 6px; }

/* GHOST buttons: transparent, machine-dim text; hairline appears + text deepens
 * to machine-fg on hover. (DESIGN.md §5 ghost button, dark tone.) */
.tbtn {
  background: transparent;
  color: var(--ink-dim);
  border: 1px solid transparent;
  border-radius: 6px;
  height: 28px;
  min-width: 30px;
  padding: 0 7px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.tbtn:hover { color: var(--ink); border-color: var(--line); background: rgba(255,255,255,0.03); }
.tbtn:active { transform: translateY(1px); }

/* PLAY: the one OUTLINED GOLD control that FILLS gold on hover (the Copy-button
 * pattern). Default = transparent fill, gold border + gold glyph. */
.tbtn-play {
  background: transparent;
  border-color: var(--lit);
  color: var(--lit);
  min-width: 38px;
}
.tbtn-play:hover { background: var(--lit); color: var(--bg); border-color: var(--lit); }
/* while playing, invert to a filled machine-bar "pause" affordance (no second
 * brand color — stays in the gold/charcoal family, just de-emphasized). */
.tbtn-play.playing { background: var(--panel); border-color: var(--line); color: var(--ink); }
.tbtn-play.playing:hover { color: var(--lit); border-color: var(--lit); }

/* speed select: sunken machine-bar well + hairline (DESIGN.md §5 input, dark) */
.speed { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-dim); font-size: 11px; margin-left: 6px; }
.speed select {
  background: var(--panel);
  color: var(--ink);
  border: 1px solid rgba(154,161,172,0.25);   /* machine-dim/25 input hairline */
  border-radius: 5px;
  padding: 3px 6px;
  font: inherit;
  font-size: 11px;
}
.speed select:focus-visible { outline: 2px solid rgba(202,169,107,0.4); outline-offset: 1px; }

/* seek: gold accent (the active scrub position is "the machine working") */
.seek { width: 100%; accent-color: var(--lit); cursor: pointer; }
.scrubreadout {
  color: var(--ink-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  min-width: 220px;
  text-align: right;
}
.scrubreadout .hl { color: var(--ink); }

/* =====================================================================
 * prefers-reduced-motion — neutralize every TRANSIENT pulse/flash/chase.
 * The steady-state (parked) treatments — `.path-lit`, `.path-node`,
 * `.rendered-now`, `.hit.failed`, `.lit-floor`, the held halos — are plain
 * declarations (no animation), so they survive untouched and the lit-vs-dark
 * hero still reads on a still. We only flatten the decaying motion + the
 * smooth opacity/transform transitions used as effects, so a scrub lands
 * instantly on its end state instead of animating there.
 * ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* the chasing edge dash + travelling bead are pure motion ornaments with no
   * meaningful end state — hide them so nothing flickers at frame 0. */
  .flow-dash, .flow-bead { display: none !important; }
}
