/* ============================================================================
 * shell.css — Reactor⚛️Dynamics M8 visual shell (layout + look only).
 *
 * A static, mock-data prototype of the control-room UI for rapid iteration on
 * appearance and layout BEFORE wiring it to the live snapshot (M5). Colors are
 * the M8 §15 palette; regions are the M8 §2 layout. Nothing here is wired to the
 * engine — every value is a placeholder.
 * ========================================================================== */

:root {
  /* QUIET-BOARD palette (concept-scan matched). The board reads near-black and
     near-monochrome when normal; color is spent ONLY on the abnormal. Green =
     equipment energized/armed (a calm, stable accent); amber = caution; red =
     alarm/trip (the only saturated, attention-pulling color). EPRI 1003662 /
     NUREG-0700 / ISA-18.2 "manage by exception". */
  /* Backgrounds (darkest → lightest) */
  --bg-strip: #0C0F12;
  --bg-plant: #0E1216;
  --bg-right: #0F1318;
  --bg-elevated: #161B21;
  --border: #232931;
  /* Text — muted by default so readouts recede until something is wrong
     (nudged a touch brighter across the board for legibility) */
  --text: #E4E9EE;
  --text-2: #98A3AF;
  --muted: #69757F;
  --disabled: #3A434D;
  /* Semantic state (board / chrome)
     green  = running / normal
     amber  = abnormal and/or caution alarms
     red    = problem and/or automatic protection (trip)
     cyan   = available for user input
     grey   = OK but not ideal, no alarm */
  --normal: #5BB3C4;        /* cyan — interactive / selected / editable */
  --caution: #D9A441;       /* amber — caution / abnormal */
  --critical: #E5484D;      /* red — trip / critical */
  --running: #46A35E;       /* green — energized, running, healthy normal */
  --running-muted: #35704A;
  --stopped: #586673;
  --trend-up: #FB923C;
  --trend-down: #67E8F9;
  --core-glow: #00C4FF;

  --radius: 6px;
  --gap: 8px;
  font-size: 14px;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  background: #070A0C;
  color: var(--text);
  font-family: ui-sans-serif, "Segoe UI", Roboto, system-ui, sans-serif;
  font-size: 14px;
  overflow: hidden;
}
.mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }

/* ===================================================================== app */
.app {
  display: flex;
  gap: var(--gap);
  height: 100vh;
  padding: var(--gap);
}

/* Left: plant-control area (M8 §2.2 — would be fixed aspect ratio; here it
   simply fills available space for easy iteration). */
.plant-area {
  flex: 1 1 auto;
  min-width: 760px;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  background: var(--bg-plant);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
  overflow: hidden;
  position: relative;   /* anchors the app toast */
}

/* Right column: sim controls + instructor + tools + scanner */
.right-col {
  flex: 1 1 340px;
  min-width: 320px;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  overflow: hidden;
}
/* Panel order in the right column: sim controls, the reference TAB strip, then the
   Instructor block below it — pull above, push below (#439, spec §2/§3). The Scanner
   used to be a fourth entry here and is now a line under the board (spec §6). */
.right-col > .sim-controls { order: 0; }
.right-col > #toolsCard     { order: 1; }
.right-col > #instructorCard { order: 2; }

/* Responsive: the layout must survive any monitor. The 760px plant-area floor
   was what forced horizontal scroll on mid-size displays; relax it first, then
   stack the columns on genuinely narrow windows. */
@media (max-width: 1200px) {
  .plant-area { min-width: 0; }
  .right-col { flex-basis: 320px; min-width: 300px; }
}
@media (max-width: 860px) {
  html, body { overflow: auto; }
  .app { flex-direction: column; height: auto; min-height: 100vh; }
  .plant-area, .right-col {
    flex: 0 0 auto; min-width: 0; max-width: none; width: 100%; overflow: visible;
  }
  .synoptic { min-height: 220px; }
  .bottom-row { flex: 0 0 auto; height: 240px; }
}

/* ============================================ PWR control-room 3-column layout
   The PWR board grids .app into: the diagram + vital gauges (left, kept as large
   as possible), the alarms (top) + strip chart (bottom) stacked into a middle
   column, and the simulator panel — time controls pinned on top — on the right.
   app.js (buildPlantDisplay) moves .bottom-row out from under the diagram into
   the middle column, so the diagram reclaims that vertical room. Gridding .app
   makes the older .app.pwr-synoptic flex rules on these children inert;
   pwr_board.js skips its plant-width lock in this mode so the board fits the
   grid track (its ResizeObserver refits on any relayout).

   The ⛶ button hides the right panel (.sim-hidden): the layout drops to two
   columns and app.js moves the time controls (.sim-controls) atop the middle
   column, enlarging the diagram further. */
.demo-btn.on { color: var(--running); border-color: var(--running); }

/* --simcol-w is the simulator track's MAXIMUM, driven by pwr_board.js fitColumns():
   the diagram keeps the width it needs at full height and the leftover — large on
   short-and-wide windows, where the board would otherwise letterbox — is handed to
   that column instead of sitting dead beside the diagram. The value here is the
   floor used before/without that measurement. */
.app.pwr-synoptic {
  --simcol-w: 360px;
  --bottomrow-h: 230px;
  position: relative;   /* splitter handles are absolutely positioned over the grid */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, var(--simcol-w));
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "diagram simcol";
}
.app.pwr-synoptic > .plant-area { grid-area: diagram; min-width: 0; max-width: none; }
.app.pwr-synoptic > .right-col  { grid-area: simcol; min-width: 0; max-width: none; }

/* PWR-only: hide the legacy 4-view switcher / status bar / control row, and let the
   board own the whole view area. app.js empties those three containers on the PWR
   path, but an empty container still takes its padding/border — so they are hidden,
   not merely cleared. These four rules moved here from the retired V1 stylesheet
   (ui/diagram/pwr_synoptic.css, #246): they were always shell hooks keyed on the
   .app class, not V1 board styling, and the V2 board needs them. RBMK/BWR untouched. */
.app.pwr-synoptic .view-switcher,
.app.pwr-synoptic .status-bar,
.app.pwr-synoptic .pd-controlbar { display: none; }
/* NO TOP OR LEFT BORDER ON THE DIAGRAM *(OWNER DIRECTIVE, 2026-08-11: "The top edge of
   the boarder around the diagram takes up too much space. Remove the top and left sides
   of the boarder around the diagram.")*. The board scales to fit its own HEIGHT, so every
   pixel the top edge takes comes straight out of the diagram — and the corner radius has
   to go with the two edges or it draws an arc against nothing. The right and bottom edges
   stay: they separate the diagram from the sim column and the trend strip. */
.app.pwr-synoptic .view-area {
  padding: 0; overflow: hidden;
  border-top: none; border-left: none;
  border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 0;
}

/* The V2 board carries its own vital-parameter tile strip (six Indicator Panel
   components across the top), which is what the shell's .gauge-strip used to provide.
   Two copies of the same six readings is worse than one, and the shell copy is the one
   that is NOT part of the diagram — so it is hidden here rather than deleted, because
   RBMK/BWR still run the legacy plant display and genuinely need it. app.js keeps
   building it; that is a few hidden nodes, and it means nothing breaks if a plant
   without its own tiles is put back on this layout. */
.app.pwr-synoptic > .plant-area > .gauge-strip { display: none; }

/* keep the diagram's flex chain honest so the board fits its column width
   instead of overflowing (the .main-area link lacks min-width:0) */
.app.pwr-synoptic > .plant-area .main-area,
.app.pwr-synoptic > .plant-area .view-area,
.app.pwr-synoptic > .plant-area .pwr-board-wrap { min-width: 0; }

/* Under-the-diagram strip: chart and alarms side by side, and — only when the sim panel
   is hidden (⛶) — the time controls tuck in at the left. Fixed height so the diagram
   keeps the rest; the two panels split the width. */
/* FIXED height, draggable — not `flex: 1 1 auto` (#233). Letting this row grow into the
   leftover height fought the sim column for the same space and the DIAGRAM lost both ways:
   a taller strip shortens the board, a shorter board needs less width, and fitColumns then
   hands that freed width to the sim column, which shortens the board again. Two auto-growers
   either side of a fit-to-height diagram is a feedback loop, not a layout. Pinning this row
   breaks it; --bottomrow-h is the operator's to drag (see the splitters below). */
.app.pwr-synoptic .plant-area > .bottom-row { flex: 0 0 var(--bottomrow-h); min-height: 150px; flex-direction: row; }

/* Draggable panel edges (#233). The auto-fit is a good default, not a policy: drag the sim
   panel's inner edge or the trend strip's top edge to trade space with the diagram. A drag
   PINS that axis (persisted in localStorage) and fitColumns stops touching it. */
.bd-split { position: absolute; z-index: 40; touch-action: none; }
.bd-split::after {
  content: ''; position: absolute; inset: 0; border-radius: 2px;
  background: var(--accent, #4fe3ff); opacity: 0; transition: opacity .12s ease;
}
.bd-split:hover::after, .bd-split.bd-dragging::after { opacity: .55; }
.bd-split-v { top: 0; bottom: 0; width: 9px; cursor: col-resize; }
.bd-split-v::after { left: 3.5px; right: 3.5px; }
.bd-split-h { height: 9px; cursor: row-resize; }
.bd-split-h::after { top: 3.5px; bottom: 3.5px; }
/* ⛶ removes the sim column, so there is no vertical edge left to drag. */
.app.pwr-synoptic.sim-hidden .bd-split-v { display: none; }
/* Stacked (narrow) layouts size themselves; dragging a track that isn't there is nonsense. */
@media (max-width: 1200px) { .bd-split { display: none; } }
.app.pwr-synoptic .plant-area > .bottom-row > .sim-controls { order: 1; flex: 0 0 auto; }
/* THE ROW IS TWO PANELS AGAIN (#713 / #724 item 7). The 1/M dock left this row for the foot of
   the right-hand column at the owner's instruction, so the three-way split it forced is over.

   WHAT THE SPLIT COST, AND WHY THE HISTORY IS KEPT RATHER THAN DELETED. #713 widened the dock
   300 -> 380 -> 420 px, taking width first from the strip chart (the owner asked for that
   specifically) and then from the alarm panel, and the grow ratio went 1.6 -> 1.55 to do it.
   Pass 3 had to hand 50 px straight back: the alarm panel's two-column content floor is 377.7 px
   under Windows' Segoe UI and 412 px under CI's DejaVu Sans, and `verify_e2e_ui` was red on every
   CI run in between. THE FLOOR IS THE NUMBER TO REMEMBER — 412 px on the platform that gates, not
   378 px on the one you are looking at — and any future rule that squeezes this panel has to be
   measured there. Diagnostic/TUNING_LOG.md 2026-09-12-workbench-d / -e.

   The ratio stays 1.55 rather than reverting to 1.6: 1.55 was the MEASURED split (the alarm panel
   does not use the width 1.6 gives it), and it is now applied to a row 370 px wider, so the panel
   clears its Linux floor by a margin no font can eat. Reverting the number would undo a
   measurement to restore a guess. */
.app.pwr-synoptic .plant-area > .bottom-row > .strip-chart  { order: 2; flex: 1 1 0; min-width: 0; }
.app.pwr-synoptic .plant-area > .bottom-row > .alarm-panel  { order: 3; flex: 1.55 1 0; min-width: 0; min-height: 0; }

/* ⛶ hides the right sim panel → the diagram column takes the whole width */
.app.pwr-synoptic.sim-hidden {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "diagram";
}
.app.pwr-synoptic.sim-hidden > .right-col { display: none; }

/* narrow screens: stack the columns so nothing overflows. The chart+alarms strip rides
   with the diagram now (it is inside .plant-area), so only the sim panel drops below. */
@media (max-width: 1200px) {
  html, body { overflow: auto; }
  .app.pwr-synoptic {
    height: auto; min-height: 100vh;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: 78vh minmax(260px, 22vh);
    grid-template-areas: "diagram" "simcol";
  }
  .app.pwr-synoptic.sim-hidden {
    grid-template-rows: minmax(0, 1fr);
    grid-template-areas: "diagram";
  }
  /* stack chart above alarms rather than squeezing both into a narrow row */
  .app.pwr-synoptic .plant-area > .bottom-row { flex: 0 0 auto; flex-direction: column; }
  /* (the 1/M dock used to be pinned here — it left the bottom row for the right-hand column,
     #724 item 7; its narrow-screen rule now lives with the dock itself, below.) */
}

/* generic panel chrome */
.panel {
  background: var(--bg-right);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.panel-title {
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-2); padding: 8px 10px 6px; font-weight: 600;
}

/* ===================================================== vital-few gauge strip */
.gauge-strip {
  flex: 0 0 auto;
  display: flex; gap: var(--gap); flex-wrap: wrap;
  background: var(--bg-strip);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px;
  align-items: stretch;
  position: relative;
}
.gauge-strip.alarm-tint { box-shadow: inset 0 0 0 1px rgba(239,68,68,.35); }
.gauge {
  flex: 1 1 0; min-width: 96px;
  background: var(--bg-plant);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 6px 8px 7px;
  display: flex; flex-direction: column; gap: 3px;
}
.gauge.lead { box-shadow: inset 0 2px 0 0 var(--normal); }
/* gauge card background subtly tints with state (reinforces the value color) */
.gauge.warn  { background: var(--gauge-bg-warn);  border-color: var(--gauge-border-warn); }
.gauge.alarm { background: var(--gauge-bg-alarm); border-color: var(--gauge-border-alarm); }
/* Quiet board: a normal reading is the DIM resting state — the value's color is
   its status. Normal = dim blue-white, Caution = amber, Alarm = red + flash. */
.gauge .g-label { font-size: 10px; color: var(--clr-dim-label); letter-spacing: .03em; display:flex; justify-content:space-between; }
/* value + mini strip chart share one row (chart to the RIGHT of the number) so the
   gauge strip stays short — freeing vertical room for the diagram below. */
.g-valrow { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* Colors match the plant diagram's readout scheme: green normal, amber caution, red alarm
   (the diagram uses cyan only for user-editable inputs, so the gauges — pure readouts —
   stay green like every other indication on the board). */
.gauge .g-value { flex: 0 0 auto; font-size: 22px; font-weight: 650; line-height: 1.05; color: #5aad7c; }
.gauge .g-units { font-size: 10px; color: var(--clr-dim-label); }
.gauge.warn  .g-value { color: #ffd166; }
.gauge.alarm .g-value { color: #ff6a4d; animation: gauge-alarm-flash .6s steps(1, end) infinite; }
@keyframes gauge-alarm-flash { 50% { opacity: .4; } }
/* trend arrow: dim at rest; color (not direction) signals deviation */
.gauge .g-trend { font-size: 11px; color: #3a4e60; }
.gauge.warn  .g-trend { color: #c09020; }
.gauge.alarm .g-trend { color: #e05030; }
.trend-up, .trend-down, .trend-flat { /* direction is the glyph; color is state (above) */ }
/* range bar: a single DIM track; a colored fill (set by renderGauges) appears to
   the needle ONLY when the value is in a warn/alarm band — never a rainbow. */
.g-band { height: 5px; border-radius: 3px; position: relative; margin-top: 2px; background: var(--bar-track-normal); }
.g-band .g-fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 3px; }
.g-band .needle { position: absolute; top: -2px; width: 2px; height: 9px; background: var(--text); border-radius: 1px;
  transition: left .12s linear; z-index: 1; }
.g-band .trip { position: absolute; top: -3px; width: 0; height: 11px; border-left: 1px dashed var(--critical); }
/* mini strip chart: sits beside the number (1 min of data), fills the slack width */
.g-spark { flex: 1 1 auto; width: auto; min-width: 32px; height: 20px; opacity: .85; }

/* ===================================================== control sections row */
/* minmax(0,1fr) — NOT plain 1fr — forces genuinely equal columns. Plain 1fr is
   minmax(auto,1fr): a column won't shrink below its content's intrinsic width,
   so the dense Reactor-Core rod cluster used to steal width from the others. */
.control-sections {
  flex: 0 0 auto;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gap);
}
.section {
  background: var(--bg-strip);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px;
  display: flex; flex-direction: column; gap: 7px;
  min-height: 150px;
  min-width: 0;          /* allow the grid track to shrink past content */
}
.section h3 {
  margin: 0 0 2px; font-size: 11px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-2); font-weight: 700;
}
/* wrap so a control cluster (rod drive + speed + nudge) reflows instead of
   forcing its column wider than the rest */
.ctl-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ctl-label { font-size: 11px; color: var(--text-2); flex: 1 1 auto; }
.ctl-readout { font-size: 12px; font-weight: 600; }

/* button group (segmented) */
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 5px; overflow: hidden; }
.seg button {
  background: var(--bg-elevated); color: var(--text-2); border: none;
  padding: 4px 8px; font-size: 11px; cursor: pointer; border-right: 1px solid var(--border);
}
.seg button:last-child { border-right: none; }
.seg button:hover { color: var(--text); }
/* selected control state (quiet board): OUTLINE chips, not filled blocks — a
   thin colored ring + colored text marks the active option, the alternative
   stays muted gray. Exactly one accented chip per control = a calm board.
   .run = energized/armed (green), .warn = caution (amber). */
.seg button.on { background: transparent; color: var(--text); font-weight: 700; box-shadow: inset 0 0 0 1px #38424d; }
.seg button.on.run { color: #6FD089; background: rgba(70,163,94,.08); box-shadow: inset 0 0 0 1px rgba(70,163,94,.65); }
.seg button.on.warn { color: var(--caution); background: rgba(217,164,65,.10); box-shadow: inset 0 0 0 1px rgba(217,164,65,.65); }
/* press-and-hold (rod drive) active state */
.seg button.holding { color: #6FD089; background: rgba(70,163,94,.16); box-shadow: inset 0 0 0 1px rgba(70,163,94,.7); }

.btn {
  background: var(--bg-elevated); color: var(--text); border: 1px solid var(--border);
  border-radius: 5px; padding: 5px 9px; font-size: 11px; cursor: pointer;
}
.btn:hover { border-color: var(--text-2); }
.btn.ghost { background: transparent; }
/* A control that can't act right now must look it (honest affordances) */
button:disabled { opacity: .45; cursor: not-allowed; }
/* Keyboard focus must be visible board-wide (never for mouse clicks) */
button:focus-visible, input:focus-visible, select:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--normal); outline-offset: 1px;
}

/* rod position bar */
.rodbar { height: 10px; border-radius: 5px; background: #11151a; border: 1px solid var(--border); position: relative; overflow: hidden; }
.rodbar > span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--running-muted); transition: width .12s linear, background .2s ease; }
.rodbar.shutdown > span { background: var(--running-muted); }
.rodbar .limit { position: absolute; top: -2px; bottom: -2px; width: 0; border-left: 1px dashed var(--caution); }

/* emergency systems set apart (§5.5) */
.emergency { border: 1px dashed var(--caution); border-radius: 5px; padding: 6px; }
.emergency .ctl-label { color: var(--caution); }

/* SCRAM cover/button (§5.1) — quiet-board states:
 *   not scrammed, cover down  → DULL GREEN striped guard (calm "armed/ready")
 *   cover lifted (armed)      → DULL RED exposed SCRAM button
 *   scrammed (manual or auto) → BRIGHT RED FLASHING indicator (always visible) */
.scram-wrap { position: relative; height: 56px; }
.scram-cover, .scram-btn {
  position: absolute; inset: 0; border-radius: 6px; display: flex; align-items: center; justify-content: center;
  font-weight: 800; letter-spacing: .1em; cursor: pointer; user-select: none; text-align: center;
}
/* guard cover nearly invisible at rest: dark bg, dim green border, dark green text
   (no siren). It only becomes vivid red once the scram has fired. */
.scram-cover {
  background: #10151a; border: 1px solid #2e5a3f; color: #3f7a55; box-shadow: none;
}
/* dull red exposed button when the cover is lifted (armed, not yet fired) */
.scram-btn { background: #7f2a2a; border: 2px solid #9e3b3b; color: #f1d6d6; display: none; }
.scram-wrap.open .scram-cover { display: none; }
.scram-wrap.open .scram-btn { display: flex; }
/* SCRAMMED: force the bright-red flashing indicator regardless of cover state */
.scram-wrap.scrammed .scram-cover { display: none; }
.scram-wrap.scrammed .scram-btn { display: flex; }
/* A4 — fired state: one red palette, 0.5s step-end opacity flash (no color cycling) */
.scram-btn.fired { cursor: default; background: #1a0600; border: 2px solid #b03020; color: #e04020;
  font-weight: 700; letter-spacing: .1em; animation: scram-flash .5s step-end infinite; }
@keyframes scram-flash { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .3; } }
.scram-arc { position: absolute; inset: -2px; border-radius: 8px; border: 2px solid transparent;
  border-top-color: var(--critical); animation: scram-countdown 3s linear forwards; pointer-events: none; }
@keyframes scram-countdown { from { transform: rotate(0deg); opacity: 1; } to { transform: rotate(360deg); opacity: .2; } }

/* ============================================================ synoptic area */
/* Diagram block is FIXED height; the chart/alarm strip below takes the slack. */
.synoptic {
  flex: 0 0 340px;
  background: var(--bg-strip);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px;
  display: flex; flex-direction: column;
  position: relative;
}
.synoptic-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.synoptic-head .seg { font-size: 10px; }
.placeholder-note { font-size: 10px; color: var(--muted); }
/* rod-bank mini-indicator — sits under the Reactor/Core numeric column */
.rod-mini { margin-top: 9px; padding-top: 9px; border-top: 1px dotted #23272d; display: flex; flex-direction: column; gap: 9px; }
.rm-grp { display: flex; flex-direction: column; gap: 4px; }
.rm-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 12px; }
.rm-head .nk { color: var(--text-2); }
.rm-head .rm-val { font-weight: 600; }
.rod-mini .rodbar { height: 9px; }
.numeric-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; overflow: auto; min-height: 0;
}
.num-col h4 { margin: 0 0 6px; font-size: 12px; letter-spacing: .05em; color: var(--normal); text-transform: uppercase; }
.num-line { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; padding: 3px 0; border-bottom: 1px dotted #23272d; }
.num-line .nk { color: var(--text-2); }
.num-line .nv { font-weight: 600; color: var(--clr-status-normal); }
/* Quiet board: status words are dim at normal; color is spent only on deviation. */
.num-line .nv.q-normal  { color: var(--clr-status-normal); }
.num-line .nv.q-caution { color: var(--caution); }
.num-line .nv.q-alarm   { color: var(--critical); }

/* ---------------- the merged indicated-vs-true list (#439, spec §3) ----------------
   TWO SHORT VALUES PER ROW, not two tables. The row is a grid rather than a flex so
   the two value columns line up down the list — a ragged right edge is what makes a
   paired list unreadable, and the whole claim of the merge is that a disagreement is
   visible without hunting. Fixed columns also mean the true side can be hidden by
   collapsing one track (HR1), with no reflow of the other two. */
/* FIXED TRACK WIDTHS, shared with the column header via custom properties. They used to
   be `auto`, which sizes to content — fine when only values were in the column, wrong the
   moment a HEADING joined it: "Plant indication" is far wider than "50 %", so the header's
   auto track came out 25px wider than the row's and every number sat left of its label.
   Two grids can only agree on `auto` if they contain the same text, which these never do. */
:root { --ind-plot: 30px; --ind-v: 64px; --ind-t: 64px; }
.num-line {
  display: grid; align-items: baseline; gap: 0 8px;
  grid-template-columns: var(--ind-plot) 1fr var(--ind-v) 0 0;
}
#indicationsList.show-true .num-line {
  grid-template-columns: var(--ind-plot) 1fr var(--ind-v) var(--ind-t) 12px;
}
/* The column headings. SAME GRID TEMPLATE AS .num-line, deliberately duplicated rather
   than shared through a wrapper: the rows are grouped inside .ind-grp blocks with their
   own <h4>, so a heading row and a value row are never siblings in one grid and cannot be
   aligned by a common parent. Two identical templates that must move together is the
   honest cost of that; KEEP THEM IN SYNC — if a track is added below, add it here. */
.ind-colhead {
  display: grid; gap: 0 8px;
  grid-template-columns: var(--ind-plot) 1fr var(--ind-v) 0 0;
  align-items: baseline; position: sticky; top: 0; z-index: 2;
  background: var(--bg-panel, var(--bg-elevated)); border-bottom: 1px solid var(--border);
  padding: 2px 0 3px; margin-bottom: 2px;
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.ind-colhead.show-true {
  grid-template-columns: var(--ind-plot) 1fr var(--ind-v) var(--ind-t) 12px;
}
/* The heading's first cell must be EXACTLY as wide as a row's .plot-cell, or every
   column below drifts left of its label: both tracks are `auto`, and an empty span
   measures 0 while a checkbox does not. Mirrors .plot-cell — keep them in step. */
.ind-colhead .ch-plot { display: inline-block; }   /* the track sizes it now */
.ind-colhead .ch-v, .ind-colhead .ch-t { text-align: right; white-space: nowrap; }
.ind-colhead:not(.show-true) .ch-t, .ind-colhead:not(.show-true) .ch-w { display: none; }
.num-line .nk { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num-line .nv { text-align: right; white-space: nowrap; }
/* The true side is deliberately quieter than the instrument beside it: the instrument
   is what the operator is meant to be reading, and this is the debug overlay. */
.num-line .nv-true {
  font-weight: 600; color: var(--muted); text-align: right; white-space: nowrap;
  font-size: 12px; overflow: hidden;
}
.num-line .nv-warn { color: var(--caution); font-size: 11px; visibility: hidden; }
#indicationsList:not(.show-true) .nv-true,
#indicationsList:not(.show-true) .nv-warn { display: none; }
/* A DISAGREEING row, flagged rather than left to be spotted. Not red: an instrument
   that disagrees with the plant is not itself a plant condition — it is a fact about
   the channel, and colouring it like an alarm would put a plant state on the board
   that the plant is not in. */
.num-line.diverged .nv-warn { visibility: visible; }
.num-line.diverged .nv { color: var(--caution); }
.num-line.diverged .nv-true { color: var(--text-2); }
/* Row-type chips. The filter is a data-attribute on the list so it costs one attribute
   write rather than a class toggle per row on a 119-row list. */
.ind-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 6px 0 8px; }
.ind-filters { display: flex; }
.ind-filters button {
  background: var(--bg-elevated); border: 1px solid var(--border); color: var(--muted);
  font: inherit; font-size: 11px; padding: 2px 8px; cursor: pointer;
}
.ind-filters button:first-child { border-radius: 4px 0 0 4px; }
.ind-filters button:last-child { border-radius: 0 4px 4px 0; }
.ind-filters button + button { border-left: none; }
.ind-filters button.on { color: var(--text); border-color: var(--normal); background: rgba(91,179,196,.12); }
.ind-truth { margin-left: auto; font-size: 11px; }
.ind-truth.on { color: var(--normal); border-color: var(--normal); }
/* A MONITORED ROW IGNORES THE FILTER (#477): the chips are a way of narrowing a 120-row
   list you are reading, and the Monitoring block is a list the reader has already narrowed
   by hand. A chip that emptied it would hide a channel the operator explicitly asked to
   watch — the one thing this block exists not to do. Hence the :not(.ind-monitor) scope. */
#indicationsList[data-filter="paired"] .ind-grp:not(.ind-monitor) .num-line:not([data-rowtype="paired"]),
#indicationsList[data-filter="ind"] .ind-grp:not(.ind-monitor) .num-line:not([data-rowtype="ind"]),
#indicationsList[data-filter="phys"] .ind-grp:not(.ind-monitor) .num-line:not([data-rowtype="phys"]) { display: none; }
/* The Monitoring block (#477) — the ticked channels, duplicated above every system group.
   It keeps `.ind-grp` for the row metrics below, so it reads as part of the same list, and
   is set apart by a live heading and extra space rather than by a rule: the group under it
   already draws its own border-top, and a second line right above that reads as a mistake.
   NOTE that it is wrapped in #indMonitor rather than being a direct child of the list, so
   `#indicationsList > .ind-grp` does not reach it and it never draws a border of its own. */
#indMonitor:not(:empty) { display: block; margin-bottom: 10px; }
.ind-monitor h4 { color: var(--normal); }
.ind-monitor .ind-mon-n { color: var(--muted); font-weight: 400; }
/* …and with the block absent, the first system group must still look like the first thing
   in the list. It stopped being `:first-child` the moment the (empty) #indMonitor took that
   slot, which would have put a rule above "Reactor core" on a board nobody had ticked. */
#indicationsList > #indMonitor:empty + .ind-grp { border-top: none; padding-top: 0; }
/* The swatch is a READOUT, not a control (#477) — it says the channel is trending, which is
   chosen in the chart's own settings window. Clicking it must reach the row underneath. */
.plot-cell .ser-swatch { pointer-events: none; }
.ff-badge { position: absolute; right: 10px; bottom: 8px; font-size: 11px; color: var(--caution);
  background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.4); border-radius: 4px; padding: 1px 6px; }

/* ====================================================== bottom: chart+alarm */
/* the diagram (main-area) now stretches; the chart/alarm strip is a fixed band */
.bottom-row { flex: 0 0 196px; display: flex; gap: var(--gap); min-height: 150px; }
.strip-chart {
  flex: 1 1 auto; background: var(--bg-strip); border: 1px solid var(--border); border-radius: var(--radius);
  display: flex; flex-direction: column; padding: 6px 8px;
}
/* The chart's own header (#439, spec §8): the legend on the left, the window ladder
   and CSV on the right. These two came OUT of Settings when Settings became a pausing
   modal — the control that changes how you are watching a transient must sit on the
   thing it changes, and must never stop the plant to do it. */
.chart-head { display: flex; align-items: center; gap: 8px; min-height: 16px; }
.chart-ctl { display: flex; align-items: center; gap: 6px; margin-left: auto; flex: 0 0 auto; }
.chart-ctl .seg button { font-size: 10px; padding: 1px 5px; min-height: 18px; }
.chart-ctl .btn { font-size: 10px; padding: 1px 6px; }
.chart-legend { flex: 1 1 auto; min-width: 0; font-size: 10px; color: var(--muted); letter-spacing: .02em; }

/* ---------------- the lane stack (#440, spec §8) ----------------------------
   PORTED FROM ui/test_panel/lane_reference.html, which is the golden artifact and
   measures itself against the budget: four 48 px lanes plus the 12 px shared axis
   and a 10 px event ribbon come to 216 px against ~220 px. Change the reference
   first, re-measure, then port — prose is poor at density because density is a
   hundred small decisions and a document can only name a few of them.

   PROHIBITED IN THIS REGION, stated as a rule because it otherwise returns every
   time the file is regenerated: card wrappers, drop shadows, nested borders, and
   default utility spacing scales. Spacing values here are explicit.

   Labels and values are HTML over the SVG. The canvas is preserveAspectRatio="none"
   over a fixed 400x120 box, so <text> inside it would be squashed non-uniformly at
   exactly the widths this budget is fighting for. */
.lane-chrome {
  position: absolute; left: 0; right: 0;
  display: flex; align-items: baseline; gap: 6px;
  padding: 0 2px 0 4px;                 /* 4 px horizontal, per the reference */
  font-size: 10px; line-height: 1.1;
  pointer-events: none;
}
.lane-name {
  font-weight: 600; letter-spacing: .02em;
  text-shadow: 0 0 3px var(--bg-strip), 0 0 3px var(--bg-strip);   /* readable over its own trace */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* EACH LANE PRINTS ITS CURRENT RANGE, so no lane's amplitude is ambiguous — which is
   what makes per-lane autoscale honest now that traces are not overlaid. */
.lane-rng {
  margin-left: auto; margin-right: 14.5%;   /* clears the value column (1 - CHART_PLOT_FRAC) */
  font-size: 9px; color: var(--muted); white-space: nowrap;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 3px var(--bg-strip);
}
/* The value column: fixed width, tabular, right-aligned. Without all three, values
   shift horizontally as digit counts change and the whole stack jitters. */
.lane-value {
  position: absolute; right: 2px; transform: translateY(-50%);
  min-width: 13%; text-align: right;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 11px; line-height: 1.15; color: var(--text);
  border-left: 2px solid var(--cf, var(--text-2)); padding-left: 4px;
  white-space: nowrap; pointer-events: none;
}
.lane-chrome.hot .lane-name, .lane-value.hot { color: var(--caution); }
/* Reading AT THE CURSOR rather than live — coloured so the two can never be confused.
   Cyan, because it is a property of the view and not of the plant. */
.lane-value.at-cursor { color: var(--normal); }
/* THE SECOND FIGURE, for a channel set to trace BOTH sides (#454). It goes on its own line
   under the instrument reading rather than beside it: the value column is deliberately
   fixed-width to stop the stack jittering, and widening it to fit a pair would undo that for
   every lane including the ones showing one number. Dotted underline, matching the dashed
   trace it names — the lane holds two lines in one hue and this is what says which is which.
   The lane is 44-56 px and two 11 px lines with a 9 px second are ~24 px, so the pair still
   sits inside the band it belongs to. */
.lane-value-phys {
  display: block; font-size: 9px; line-height: 1.1; color: var(--text-2);
  border-bottom: 1px dashed currentColor; padding-bottom: 1px;
}
.lane-value.paired { line-height: 1.05; }
.lane-numv .lane-value-phys { display: inline; border-bottom: 1px dashed currentColor; margin-left: 4px; }

/* NUMERIC ROWS — the pinned channels that did not fit as traces (#440, §14-7a ruling).
   ~18 px each against a 44–56 px lane, so a stack that cannot hold another trace still
   holds several more numbers. Same pinned list, a second rendering — not a second
   feature, which is why they share the lane host and the value column's alignment. */
.lane-nums {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column;
  pointer-events: none;
}
.lane-num {
  display: flex; align-items: baseline; gap: 6px;
  height: 18px; padding: 0 2px 0 4px;
  border-top: 1px solid var(--border);
  font-size: 10px; line-height: 18px;
  background: var(--bg-strip);
}
.lane-numv {
  margin-left: auto; min-width: 13%; text-align: right;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 11px; color: var(--text);
  border-left: 2px solid var(--cf, var(--text-2)); padding-left: 4px;
}
.lane-num.hot .lane-name, .lane-num.hot .lane-numv { color: var(--caution); }

/* ---------------- the SOE ribbon (#442, spec §8) ----------------------------
   10 px, reserved in the density budget by the reference lane. Tier-2 ticks live
   here; tier 1 draws full height across the lanes instead, because a
   plant-defining event is context for every lane at once.

   OPERATOR ACTIONS ARE VISUALLY DISTINCT FROM PLANT RESPONSES — cyan for what you
   did, amber for what the plant did. The spec calls this the most valuable
   teaching distinction the timeline can carry: a student reads their own hand in
   the record and sees cause and effect directly. */
.soe-ribbon {
  position: relative; height: 10px; flex: 0 0 auto;
  border-top: 1px solid var(--border);
  background: rgba(255, 255, 255, .015);
}
.soe-mark {
  position: absolute; top: 1px; height: 8px; min-width: 2px;
  background: var(--caution); border-radius: 1px;
  font-size: 7px; line-height: 8px; color: #17120A; text-align: center;
  cursor: pointer;
  /* Density applies to DISPLAY, not controls: keep the visual small and expand the
     hit region beyond it, or a 2 px mark is unclickable. */
  padding: 0 1px;
  box-shadow: 0 0 0 6px transparent;
}
.soe-mark.op { background: var(--normal); color: #04121a; }
/* A CLUSTER carries its count. A trip cascade fires a dozen events inside two
   seconds — one pixel in a 30-minute window — and drawn individually they overlap
   into a mark that says "something happened" and nothing else. */
.soe-mark.many { min-width: 9px; font-weight: 700; }
.soe-mark:hover { outline: 1px solid var(--text-2); }
/* Tier-1 lines are drawn in the SVG, so they scale with the plot rather than
   needing their own layer. */
#chartCanvas .soe-t1 { pointer-events: none; }
/* WHERE THE RUN BEGAN (sim time zero). The chart opens empty and fills from the right
   (#501 removed the preseeded history), so this marks where the record begins on the
   otherwise-bare axis.

   THE TAG SITS AT THE BOTTOM, and that is a screenshot finding rather than a preference. At
   the top it landed inside the first lane's range label and rendered as "40% T+0 %" — the
   lane chrome puts a NAME top-left and a RANGE top-right in every lane, so the top strip is
   the one row of the plot that is never free. The bottom edge carries nothing but trace, and
   it is also the end nearest the time axis the mark belongs to. Element-counting checks saw
   nothing wrong either way.

   It hangs to the LEFT of the line because the line spends the first `window` seconds of
   every run near the right-hand edge (measured: 97 % at T+10 s on a 5-minute window), where
   a right-anchored tag would be clipped by the value gutter. */
#chartCanvas .run-start { pointer-events: none; }
.run-start-tag {
  position: absolute; bottom: 1px; transform: translateX(-100%);
  padding: 0 3px; font-size: 8px; letter-spacing: .06em;
  color: var(--muted); background: var(--bg-strip); border-radius: 2px;
  pointer-events: none; white-space: nowrap;
}
.chart-legend .chart-meta.muted { font-style: italic; }
/* plot wrapper holds the stretched SVG + the floating value-label overlay */
.chart-plot { position: relative; flex: 1 1 auto; min-height: 0; overflow: visible; }
.chart-canvas { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.chart-floats { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.chart-trace { transition: opacity .14s ease; }
.chart-trace .chart-line, .chart-trace .chart-dot { transition: opacity .14s ease, stroke-width .14s ease; }
.chart-plot.chart-focus .chart-trace { opacity: .32; }
.chart-plot.chart-focus .chart-trace.hl { opacity: 1; }
.chart-plot.chart-focus .chart-trace.hl .chart-line { stroke-width: 2.2px !important; }
.chart-plot.chart-focus .chart-trace.hl .chart-dot { filter: drop-shadow(0 0 2px rgba(255, 255, 255, .35)); }
.cfloat {
  /* value indication sits in the right gutter, to the RIGHT of the trace ends */
  position: absolute; right: 3px; left: auto; transform: translate(0, -50%);
  display: flex; align-items: center; gap: 3px;
  padding: 1px 5px 1px 3px; border-radius: 2px;
  background: rgba(8, 11, 14, .84);
  border-left: 2px solid var(--cf, var(--text-2));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .35);
  white-space: nowrap; overflow: hidden;
  pointer-events: auto; cursor: default;
  transition: opacity .14s ease, box-shadow .14s ease, background .14s ease;
  max-width: 42%;
}
.chart-plot.chart-focus .cfloat { opacity: .38; }
.chart-plot.chart-focus .cfloat.hl {
  opacity: 1; z-index: 2;
  background: rgba(10, 14, 18, .94);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .08), 0 0 10px color-mix(in srgb, var(--cf) 28%, transparent);
}
.cfloat.hot { background: rgba(12, 10, 8, .9); }
.chart-plot.chart-focus .cfloat.hl.hot { box-shadow: 0 0 0 1px rgba(255, 200, 120, .15), 0 0 10px rgba(255, 180, 80, .18); }
.cfloat-swatch {
  flex: 0 0 auto; width: 8px; height: 2px; border-radius: 1px;
  background: var(--cf, var(--text-2));
}
.cfloat-name {
  flex: 1 1 auto; min-width: 0;
  font: 500 9px/1.1 var(--font, system-ui, sans-serif);
  color: var(--cf, var(--text-2)); opacity: .92;
  overflow: hidden; text-overflow: ellipsis;
}
.cfloat-val {
  flex: 0 0 auto;
  font: 600 9px/1.1 ui-monospace, "Cascadia Mono", Consolas, monospace;
  color: var(--text-1); opacity: .95;
}
.scrubber { height: 14px; display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.scrubber .track { flex: 1 1 auto; height: 4px; background: #23272d; border-radius: 2px; position: relative; cursor: pointer; }
.scrubber .track:hover { background: #2b3138; }
/* The head marks the present: the chart always ends at LIVE (right edge). */
.scrubber .head { position: absolute; right: 0; top: -3px; width: 3px; height: 10px; background: var(--normal); border-radius: 2px; pointer-events: none; }
.scrubber .lbl { font-size: 10px; color: var(--muted); }

/* App-level feedback toast (save / load / file errors) */
.app-toast {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%) translateY(-6px);
  background: var(--bg-elevated); border: 1px solid var(--normal); border-radius: 6px;
  color: var(--text); font-size: 12px; padding: 7px 14px; z-index: 120;
  opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .5);
}
.app-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.app-toast.error { border-color: var(--alarm, #e5484d); color: var(--alarm, #e5484d); }

/* Two-press armed confirm (the SCRAM idiom, for destructive plant actions) */
button.armed { background: #3a1618 !important; color: #ff9d9d !important; border-color: #a13a3a !important; animation: pulse 1s ease-in-out infinite; }

.alarm-panel {
  flex: 0 0 33%; background: var(--bg-strip); border: 1px solid var(--border); border-radius: var(--radius);
  display: flex; flex-direction: column; min-width: 220px;
}
.alarm-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border); }
.alarm-head .t { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-2); font-weight: 700; }
.alarm-stack { flex: 1 1 auto; overflow: auto; padding: 6px; display: flex; flex-direction: column; gap: 5px; }
/* Under the diagram the alarm panel is wide and short, so a single column wastes the width
   and buries anything past the first two or three. Two columns, filled top-to-bottom in
   priority order (app.js sorts the stack), so the worst alarm is still the first thing read.

   TWO COLUMNS ONLY WHILE BOTH CAN HOLD A TILE (#713 pass 2). `1fr 1fr` is minmax(auto, 1fr):
   the columns cannot shrink below a tile's min-content, so a narrow panel did not reflow, it
   OVERFLOWED SIDEWAYS — measured with 18 alarms up, the stack's scrollWidth pinned at 376px
   against a clientWidth that kept falling, from 376px of panel down. That already bit at a
   1250px viewport (alarm panel 267.8px) before this pass took any width back. The measured
   per-column min-content is 179.3px ("Overtemperature Limit Approaching"); the track minimum
   is 184px rather than that, because an EXPLICIT px minimum does NOT grow to fit content the
   way the auto it replaces did — a platform whose font measures a little wider would overflow
   the track instead of widening it, and 4.7px buys that. The calc(50% - 2.5px) half of the
   max() is what CAPS it at two columns on a wide board, where a bare auto-fill would have
   found room for three at 1920px. Below 387px of panel it now drops to one column and scrolls
   vertically, which is what the stack does under load anyway. Swept 330-400px with 18 alarms
   up and the registry's widest label in every tile: zero overflowing tiles at every width. */
.app.pwr-synoptic .plant-area > .bottom-row > .alarm-panel > .alarm-stack {
  display: grid; gap: 5px; align-content: start;
  grid-template-columns: repeat(auto-fill, minmax(max(184px, calc(50% - 2.5px)), 1fr));
}
.app.pwr-synoptic .plant-area > .bottom-row > .alarm-panel > .alarm-stack > .alarm-empty {
  grid-column: 1 / -1;
}
.alarm-empty { color: var(--muted); font-size: 11px; padding: 10px; text-align: center; font-style: italic; }
.alarm-tile {
  display: flex; gap: 7px; align-items: flex-start; padding: 6px 8px; border-radius: 5px;
  background: var(--bg-elevated); border: 1px solid var(--border); position: relative;
}
.alarm-tile .bar { width: 4px; align-self: stretch; border-radius: 2px; background: var(--muted); flex: 0 0 auto; }
.alarm-tile { cursor: pointer; }
.alarm-tile .ack-chip {
  flex: 0 0 auto; align-self: center; font-size: 9px; font-weight: 700; letter-spacing: .06em;
  color: var(--text-2); border: 1px solid var(--border); border-radius: 4px; padding: 2px 5px;
}
.alarm-tile:hover .ack-chip { color: var(--text); border-color: var(--normal); }
.alarm-tile .body { flex: 1 1 auto; }
.alarm-tile .label { font-size: 12px; font-weight: 600; line-height: 1.2; }
.alarm-tile .meta { font-size: 10px; color: var(--text-2); margin-top: 2px; }
.alarm-tile .glyph { font-size: 13px; }
.alarm-tile.crit { border-color: rgba(229, 72, 77, 0.55); }
.alarm-tile.crit .glyph { color: var(--critical); }
.alarm-tile.crit .bar { background: var(--critical); }
.alarm-tile.warn .glyph { color: var(--caution); }
.alarm-tile.warn .bar { background: var(--caution); }
/* Only unacked critical tiles flash — warnings stay solid amber so a flood
   does not strobe the whole panel. */
.alarm-tile.unack.crit {
  animation: alarmCritFlash 0.9s steps(1, end) infinite;
}
@keyframes alarmCritFlash {
  0%, 100% { background: transparent; }
  50% { background: rgba(229, 72, 77, 0.18); }
}
/* (The `.alarm-tile.unack.crit` reduced-motion override that stood here is SUPERSEDED and was
   deleted at #740: the block at the end of this file re-states `animation: none` and replaces its
   inset ring with the 5px double outline. Leaving a dead override three lines from a comment
   insisting that placement is what matters was the confusion this change is arguing against.) */

/* ===================================================== right col: sim ctrls */
.sim-controls {
  flex: 0 0 auto; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 8px 10px; display: flex; flex-direction: column; gap: 8px;
}
.sim-top { display: flex; align-items: center; justify-content: space-between; }
.logo { font-weight: 800; letter-spacing: .01em; font-size: 15px; white-space: nowrap;
  color: var(--text); text-decoration: none; cursor: pointer; }
.logo:hover { color: var(--text); opacity: 0.82; text-decoration: none; }
.logo .a { color: var(--normal); }
.brand { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.logo-ver { font-size: 10px; font-weight: 600; color: var(--muted); white-space: nowrap; }
/* TEST-BUILD marker — shown only off the released channel, keyed off the
   html[data-channel] attribute shell.html sets from window.RD_CHANNEL. Hidden by
   DEFAULT and opted into per channel, so a missing or misspelled attribute leaves
   the released simulator unmarked rather than falsely branded. Sits in the header
   beside the version, which is the one piece of chrome always on screen. */
.logo-test { display: none; }
html[data-channel="preview"] .logo-test,
html[data-channel="dev"] .logo-test {
  display: inline-block;
  background: var(--caution); color: #17120A;
  border-radius: 999px; padding: 1px 7px;
  font-size: 9px; font-weight: 700; letter-spacing: 1.2px; white-space: nowrap;
}
.clock { font-size: 15px; font-weight: 600; }
/* THE RUNNING CLOCK DOES NOT FADE (#687 item 1, OWNER 2026-09-09: "Other UI elements like the
   time keep doing the same [appearing and disappearing]"). It carried
   `animation: pulse 2s ease-in-out infinite`, i.e. opacity 1.0 -> 0.6 -> 1.0 for ever while the
   plant ran — MEASURED in headless Edge over a 50 s walkthrough ride: 576 opacity transitions on
   #clock, cycling 1.00 / 0.87 / 0.84 / 0.81 / 0.77 / 0.75 / 0.73 / 0.70 and back. That is an
   element appearing and disappearing, permanently, in peripheral vision — the exact thing this
   file's own instrAttnPulse note calls out as "fatiguing over an hour and stops being noticed",
   and it had no prefers-reduced-motion escape either.
   A steady colour carries the same fact with no motion: green is already the board's "running"
   (.play uses var(--running)), and .accel's amber still wins below by source order. The digits
   advancing are the primary cue and always were. */
.clock.running { color: var(--running); }
.clock.accel { color: var(--caution); }
@keyframes pulse { 50% { opacity: .6; } }
.sim-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.play {
  width: 40px; height: 30px; border-radius: 6px; border: 1px solid var(--border);
  background: var(--bg-right); color: var(--running); font-size: 15px; cursor: pointer; flex: 0 0 auto;
}
.play.paused { color: var(--caution); }
/* First-run cue: pulse the Play button until the sim is started for the first time. */
.play.attention {
  border-color: var(--running); color: var(--running);
  animation: playPulse 1.4s ease-in-out infinite;
}
@keyframes playPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(64, 209, 122, 0.55); }
  50%      { box-shadow: 0 0 0 6px rgba(64, 209, 122, 0); }
}
@media (prefers-reduced-motion: reduce) { .play.attention { animation: none; box-shadow: 0 0 0 2px rgba(64,209,122,0.5); } }
/* ONE ROW, STATUS STRIP BELOW, TOOLS ABOVE (owner, 2026-09-08, #660 — the two-row form of #655
   lasted a day). The six rungs sit edge to edge in one strip; the status line is its own
   full-width row under it; Manual / Help / Feedback / Settings are the row above. */
.speed { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 5px; overflow: hidden; flex: 1 1 auto; }
.speed button { flex: 1 1 0; background: var(--bg-right); color: var(--text-2); border: none; border-right: 1px solid var(--border);
  padding: 4px 0; font-size: 11px; cursor: pointer; }
.speed button:last-child { border-right: none; }
/* SIX CONTROLS IN A 338 px ROW SINCE #689 ADDED MAIN MENU, and .sim-row wraps — so this is a
   measurement, not a taste. At the shipped padding the six came to 402 px of buttons plus 40 px
   of gaps against 338 px: the row WRAPPED and Main Menu dropped to a second line beside ⛶,
   which is "to the right of settings" in DOM order only. Tightened here and here ONLY (every
   other .btn in the app keeps 5px 9px / 12px) the six measure 299 px of buttons plus 20 px of
   gaps = 319 px, one row 27 px tall, 19 px of headroom.
   A SEVENTH NAMED TOOL WRAPS IT AGAIN — MEASURED (quality pass, 2026-09-11): adding one takes this
   row from 27 px to 55 px on TWO lines, [manual, help, feedback, settings, mainMenu] then [the new
   tool, demoBtn]. Note WHICH control drops: `flex-wrap` pushes the OVERFLOW to the end, so the ⛶
   end-cap goes below the row and the tool you added goes with it — never the one in the middle.
   That is also why the row-and-gap assertions this comment used to credit did NOT catch it: they
   ask whether Main Menu is beside Settings, and at seven controls it still is. `testMainMenuButton`
   now asserts the ROW's painted height against its tallest control, which reds for whichever
   control wraps; the original two still red on reverting the padding here (Main Menu itself drops,
   gap -120 px). Both injection-proven. */
.sim-tools { justify-content: flex-end; gap: 4px; }
.sim-tools .btn { padding: 5px 5px; font-size: 11px; }
.sim-tools .text-btn { padding: 4px 5px; font-size: 11px; }
/* the status strip (#686): the active checklist step's own wait + recommended rung, or the
   one exception that persists across clicks — the accumulator hold ("held at real time").
   Empty and hidden otherwise; min-height keeps the speed bar from reflowing when it is. */
.warp-info { min-height: 16px; margin: 4px 0 2px; padding: 2px 6px; font-size: 10.5px; line-height: 1.3; border-radius: 4px;
  color: var(--text-2); background: var(--bg-elevated, var(--bg-right)); border: 1px solid var(--border);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.warp-info.dropped { color: var(--danger, #ef4444); }
/* SELECTED SPEED: GREEN FILL, LIGHT-GREY RING *(OWNER, 2026-09-14, #755 item 14: "The time
   controls are confusing when they are cyan since the highlight is cyan as well. lets make the
   background of the selected time rate green and the boarder a light grey.")*.

   IT WAS A CYAN INSET RING, and cyan is the walkthrough's own colour on this very strip —
   `.ckl-speed-rung` rings the RECOMMENDED rung in `rgba(90, 240, 255, …)` two rules below, so
   "the rate you are on" and "the rate the walkthrough wants" differed by animation and a few
   points of hue. Selection is now the only thing on the strip that FILLS, which is a channel
   the cue does not use at all.

   THE FILL IS SEMI-TRANSPARENT, not `var(--running)` flat. The two warp rungs keep amber LABELS
   (`.speed button.warp`, below) and amber on saturated green is unreadable; at this alpha over
   `--bg-right` the label sits on #24402f and both the amber and the plain text clear contrast.
   The amber `.warp.on` ring below is left alone deliberately — the tier reading is a separate
   claim from "this one is selected", and the fill now carries the selection on its own. */
.speed button.on { background: rgba(70, 163, 94, 0.42); color: var(--text); font-weight: 700; box-shadow: inset 0 0 0 1px rgba(200, 208, 216, 0.85); }
/* The two WARP rungs (#625): a coarser physics step for long quiet evolutions. Visibly a
   different tier, and DARK while the plant is in a transient — the service refuses them then,
   so a lit button that did nothing would read as broken. */
.speed button.warp { color: var(--caution); letter-spacing: .02em; }
.speed button.warp.on { box-shadow: inset 0 0 0 1px var(--caution); }
.speed button.warp.locked { color: var(--text-3, #6b7280); opacity: .45; cursor: not-allowed; }
/* THE RECOMMENDED RUNG, AND ONLY IT *(OWNER, 2026-09-13 playtest, #743: "instead of highlighting
   all the speed controls, just highlight the one that is suggested.")*. Applied by
   `applyCklSpeedGlow` in ui/app.js when the active walkthrough step asks for a hold of 180 s or
   more; the strip itself no longer lights.

   ⚠ INSET, AND THAT IS THE WHOLE REASON THE OLD VERSION GLOWED THE STRIP. `.speed` is
   `display: inline-flex` with `overflow: hidden` (above), so an OUTER box-shadow on a child button
   is clipped by the strip and `position: relative` + a z-index does not escape a clip — the bar was
   lit as a fallback because a ring on `button[data-speed="600"]` barely showed. An INSET shadow
   paints inside the button's own border box and is never clipped, which is why `.speed button.on`
   could already use one. Lifting the clip was the alternative and was not taken: it exists to round
   the six square-cornered rungs into the strip's 5px radius.

   NO `z-index` HERE, deliberately. `run_glow_stacking` collects every glow class in this file that
   lifts a z-index and requires a matching `z-index: auto` pin in pwr_board.css; this one is a shell
   button in a flex row, never a board tile, and an inset needs no lift to be seen. Adding one would
   make the gate demand a board entry for a class the board never sees.

   ONCE PRESSED IT STOPS PULSING — see `.ckl-speed-rung.on` below. */
.speed button.ckl-speed-rung {
  animation: cklRungGlow 1.2s ease-in-out infinite;
  color: var(--text); font-weight: 700;
}
@keyframes cklRungGlow {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(90, 240, 255, 0.70), inset 0 0 9px 1px rgba(90, 240, 255, 0.30); }
  50%      { box-shadow: inset 0 0 0 2px rgba(90, 240, 255, 1), inset 0 0 15px 3px rgba(90, 240, 255, 0.58); }
}
/* THE PULSE IS "ACT ON THIS", SO IT MUST END WHEN THE ACT IS DONE — otherwise the rung the player
   pressed first goes on pulsing for the whole 180 s-plus hold it was asking for, which is most of
   the time the cue is up, and a cue that keeps firing after it is satisfied is how the player learns
   to stop reading cues. Handled in CSS, not JavaScript, so it follows `.on` the instant the strip
   re-renders — no second code path to keep in step with the first.

   IT DOES NOT DE-ESCALATE TO A STEADY CYAN RING, because `.on` ALREADY IS one (`inset 0 0 0 1px
   var(--normal)`, and --normal is cyan): a "you did it" ring would have been indistinguishable from
   ordinary selection. So the treatment simply stands down and lets selection speak, with the
   `.warp-info` line under the strip still naming the rung in words. A selected WARP rung keeps its
   amber `.on` ring and its amber label, so the tier reading is not overwritten either. */
.speed button.ckl-speed-rung.on { animation: none; font-weight: 700; }
/* Achieved-rate readout (#581, folded into #625): what the plant actually does per wall
   second. Green = keeping up, amber = behind the request, red = well behind or stalling. */
.ff-rate { flex: 0 0 auto; font-size: 11px; padding: 3px 6px; border-radius: 4px; border: 1px solid var(--border); color: var(--text-2); }
.ff-rate.ok   { color: var(--running); border-color: rgba(34,197,94,.35); }
.ff-rate.warn { color: var(--caution); border-color: rgba(245,158,11,.4); background: rgba(245,158,11,.08); }
.ff-rate.bad  { color: var(--danger, #ef4444); border-color: rgba(239,68,68,.45); background: rgba(239,68,68,.08); }
/* DROPOUT FLASH *(OWNER, 2026-09-03, #619 item 7: "when dropping out of warp, flash the warp
   buttons for a moment to make it more obvious.")*. Added by app.js for ~1.6 s whenever the
   snapshot carries a `speed_snap`, so it marks every automatic dropout — a scram, a failure,
   a first alarm, or a checklist step completing.

   FINITE, not infinite like .play.attention: that one is a standing "you have not started yet"
   cue, this one reports a thing that just happened and must stop on its own. Three pulses.

   The glow goes on the CONTAINER, not the buttons: the buttons carry .on as an inset outline
   and an outer box-shadow on a flex child inside an overflow:hidden strip is clipped. Same
   colour family as the clock's accel warning, so the two read as one idea. */
.speed.speed-snapped { animation: speedSnap 0.55s ease-out 3; }
@keyframes speedSnap {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0); border-color: var(--border); }
  40%      { box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.45); border-color: var(--caution); }
}
@media (prefers-reduced-motion: reduce) {
  .speed.speed-snapped { animation: none; box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.5); border-color: var(--caution); }
}
.text-btn { flex: 0 0 auto; padding: 4px 10px; font-size: 12px; font-weight: 600; }
.demo-btn { flex: 0 0 auto; min-width: 28px; padding: 4px 8px; }

/* THE .sim-status BAR IS GONE (#689, owner 2026-09-09: "Add a Main Menu button to the right of
   settings. Change the SELECT PLANT, MISSION & RESET menu to this button and get rid of the old
   button."). It was a full-width bar under the speed controls with a 9 px .ss-lbl, a live
   .ss-txt readout and a .ss-chev; #mainMenuBtn wears the plain .btn .text-btn of the tools row
   it now sits in, so it needs no rules of its own, and the readout was dropped (option A). */
/* The pointer back to that button, for a few seconds after the window is closed. Was .ss-tip;
   renamed with the button at #689, and it now sits directly under .sim-tools so its ▲ points
   at the control it names instead of three rows past it.
   THE VERTICAL MOVE WAS ONLY HALF OF IT (quality pass, 2026-09-11). The bubble is full-row
   width and the old ▲ was the first glyph of a CENTRE-ALIGNED line, which was right under the
   full-width .sim-status bar it used to sit below and is wrong under a six-button row: MEASURED
   at a 1500 px viewport, arrow centre x 1252.7 against a Main Menu box of 1380.2–1449.0 — 128 px
   off, pointing at the middle of the speed bar. So the glyph leaves the text flow and is aimed
   from --mm-arrow-x, which aimMainMenuTip() sets from the button's own measured centre each time
   the tip is shown. Aimed rather than hard-coded because the row is not fixed: ⛶ is the end-cap,
   and #689's own note says a seventh named tool re-lays the row out.
   Its own strip above the text (padding-top 13px) rather than inline, so the arrow can never
   land on top of the centred sentence at a narrow panel width. */
.mm-tip {
  position: relative; margin: 2px 0 4px; padding: 13px 8px 4px;
  background: rgba(91, 179, 196, .12); border: 1px solid var(--normal); border-radius: 5px;
  color: var(--text); font-size: 11px; line-height: 1.35; text-align: center;
  animation: mmTipIn .18s ease-out;
}
.mm-tip .mm-arrow {
  position: absolute; top: 1px; left: var(--mm-arrow-x, 50%); transform: translateX(-50%);
  font-size: 10px; line-height: 1; color: var(--normal);
}
.mm-tip[hidden] { display: none; }
@keyframes mmTipIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .mm-tip { animation: none; } }

/* Help / tour — reuses the mission-modal chrome, narrower and content-sized */
.help-modal { width: min(560px, 96vw); height: auto; max-height: min(84vh, 760px); }
.help-body { padding: 14px 18px; overflow: auto; }

/* About docs (#259) — Disclaimer / License / Changelog. Wider and scrollable so
   the full changelog fits; content is packed HTML from the site pages. */
.mission-modal.doc-modal {
  width: min(720px, 96vw); height: min(84vh, 800px);
}
.doc-body {
  flex: 1; min-height: 0; overflow: auto;
  padding: 14px 18px 22px;
  font-size: 13px; line-height: 1.55; color: var(--text);
}
.doc-body h2 {
  margin: 1.1em 0 0.4em; font-size: 13px; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-2); border: none; padding: 0;
}
.doc-body h2:first-child { margin-top: 0; }
.doc-body p { margin: 0 0 0.75em; }
.doc-body ul { margin: 0 0 0.9em; padding-left: 1.25em; }
.doc-body li { margin-bottom: 0.4em; }
.doc-body a { color: var(--normal); }
.doc-body code {
  font-family: var(--mono, ui-monospace, monospace); font-size: 12px;
  background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;
}
.doc-body .notice, .doc-body .doc-alpha {
  background: rgba(217, 164, 65, 0.10); border: 1px solid var(--caution);
  border-radius: 6px; padding: 10px 12px; margin: 0 0 14px;
  color: var(--text-2); font-size: 13px;
}
.doc-body .notice strong, .doc-body .doc-alpha { color: var(--caution); font-weight: 600; }
.doc-body .lede { color: var(--text-2); margin-bottom: 1em; }
.doc-body .log-entry {
  border-top: 1px solid var(--border); padding-top: 16px; margin-top: 18px;
}
.doc-body .log-entry:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }
.doc-body .log-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.doc-body .log-head h2 {
  margin: 0; font-size: 14px; letter-spacing: 0; text-transform: none; color: var(--text);
}
/* The packed changelog is changelog.html verbatim (tools/pack_site_docs.js), so it
   arrives as <details class="log-entry"><summary class="log-head"> — collapsible in
   the About → Changelog modal exactly as on the site. `display:flex` above kills the
   default disclosure triangle in every engine, so draw one. */
.doc-body summary.log-head { cursor: pointer; list-style: none; flex-wrap: nowrap; }
.doc-body summary.log-head h2 { flex: 1 1 auto; min-width: 0; }
.doc-body summary.log-head .log-ver,
.doc-body summary.log-head .log-date { flex: 0 0 auto; }
.doc-body summary.log-head::-webkit-details-marker { display: none; }
.doc-body summary.log-head::before {
  content: '▸'; flex: 0 0 auto; color: var(--muted); font-size: 13px; line-height: 1;
  transition: transform 0.12s; transform-origin: 50% 45%;
}
.doc-body details[open] > summary.log-head::before { transform: rotate(90deg); }
.doc-body summary.log-head:hover h2 { color: var(--normal); }
.doc-body .log-note { color: var(--muted); font-size: 12px; margin: 0 0 12px; }
.doc-body .log-ver {
  font-size: 11px; font-weight: 700; letter-spacing: 0.4px; color: var(--normal);
  border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px;
}
.doc-body .log-date { color: var(--muted); font-size: 12px; margin-left: auto; }
.doc-body .log-entry ul { margin: 10px 0 0; padding-left: 0; list-style: none; }
.doc-body .log-entry li { margin-bottom: 8px; font-size: 13px; line-height: 1.5; }
.doc-body .log-tag {
  display: inline-block; min-width: 56px; margin-right: 6px;
  font-size: 10px; letter-spacing: 0.5px; text-transform: uppercase;
  text-align: center; padding: 1px 5px; border-radius: 3px;
  border: 1px solid currentColor; vertical-align: 1px;
}
.doc-body .log-tag.added { color: var(--running, #79d297); }
.doc-body .log-tag.changed { color: var(--normal); }
.doc-body .log-tag.fixed { color: var(--caution); }
/* Simulator performance readout (Physics tab). Deliberately plain: it is a diagnostic,
   not a dashboard, and colour here competes with the board where colour means something. */
.perf-panel { margin-top: 4px; }
.perf-verdict {
  font-size: 12px; line-height: 1.45; padding: 7px 9px; margin-bottom: 6px;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg-elevated); color: var(--text-2);
}
.perf-verdict.ok   { border-color: var(--running); }
.perf-verdict.warn { border-color: var(--caution); }
.perf-verdict.bad  { border-color: var(--caution); color: var(--text); }
.perf-rows .set-row .k { text-transform: none; letter-spacing: 0; cursor: help; }

.set-about-btns { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.help-sec { margin-bottom: 14px; }
.help-sec h4 { margin: 0 0 4px; font-size: 12px; letter-spacing: .05em; text-transform: uppercase; color: var(--text-2); }
/* ---- Coach-mark tour (spotlight + tip beside the target) ---- */
.tour-root {
  position: fixed; inset: 0; z-index: 9500; pointer-events: none;
}
.tour-root:not([hidden]) { display: block; }
.tour-scrim {
  position: fixed; inset: 0; pointer-events: auto;
  /* Dim is carried by the spot's box-shadow so the target stays clear */
  background: transparent;
}
.tour-spot {
  position: fixed; pointer-events: none;
  border: 2px solid var(--normal);
  border-radius: 8px;
  box-shadow:
    0 0 0 2px rgba(91, 179, 196, 0.35),
    0 0 0 9999px rgba(0, 0, 0, 0.58);
  transition: top 0.15s ease, left 0.15s ease, width 0.15s ease, height 0.15s ease;
}
.tour-tip {
  position: fixed; pointer-events: auto;
  width: min(320px, calc(100vw - 24px));
  background: var(--bg-elevated);
  border: 1px solid var(--normal);
  border-radius: var(--radius);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  padding: 12px 14px 12px;
  color: var(--text-2);
  font-size: 13px; line-height: 1.5;
  z-index: 9501;
}
.tour-tip-title {
  margin: 0 0 6px; font-size: 13px; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text);
}
.tour-tip-body p { margin: 0 0 0.5em; }
.tour-tip-body p:last-child { margin-bottom: 0; }
.tour-tip-nav {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border);
}
.tour-tip-nav .btn { font-size: 12px; padding: 4px 10px; }
.tour-prog { color: var(--muted); font-size: 11px; margin-right: auto; }
/* Raised above the dim while a tour step points at it */
.tour-target-live {
  position: relative;
  z-index: 9502 !important;
}
.instr-idle { font-size: 12px; line-height: 1.45; color: var(--text-2); }
.instr-idle-lead { margin: 0 0 8px; color: var(--text); }
.instr-idle-list { margin: 0 0 10px; padding-left: 1.2em; }
.instr-idle-list li { margin-bottom: 4px; }
.instr-idle-more { margin: 0; font-size: 11px; color: var(--muted); }
.btn.linkish {
  display: inline; padding: 0; border: none; background: none;
  color: var(--normal); font-size: inherit; font-weight: 600; cursor: pointer;
  text-decoration: underline;
}
.btn.linkish:hover { color: var(--text); }
.persona .icon:empty { display: none; }
.persona .icon:not(:empty) {
  width: 22px; height: 22px; border-radius: 4px; background: var(--bg-elevated);
  border: 1px solid var(--border); font-size: 11px; display: flex;
  align-items: center; justify-content: center; color: var(--text-2);
}
.help-sec p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text); }

/* Feedback overlay (reuses the help modal chrome). No longer a form — it carries
   the address to write to plus the optional diagnostics download. */
.fb-field { margin-bottom: 12px; }
.fb-field label { display: block; font-size: 12px; letter-spacing: .05em; text-transform: uppercase; color: var(--text-2); margin-bottom: 4px; }
.fb-hint { font-size: 12px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
/* In-sim bug report (2026-08-07). The send block is `hidden` in the markup and only
   revealed when a deploy stamped a telemetry endpoint, so a local checkout and the
   offline build show the email route alone rather than a button that cannot work. */
/* (`.fb-warn`, the restricted-information notice's caution-accented box, and `.fb-ver`, the build
   stamp's margin, were REMOVED with their markup on 2026-09-14 — #755 item 5. Both were one-use
   rules; ui/shell.html carries the reason where each element stood.) */
.lc-fb { margin-top: 10px; }
.fb-send { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 14px; }
.fb-note {
  width: 100%; resize: vertical; min-height: 68px;
  background: var(--bg-elevated); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; font: inherit; font-size: 13px; line-height: 1.45;
}
.fb-note:focus { outline: none; border-color: var(--normal); }
.fb-attach {
  display: flex; align-items: center; gap: 7px; margin: 8px 0;
  font-size: 12px; color: var(--text-2); text-transform: none; letter-spacing: 0;
}
.fb-send-btn { border-color: var(--running); }
.fb-send-btn:disabled { opacity: .5; cursor: default; }

/* The first-launch consent modal's rules (.consent-modal / .consent-btns / .consent-yes)
   were removed with the prompt itself, 2026-08-09 — see the note in ui/shell.html where the
   overlay stood. Nothing else used them; the Settings toggle uses the ordinary segment
   control. */
.fb-msg { font-size: 12px; color: var(--running); }
.fb-msg.err { color: var(--caution); }
.fb-lead { margin: 0 0 10px; font-size: 13px; line-height: 1.5; color: var(--text); }
/* The address is the point of this panel, so it gets the size — and it wraps
   rather than overflowing on a narrow window. */
.fb-addr { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.fb-mail { font-size: 17px; color: var(--running); text-decoration: none; border-bottom: 1px dotted var(--running); word-break: break-all; }
.fb-mail:hover { color: var(--text); border-bottom-color: var(--text); }
.fb-diag { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.help-keys { font-size: 11px; color: var(--text-2); }

/* ===================================================== right col: instructor */
/* Cards focus model (§ quiet-board "management by exception"): exactly one of
   {instructor, tools} is EXPANDED (fills the column), the other COLLAPSED to a
   minimal header. Collapsed instructor shows only its latest message; collapsed
   tools shows only its tab strip. Clicking a collapsed card's header — or any
   tool tab — brings it into focus (and collapses the other). */
.instructor, .tools {
  min-height: 0; display: flex; flex-direction: column;
  transition: flex-grow .15s ease;
}
.instructor.expanded, .tools.expanded { flex: 1 1 0; }
.instructor.collapsed, .tools.collapsed { flex: 0 0 auto; }
/* the whole collapsed instructor is a click target to maximize (persona is hidden in chat-mode) */
.instructor.collapsed { cursor: pointer; }
.instructor .persona { cursor: pointer; }
/* #237 attention cue: unseen-content badge + a brief pulse while collapsed —
   the instructor cues instead of stealing the column. Role takes margin-right:auto
   so badge + minimize sit at the right edge of the header. */
.persona .instr-badge {
  background: var(--caution, #d9a441); color: #14181c;
  font-size: 10px; font-weight: 700; border-radius: 8px; padding: 1px 6px; line-height: 1.4;
}
.persona .instr-badge[hidden] { display: none; }
.persona .role { margin-right: auto; }   /* pushes badge + minimize to the right edge */
/* Explicit minimize — the old CSS ▸ chevron was easy to miss; this is a real button
   at the top-right of the instructor header. Hidden while collapsed (the whole card
   is already the expand target then). */
.instr-min {
  flex: 0 0 auto; width: 26px; height: 22px; padding: 0; margin: 0;
  border: 1px solid var(--border); border-radius: 5px;
  background: var(--bg-elevated); color: var(--text-2);
  font-size: 16px; font-weight: 700; line-height: 1; cursor: pointer;
}
.instr-min:hover { color: var(--text); border-color: var(--text-2); background: color-mix(in srgb, var(--bg-elevated) 70%, var(--text-2)); }
/* FULLY MINIMIZED *(OWNER DIRECTIVE, 2026-08-04: "Need button to be able to fully minimize
   instructor block.")*, #350 item 19. `collapsed` still spends a header plus a one-line
   message preview on the column; `mini` spends the header alone, which is the least that can
   still carry the unseen-content badge — and that badge is the whole reason not to remove the
   card outright, since it is the only cue that the instructor has said something.
   The minimize button therefore steps expanded -> collapsed -> mini and stays reachable in
   all three, rather than hiding itself the moment the card collapses. */
.instructor.mini .instr-body,
.instructor.mini .instr-nav,
.instructor.mini .instr-ack { display: none; }
.instructor.mini .persona { padding-top: 2px; padding-bottom: 2px; }
/* NEW-MESSAGE CUE — CYAN, and bounded (#439, spec §4).
   It was AMBER. On a plant board amber is a plant condition, so the Instructor
   announcing itself read as the plant announcing something — the one colour the cue
   must not borrow. Cyan is the only board colour that is not a state.
   Two cycles, then a steady marker: indefinite motion in peripheral vision is
   fatiguing over an hour and stops being noticed, which spends the signal. */
@keyframes instrAttnPulse {
  0%, 100% { box-shadow: none; }
  45% { box-shadow: inset 0 0 0 2px var(--normal, #5BB3C4); }
}
.instructor.instr-attn { animation: instrAttnPulse 1.1s ease 2; }
/* Reduced motion: the signal must be FULLY legible with no motion at all — motion is
   an accelerator here, never the only carrier. The steady ring is what the animation
   settles to anyway, so this is the same cue minus the flash phase. */
@media (prefers-reduced-motion: reduce) {
  .instructor.instr-attn { animation: none; box-shadow: inset 0 0 0 2px var(--normal, #5BB3C4); }
}
/* Open, but scrolled away from the newest line (spec §4) — a quiet marker inside the
   panel, not a flash on a header the player is already looking at. */
.instr-newbelow {
  display: block; width: 100%; margin-top: 4px; padding: 2px 6px;
  background: transparent; border: 1px solid var(--normal); border-radius: 4px;
  color: var(--normal); font: inherit; font-size: 11px; cursor: pointer; text-align: center;
}
.instr-newbelow[hidden] { display: none; }
.instructor.collapsed .instr-prev,
.instructor.collapsed .instr-nav,
.instructor.collapsed .instr-ack { display: none; }
.instructor.collapsed .instr-current {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* COLLAPSED MEANS ONE LINE, and the free-play content is not one line. `.collapsed` is
   `flex: 0 0 auto` — size to content — and it relied on the body being a text node that
   `white-space: nowrap` could ellipsize. The idle block is BLOCK content (the checklist
   launcher and the numbered orientation list), which nowrap does not touch, so a collapsed
   card still stood 374 px tall and squeezed the tab it had just handed the column to.
   Measured: tools 205 px against an instructor 374 px, on a card marked collapsed. */
.instructor.collapsed .instr-launch,
.instructor.collapsed .instr-idle { display: none; }
.tools.collapsed .tab-body { display: none; }
/* #237: a scenario that declares ui_policy.failures:'locked' owns the Failures
   tab — visible but inert, with the note explaining why. */
.tabpane.fail-locked #failList, .tabpane.fail-locked .advanced-exp,
.tabpane.fail-locked #advFailPanel { pointer-events: none; opacity: 0.45; }
.fail-locked-note {
  font-size: 12px; color: var(--caution, #d9a441); padding: 6px 8px; margin-bottom: 6px;
  border: 1px solid var(--border); border-radius: 6px; background: var(--bg-elevated);
}
.instructor .persona { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
.persona .icon { width: 26px; height: 26px; border-radius: 50%; background: var(--bg-elevated); border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 14px; }
.persona .role { font-size: 11px; letter-spacing: .06em; color: var(--text-2); font-weight: 700; }
.instr-body { flex: 1 1 auto; padding: 10px; overflow: auto; }
.instr-prev { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
.instr-current { color: var(--text); font-size: 13px; }
.instr-standby { color: var(--muted); font-style: italic; }
.instr-nav { display: flex; gap: 6px; padding: 0 10px 8px; }
.instr-nav[hidden] { display: none; }
.instr-nav .btn { padding: 4px 8px; }
.instr-ack { margin: 0 10px 10px; display: flex; gap: 6px; align-items: stretch; }
.instr-ack[hidden] { display: none; }
.instr-ack .btn { flex: 0 0 auto; }
/* The primary "I've read this, continue" action (M8 §9) — a real, live control. */
.ack-btn { flex: 1 1 auto; padding: 8px; background: var(--bg-elevated); border: 1px solid var(--normal);
  border-radius: 6px; color: var(--normal); font-weight: 700; cursor: pointer; }
.ack-btn:hover { background: color-mix(in srgb, var(--normal) 12%, var(--bg-elevated)); }

/* ===================================================== right col: tools block */
.tabbar { display: flex; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.tabbar button { flex: 1 1 0; background: transparent; border: none; color: var(--text-2);
  padding: 8px 4px; font-size: 11px; cursor: pointer; border-bottom: 2px solid transparent; }
.tabbar button:hover { color: var(--text); }
.tabbar button.on { color: var(--text); border-bottom-color: var(--normal); }
/* fixed height (fills the tools box); overflowing tab content scrolls here */
.tab-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 10px; }
.tabpane { display: none; }
.tabpane.on { display: block; }

/* failures tab rows */
/* Inject Failure group heading *(OWNER DIRECTIVE, 2026-08-04: "organize the list of
   failures into logical groupings.")*. Same type treatment as the Physics tab's
   .phys-grp h4, because both lists are ordered on the same energy-path spine and
   should read as the same idea. No border: the .fail-row rules below already rule
   off every row, and a second line under the heading reads as an empty row. */
.fail-grp { font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
            color: var(--normal); margin: 12px 0 2px; }
.fail-grp:first-child { margin-top: 2px; }
.fail-row { padding: 7px 0; border-bottom: 1px solid #23272d; }
.fail-head { display: flex; align-items: center; gap: 8px; }
.fail-name { flex: 1 1 auto; font-size: 12px; }
/* category LABELS — low-saturation tinted background + brighter hue text (they
   classify a row; they are not warnings, so they don't get a saturated fill). */
.fail-cat { font-size: 9px; text-transform: uppercase; letter-spacing: .04em; padding: 1px 5px; border-radius: 3px; }
/* E4 — desaturated category pills (they classify a row; they are not status). */
.fail-cat.reactivity    { background: #180a18; color: #9050a0; }
.fail-cat.coolant       { background: #0a1820; color: #4a90c0; }
.fail-cat.power         { background: #180d00; color: #b07828; }
.fail-cat.instrument    { background: #141020; color: #6858a0; }
.fail-cat.safety_system { background: #0a1608; color: #508040; }
.fail-slider { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.fail-slider input[type=range] { flex: 1 1 auto; accent-color: var(--normal); }
.fail-slider .sv { font-size: 11px; color: var(--text-2); min-width: 96px; text-align: right; }
.fail-row.active { background: rgba(34,211,238,.05); }
.fail-row.active .fail-name { color: var(--normal); }
.m-filter { display: block; width: 240px; margin: 6px 0 10px; }
.advanced-exp { margin-top: 10px; font-size: 11px; color: var(--text-2); cursor: pointer; user-select: none; }
.advanced-exp:hover { color: var(--text); }
.advanced-exp.open { color: var(--text); }
/* Advanced instrument failure panel (Failures tab) */
.adv-fail { margin-top: 6px; padding: 8px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 6px; display: flex; flex-direction: column; gap: 6px; }
.adv-fail[hidden] { display: none; }
.adv-fail .row { display: flex; align-items: center; gap: 6px; }
.adv-fail .row .k { flex: 0 0 76px; font-size: 11px; color: var(--text-2); }
.adv-fail select { flex: 1 1 auto; min-width: 0; background: var(--bg-strip); color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 4px 6px; font-size: 12px; }
.adv-fail .num-input { width: 90px; }
.adv-fail .note { font-size: 10px; color: var(--muted); }
.adv-fail .actions { display: flex; gap: 6px; justify-content: flex-end; }
.adv-fail .active-list { font-size: 11px; color: var(--caution); }

/* physics tab — the true-state diagnostic overlay. One column, grouped along the
   energy path; reuses the numeric grid's row styling (.num-line/.nk/.nv). */
.phys-note { font-size: 10px; color: var(--muted); line-height: 1.5; margin: 0 0 10px; }
.phys-note b { color: var(--text-2); font-weight: 600; }
.phys-none { font-size: 11px; color: var(--muted); }
.phys-grp + .phys-grp { margin-top: 12px; }
.phys-grp h4 { margin: 0 0 4px; font-size: 10px; letter-spacing: .06em; color: var(--normal); text-transform: uppercase; }
.phys-grp .num-line { font-size: 12px; }
/* tabular figures: these numbers change every frame, and proportional digits make
   the whole column jitter as they do */
.phys-grp .nv { font-variant-numeric: tabular-nums; white-space: nowrap; }
.phys-grp .nk { padding-right: 8px; }

/* Physics values are BRIGHTER than the quiet-board default and carry the board's
   INDICATION scheme *(OWNER DIRECTIVE, 2026-08-04: "make the physics numbers brighter
   under the physics tab. The contrast is currently too low. Also make these physics
   numbers follow the indication color scheme (grey, green, yellow, red, etc.)")*.
   The generic `.num-line .nv` above paints every numeric grid in
   --clr-status-normal (#4a6070), which is ~2.5:1 on --bg-elevated — deliberate for
   the quiet board, and unreadable for a panel you are reading numbers off. These
   rules are SCOPED to .phys-grp so the quiet default survives everywhere else; the
   All view and the RBMK/BWR numeric grids are untouched.
     grey   #98A3AF  a quantity with no health criterion — most of the panel
     green  --running   a criterion that EXISTS and is currently satisfied
     amber  --caution   caution band
     red    --critical  alarm band
   Grey vs green is the distinction that carries the teaching: green is not
   decoration, it means "something is being checked here and it is fine", so a row
   that is merely informational must stay grey or green stops meaning anything. */
.phys-grp .nv           { color: var(--text-2); }
.phys-grp .nv.q-ok      { color: var(--running); }
.phys-grp .nv.q-caution { color: var(--caution); }
.phys-grp .nv.q-alarm   { color: var(--critical); }

/* PLOT COLUMN *(OWNER, 2026-08-08: "I would like a column to the left of the lables with a
   checkbox for the strip chart. when you check this box it puts this value on the chart.")*.
   A fixed-width leading column shared by the Physics and Indications lists, so both read as
   one table and the labels line up whether or not a given row can be plotted.
     .plot-cell   the column itself — FIXED width, never shrinks, so a row with no
                  checkbox (a composite verdict like "intact - 507 F to damage", which is
                  text and cannot be a trace) still indents its label to the same place.
     .ser-swatch  the trace colour, shown only once the row is actually plotted. Off, the
                  slot is empty rather than grey: a colour chip on an unplotted row reads
                  as "this is the colour it has", which is a promise the chart has not made
                  and cannot keep while six other traces are competing for the palette. */
.plot-cell { flex: 0 0 30px; display: flex; align-items: center; gap: 4px; }
.plot-cell input { accent-color: var(--normal); margin: 0; cursor: pointer; }
.plot-cell .ser-swatch { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.num-line .nk { flex: 1 1 auto; }

/* Indications tab — every plant channel, grouped on the Physics tab's energy-path spine and
   sharing its row styling on purpose: the two panes are the two halves of one split (what the
   instruments read / what the plant is doing) and they should read as one table with two
   pages. Values stay QUIET here rather than taking the Physics tab's green/amber/red: these
   are raw channel readings with no health criterion attached, and the board's own gauges and
   the alarm panel are where a reading gets judged. Colouring them would make green mean
   "listed" instead of "checked and fine", which is the distinction .phys-grp is built on. */
.ind-grp .num-line { font-size: 12px; }
.ind-grp .nk { padding-right: 8px; }
.ind-grp .nv { color: var(--text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Headers stay named while the list scrolls — it is ~100 rows on the PWR. */
.ind-grp h4 { margin: 0 0 4px; font-size: 9px; letter-spacing: .08em; color: var(--muted); text-transform: uppercase;
  position: sticky; top: 0; z-index: 1; background: var(--bg-panel, #12181f); padding: 3px 0 2px; }
/* ONE COLUMN *(OWNER DIRECTIVE, 2026-08-11: "Put indications in the indications tab into
   one column.")*. This SUPERSEDES the 2026-08-04 directive that asked for a second column
   when space allowed — and the reason that one has expired is that the rows changed under
   it. When that was written this was a list of 51 checkboxes with a label; since #439 every
   row carries a label, an indicated value, a true value and a divergence flag, so a 240 px
   track splits a row that needs the width. Two columns of truncated readings read worse
   than one column of complete ones.

   The sticky group headers stay, which is why this remains a grid rather than reverting to
   CSS `columns`: `position: sticky` does not work inside a multicol container. */
#indicationsList {
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
/* In a grid the first item of the SECOND column is not a first-child, so a sibling rule
   would leave it with a top border and the one beside it without. Uniform reads as a
   per-group rule instead of a mistake. */
#indicationsList > .ind-grp { border-top: 1px solid var(--border); padding-top: 5px; margin: 0 0 6px; }
#indicationsList > .ind-grp:first-child { border-top: none; padding-top: 0; }

/* settings tab rows */
.set-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #23272d; }
/* display:flex above beats the `hidden` ATTRIBUTE, so a row hidden from script
   (the #241 Features row) stays on screen unless this says otherwise. */
.set-row[hidden] { display: none; }
.set-row .k { font-size: 12px; color: var(--text-2); }

/* ================================================ System Scanner — a STATUS LINE
   #439 / spec §6. It was a panel at the bottom of the right column, two fixed
   heights (74 px / 28vh) with the choice persisted. Both are gone: the content is
   one sentence plus an affordance, and the panel sat at the maximum eye distance
   from the board it describes. It is now a single ~26 px line directly under the
   board, and the long form is a pausing modal.

   The FIXED height stays load-bearing for the same reason it always was (owner,
   2026-07-28: "the jumping up and down when moving the mouse over things is
   annoying") — one line, `nowrap`, ellipsis. A line that reflows to two while the
   pointer crosses the board would push the whole board up and down. */
.scanline {
  flex: 0 0 auto; height: 26px; display: flex; align-items: center; gap: 8px;
  padding: 0 8px; margin-top: 4px;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px;
  font-size: 12px; color: var(--text-2); line-height: 1;
}
.scanline-tag {
  font-size: 9px; letter-spacing: .12em; color: var(--muted);
  border-right: 1px solid var(--border); padding-right: 8px; flex: 0 0 auto;
}
.scanline-body {
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; overflow: clip; text-overflow: ellipsis;
  /* `overflow: hidden` here is doing real work — it is what makes the ellipsis happen — but
     it still made this a live SCROLLPORT (#723, sibling of #717): a single nowrap line's
     content box runs ~2px taller than its own line-height (font-metric rounding, not real
     content), and that 2px was a `scrollTop` an operator could set. `overflow: clip` keeps
     the ellipsis (verified: identical rendering, both compute to the same clipped glyph) and
     refuses the scroll write outright, same backstop shape as `.pwr-board-wrap`. */
}
/* EXPANDED: the line grows in place to show the whole description — no second box
   *(OWNER DIRECTIVE, 2026-08-11)* — and while expanded it is PINNED TO ONE HEIGHT
   *(OWNER DIRECTIVE, 2026-08-11: "Find a max height for the expanded scanner and pin it
   to that height when maximized. The height jumps around shifting everything when the
   player mouses over things.")*.
   
   The previous rule CLAIMED to be a fixed height in its own comment and was
   `height: auto; max-height: 190px` — content-sized, so every hover re-laid the board
   below it. 140px IS MEASURED, not chosen: across all 133 `data-scanner-hint` elements
   the tallest description renders 127px of content (p95 109, median 92, min 17), and
   140 = 127 + 10 padding + 2 border. So every description in the app fits with nothing
   clipped and nothing scrolling, and the height never changes while expanded.
   
   The 190 it replaces was 50% taller than the tallest thing that exists. The body keeps
   `overflow-y: auto` purely as a safety net for copy written later — if that scrollbar
   ever appears, the right fix is to re-run the measurement, not to raise this number. */
.scanline.expanded {
  height: 140px; align-items: flex-start; padding: 5px 8px; line-height: 1.45;
}
.scanline.expanded .scanline-tag { padding-top: 1px; }
.scanline.expanded .scanline-body {
  white-space: normal; overflow-y: auto; text-overflow: clip;
  max-height: 128px; overscroll-behavior: contain;
}
.scanline.expanded .scan-exp { position: sticky; top: 0; }
.scan-detail { display: block; margin-top: 5px; color: var(--text-2); }
.scan-meta {
  display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center;
  margin-top: 5px; font-size: 11px; color: var(--muted);
}
.scanline-body strong { color: var(--text); }
.scanline .idle { color: var(--muted); font-style: italic; }
.scan-exp {
  flex: 0 0 auto;
  background: transparent; border: 1px solid var(--border); border-radius: 4px;
  color: var(--text-2); font: inherit; font-size: 10px; letter-spacing: .06em;
  padding: 1px 6px; cursor: pointer; text-transform: uppercase;
}
.scan-exp:hover { color: var(--text); border-color: var(--text-2); }
.scan-exp[hidden] { display: none; }
.scan-brief { color: var(--text); font-size: 13px; margin: 0 0 10px; }
.scan-manual {
  background: transparent; border: none; padding: 0; cursor: pointer;
  color: var(--running); font: inherit; font-size: 11px;
}
.scan-manual:hover { text-decoration: underline; }
.scan-hint { font-style: italic; }
/* Live automation-channel status (#214) — what the control the pointer is over is
   ACTUALLY doing, as against the authored copy around it. Its own block so the two
   never read as one sentence: everything else in the scanner is fixed prose, this
   line changes under you. Monospace and dimmed in MANUAL, lit in AUTO, matching how
   the board itself distinguishes the two. */
.scan-live {
  display: block; margin-top: 5px; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 11px; letter-spacing: .02em; color: var(--muted);
}
.scan-live.on { color: var(--running); }
.scanline .scan-live { display: inline; margin-top: 0; margin-left: 8px; }
/* NO hover ring on the inspected object (owner, 2026-07-28) — the first cut drew
   one and it reads as noise, flickering across every control the pointer passes.
   The glows that remain (.instr-glow, .ckl-glow) point at something the player did
   NOT choose, which is the case that needs a marker. */

/* ===================================================== additions (wired UI) */
.num-input {
  background: var(--bg-plant); color: var(--text); border: 1px solid var(--border);
  border-radius: 4px; padding: 3px 6px; font-size: 12px; width: 64px;
}
select.num-input { width: auto; }

/* strip-chart low-profile x-axis (time ticks). The right 14% is a gutter for the
   live value chips (traces stop at CHART_PLOT_FRAC), so reserve it here too — the
   "0" (now) tick then lines up with each trace's end. */
.chart-xaxis { display: flex; justify-content: space-between; font-size: 9px; color: var(--muted);
  padding: 1px 14% 0 6px; line-height: 1.1; height: 12px; box-sizing: border-box; }

/* Automate tab (operator automation toggles) */
.auto-master { display: flex; align-items: center; justify-content: space-between; padding: 2px 0 8px; border-bottom: 1px solid #23272d; }
.auto-master .k { font-size: 12px; color: var(--text-2); }
.auto-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #23272d; }
.auto-tog { flex: 0 0 auto; width: 46px; padding: 4px 0; font-size: 10px; font-weight: 700; letter-spacing: .05em;
  border: 1px solid #3a4048; border-radius: 5px; background: transparent; color: var(--muted); cursor: pointer; }
.auto-tog.on { color: var(--running); border-color: var(--running-muted); background: rgba(70,163,94,.10); }
.auto-main { flex: 1 1 auto; min-width: 0; }
.auto-name { font-size: 12px; color: var(--text-2); }
.auto-row.on .auto-name { color: var(--text); }
.auto-read { font-size: 10px; color: var(--muted); margin-top: 1px; }
.auto-row.on .auto-read { color: var(--running); }
.auto-spbox { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
.auto-splbl { font-size: 9px; color: var(--muted); letter-spacing: .05em; }
.auto-sp { width: 68px; }
.auto-sp:disabled { opacity: .35; }
.auto-spunit { font-size: 10px; color: var(--muted); min-width: 18px; }

.g-section-title { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-2); margin: 0 0 6px; }

/* numeric placeholder — overlay/true tags + boolean states */
.true-tag { color: var(--caution); font-size: 11px; font-weight: 600; margin-left: 4px; }
.hidden-true { color: var(--disabled); font-style: italic; font-size: 11px; }
/* E2 — near-invisible readout (e.g. SUR at steady power: carries no information) */
.dim-info { color: #2c343d; }
.nv.bool-off { color: var(--text-2); }

/* failures-tab inject toggle */
.fail-toggle { background: var(--bg-elevated); color: var(--text-2); border: 1px solid var(--border);
  border-radius: 4px; padding: 2px 8px; font-size: 10px; cursor: pointer; flex: 0 0 auto; min-width: 48px; }
.fail-toggle:hover { color: var(--text); }
.fail-row.active .fail-toggle { background: var(--critical); color: #fff; border-color: var(--critical); }

/* ============================================================================
 * TABBED CONTROL STRIP — the four control sections are stacked behind a tab bar
 * so only ONE is shown at a time, spread across the FULL width of the plant
 * area in a single low row. This makes the control band much skinnier, freeing
 * vertical room for the synoptic and the chart/alarm row.
 *
 * NOTE: this deliberately departs from M8 §5 ("always visible — not tabs").
 * User-directed design change; logged as a deviation in BUILD_DECISIONS.
 * ========================================================================== */
.control-strip {
  flex: 0 0 auto;
  background: var(--bg-strip);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex; flex-direction: row; align-items: stretch;
  overflow: hidden;
}
/* the tabbed area takes the remaining width; SCRAM box is pinned to the right */
.ctl-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.scram-box {
  flex: 0 0 auto; width: 150px;
  border-left: 1px solid var(--border);
  display: flex; align-items: stretch; justify-content: center;
  padding: 8px;
}
.scram-box .scram-wrap { width: 100%; height: auto; min-height: 60px; }
.ctl-tabs { display: flex; border-bottom: 1px solid var(--border); }
.ctl-tabs button {
  background: transparent; border: none; color: var(--text-2);
  padding: 7px 16px; font-size: 12px; letter-spacing: .05em; text-transform: uppercase;
  font-weight: 700; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap;
}
.ctl-tabs button:hover { color: var(--text); }
.ctl-tabs button.on { color: var(--text); border-bottom-color: var(--normal); }

.ctl-panes { padding: 9px 10px; }
/* nowrap + a min-height floor → every tab's pane is the SAME height (one centered
   row of control groups), so the strip never resizes when you switch tabs. A pane
   with more groups than fit scrolls horizontally rather than growing taller. */
.ctl-pane { display: none; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
  gap: 8px 18px; align-items: center; min-height: 64px; }
.ctl-pane.on { display: flex; }

/* one control = a label-over-control group; groups flow left-to-right */
.cg { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; flex: 0 0 auto; }
.cg-l { font-size: 11px; color: var(--text-2); letter-spacing: .02em; line-height: 1.1; white-space: nowrap; }
.cg-controls { display: flex; align-items: center; gap: 6px; }
/* slightly larger control affordances in the strip */
.ctl-pane .seg button,
.ctl-pane .btn { font-size: 12px; padding: 5px 10px; }
.ctl-pane .num-input { font-size: 12px; padding: 3px 7px; width: 60px; }
.cg.emergency { border: 1px dashed var(--caution); border-radius: 5px; padding: 4px 8px 6px; }
.cg.emergency .cg-l { color: var(--caution); }

/* ============================================================================
 * QUIET-BOARD palette tokens (the board is now quiet-only — the old/“current”
 * look and its A/B toggle were removed). Color is reserved for deviation: the
 * gauge bars (single dim track + colored fill in-band), status words (dim
 * normal), value text/card tint by state, and the muted trace palette all key
 * off these. The state rules themselves live with their components above.
 * ========================================================================== */
:root {
  --clr-normal-value:    #a8b8c8;
  --clr-dim-label:       #62768a;   /* matches the diagram's component-label grey (.comp-label / .lbl-name) */
  --clr-status-normal:   #4a6070;
  --clr-caution:         #d4a020;
  --clr-alarm:           #e05030;
  --clr-cleared:         #40a060;   /* green — cleared alarms ONLY */
  --bar-track-normal:    #1a3040;
  --bar-warn:            #7a5000;
  --bar-alarm:           #7a2010;
  --gauge-bg-normal:     #0d1017;
  --gauge-bg-warn:       #0e0c00;
  --gauge-bg-alarm:      #0e0600;
  --gauge-border-normal: #1a2030;
  --gauge-border-warn:   #3a2800;
  --gauge-border-alarm:  #5a1800;
  --scram-bg-normal:     #0a1208;
  --scram-border-normal: #253a20;
  --scram-text-normal:   #304828;
}

/* SCRAM guard cover — present but dim when armed (the fired state is the
   existing bright-red scram-flash). */
.scram-cover {
  background: var(--scram-bg-normal);
  border: 1.5px solid var(--scram-border-normal);
  color: var(--scram-text-normal);
  font-weight: 600; letter-spacing: .1em;
}

/* ============================================================================
 * PLANT-DISPLAY layout (swappable variant — Dev tab -> "Plant Display").
 * Status bar + 4-view switcher (Diagram / Primary / Secondary / All) replacing
 * the classic tabbed control strip + synoptic table. Scoped so the classic board
 * is untouched under layout-classic. Hexes per the redesign spec.
 * ========================================================================== */
:root {
  --status-bar-bg: #060910;
  --slot-dot-normal: #1a2a38; --slot-dot-caution: #c09020; --slot-dot-alarm: #e04020; --slot-dot-running: #3a9060;
  --slot-name-normal: #1e2e3e; --slot-name-caution: #a07818; --slot-name-alarm: #c03818; --slot-name-running: #306848;
  --slot-badge-alarm-bg: #1a0600; --slot-badge-alarm-color: #e04020; --slot-badge-alarm-border: #6a1808;
  --slot-badge-warn-bg: #1e1800; --slot-badge-warn-color: #c09020; --slot-badge-warn-border: #4a3800;
  --slot-badge-run-bg: #081a10; --slot-badge-run-color: #3a9060; --slot-badge-run-border: #1a4828;
  --view-btn-color: #2a4050; --view-btn-hover: #4a6878; --view-btn-active-color: #7aabb8; --view-btn-active-line: #3a7080;
  --cross-bg: #060910; --cross-label-color: #1a3040; --cross-name-color: #1e3040; --cross-val-color: #2e4a58;
  --section-head-color: #2a4050; --section-head-border: #0e1520;
}

/* system status bar */
.status-bar { flex: 0 0 auto; height: 22px; display: flex; align-items: center;
  background: var(--status-bar-bg); border: 1px solid var(--border); border-top: none; padding: 0 10px; overflow: hidden; }
.bar-sep { width: 0; align-self: stretch; margin: 4px 8px; border-left: 1px solid #131c28; }
.sys-slot { display: flex; align-items: center; gap: 4px; padding: 0 7px; }
.slot-dot { width: 4px; height: 4px; border-radius: 50%; flex-shrink: 0; background: var(--slot-dot-normal); }
.slot-name { font-size: 8px; text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; color: var(--slot-name-normal); }
.slot-badge { font-size: 7px; font-weight: 600; padding: 1px 4px; border-radius: 2px; letter-spacing: .04em; white-space: nowrap; display: none; }
.sys-slot.state-running .slot-dot { background: var(--slot-dot-running); }
.sys-slot.state-running .slot-name { color: var(--slot-name-running); }
.sys-slot.state-running .slot-badge { display: inline; background: var(--slot-badge-run-bg); color: var(--slot-badge-run-color); border: .5px solid var(--slot-badge-run-border); }
.sys-slot.state-caution .slot-dot { background: var(--slot-dot-caution); }
.sys-slot.state-caution .slot-name { color: var(--slot-name-caution); }
.sys-slot.state-caution .slot-badge { display: inline; background: var(--slot-badge-warn-bg); color: var(--slot-badge-warn-color); border: .5px solid var(--slot-badge-warn-border); }
.sys-slot.state-alarm .slot-dot { background: var(--slot-dot-alarm); }
.sys-slot.state-alarm .slot-name { color: var(--slot-name-alarm); }
.sys-slot.state-alarm .slot-badge { display: inline; background: var(--slot-badge-alarm-bg); color: var(--slot-badge-alarm-color); border: .5px solid var(--slot-badge-alarm-border); }
@keyframes status-flash { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .2; } }
.sys-slot.state-alarm .slot-dot, .sys-slot.state-alarm .slot-badge { animation: status-flash .8s step-end infinite; }
.scram-badge { margin-left: auto; font-size: 8px; font-weight: 700; padding: 2px 7px; border-radius: 2px;
  background: #1a0600; color: #e04020; border: .5px solid #6a1808; letter-spacing: .06em; animation: status-flash .5s step-end infinite; display: none; }
.scram-badge.on { display: inline-block; }

/* main area: vertical view switcher (left) + view stack (view-area + control bar) */
.main-area { flex: 1 1 auto; min-height: 0; display: flex; gap: var(--gap); }
.view-switcher { flex: 0 0 96px; display: flex; flex-direction: column; gap: 4px;
  background: #070a0f; border: 1px solid var(--border); border-radius: var(--radius); padding: 6px; }
.view-btn { font-size: 11px; font-weight: 500; color: var(--view-btn-color); padding: 9px 8px; background: transparent;
  border: none; border-left: 2px solid transparent; border-radius: 4px; cursor: pointer; text-align: left; letter-spacing: .03em; }
.view-btn:hover { color: var(--view-btn-hover); }
.view-btn.active { color: var(--view-btn-active-color); border-left-color: var(--view-btn-active-line); background: #0c1118; }
.view-stack { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: var(--gap); }

/* the view-area STRETCHES to fill; the active view fills it (no scroll for diagrams) */
.view-area { flex: 1 1 auto; min-height: 0; background: var(--bg-strip); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; display: flex; position: relative; }
.pdview { display: none; width: 100%; height: 100%; padding: 8px; }
.pdview.on { display: flex; flex-direction: column; }
/* the single child (diagram or grid) fills the panel */
.pdview > .pd-diagram, .pdview > .pd-sections, .pdview > .pd-all-grid { flex: 1 1 auto; min-height: 0; }
.pdview > .pd-sections, .pdview > .pd-all-grid { overflow: auto; }
.view-placeholder { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; color: var(--muted); }
.view-placeholder .placeholder-sub { font-size: 11px; color: #2a3a48; }

/* shared control bar (active view's controls + persistent SCRAM at the right) */
.pd-controlbar { flex: 0 0 auto; min-height: 58px; display: flex; align-items: center; gap: 14px;
  background: var(--bg-strip); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 10px; }
.pd-ctlrow { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; overflow-x: auto; }
.pd-scram { flex: 0 0 auto; align-self: stretch; font-size: 12px; font-weight: 700; letter-spacing: .1em; cursor: pointer;
  padding: 0 20px; min-width: 96px; border-radius: 5px; background: #0a1208; border: 1.5px solid #253a20; color: #304828; }
.pd-scram.armed { background: #7f2a2a; border-color: #9e3b3b; color: #f1d6d6; }
.pd-scram.fired { background: #1a0600; border: 2px solid #b03020; color: #e04020; animation: scram-flash .5s step-end infinite; }

/* param sections (RBMK/BWR Primary/Secondary) */
.pd-sections { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 18px; align-content: start; }
.pd-section h5 { margin: 0 0 5px; font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--section-head-color); border-bottom: 1px solid var(--section-head-border); padding-bottom: 3px; font-weight: 700; }
.pd-row { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; padding: 3px 0; border-bottom: 1px dotted #161d27; }
.pd-row .pk { color: var(--text-2); }
.pd-row .pv { font-weight: 600; color: var(--clr-status-normal); }
.pd-row .pv.q-caution { color: var(--caution); }
.pd-row .pv.q-alarm { color: var(--critical); }
.pd-row.subcool { padding: 6px 0; border-top: 1px solid #141b24; border-bottom: 1px solid #141b24; }
.pd-row.subcool .pv { font-size: 15px; }
.pd-row.subcool .pv.warn { color: var(--caution); }
.pd-row.subcool .pv.alarm { color: var(--critical); animation: gauge-alarm-flash .6s steps(1,end) infinite; }
.sat-badge { font-size: 8px; font-weight: 700; color: var(--critical); border: .5px solid #6a1808; background: #1a0600;
  border-radius: 2px; padding: 1px 4px; margin-left: 6px; letter-spacing: .05em; }

/* All-params view */
.pd-all-head { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.pd-all-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; align-content: start; }

/* ============================================================================
 * PWR primary-loop diagram (embedded SVG, wired to the live snapshot). Vars are
 * scoped to .pd-diagram so the schematic's palette (--text/--border/etc.) does
 * not clash with the shell's. Animations driven by --flow-dur/--spin-dur + the
 * .stopped class on #loop, set each frame from RCP flow.
 * ========================================================================== */
.pd-diagram { --pipe-case:#2a3744; --warm:#c98a5a; --cool:#6a9dc0; --dtext:#bcccd8; --ddim:#62768a; --vdim:#3a4e60;
  --tap:#507893; --leader:#36485a; --water:#1e455c; --rod:#4aa0c8;
  background: radial-gradient(ellipse at 42% 45%, #0c1019 0%, #070a0f 82%);
  border: .5px solid #243040; border-radius: 8px; padding: 4px; display: flex; min-height: 0; }
.pd-diagram svg.loop { width: 100%; height: 100%; display: block; }
@keyframes flow { to { stroke-dashoffset: -16; } }
@keyframes spin { to { transform: rotate(360deg); } }
.pd-diagram .pipe-case { fill: none; stroke: var(--pipe-case); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
.pd-diagram .pipe-case.thin { stroke-width: 7; }
.pd-diagram .flow { fill: none; stroke-width: 4; stroke-linecap: butt; stroke-dasharray: 5 11; animation: flow var(--flow-dur, 1.1s) linear infinite; }
.pd-diagram .tube-flow { fill: none; stroke-width: 3; stroke-linecap: butt; stroke-dasharray: 4 8; animation: flow var(--flow-dur, 1.1s) linear infinite; }
.pd-diagram .loop.stopped .flow, .pd-diagram .loop.stopped .tube-flow { animation-play-state: paused; }
.pd-diagram .vessel { fill: #10171f; stroke: #3a5870; stroke-width: 1.2; }
.pd-diagram .vessel-inner { fill: #0e141c; stroke: #28384a; stroke-width: .8; }
.pd-diagram .fuel { stroke: #34504e; stroke-width: 1.4; }
.pd-diagram .comp-label { fill: var(--ddim); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.pd-diagram .comp-sub { fill: var(--vdim); font-size: 9px; letter-spacing: .04em; }
.pd-diagram .water { fill: var(--water); opacity: .6; }
/* smooth the level/rod motion between broadcasts (10-20 Hz) so geometry updates
   interpolate instead of snapping — kills the per-frame jitter */
.pd-diagram .water, .pd-diagram .rod-fill { transition: y .16s linear, height .16s linear; }
.pd-diagram .surface { fill: none; stroke: #4e88a4; stroke-width: 1.4; opacity: .9; transition: d .16s linear; }
.pd-diagram .steam-space { fill: #121a22; }
.pd-diagram .valve { fill: #10171f; stroke: #4a6680; stroke-width: 1; }
.pd-diagram .pump-body { fill: #10161f; stroke: #3a5870; stroke-width: 1.2; }
.pd-diagram .pump-vane { stroke: #4a7088; stroke-width: 1.4; stroke-linecap: round; }
.pd-diagram #pumpRotor { transform-origin: 440px 405px; animation: spin var(--spin-dur, 1.1s) linear infinite; }
.pd-diagram .loop.stopped #pumpRotor { animation-play-state: paused; }
.pd-diagram .rod-track { fill: #0c121a; stroke: #2c3e4e; stroke-width: 1; }
.pd-diagram .rod-fill { fill: var(--rod); opacity: .85; }
.pd-diagram .rod-cap { fill: #5ab0d8; }
.pd-diagram .sec-arrow { fill: none; stroke: #46586a; stroke-width: 1.4; }
.pd-diagram .sec-label { fill: var(--vdim); font-size: 9.5px; letter-spacing: .04em; }
.pd-diagram .tap { fill: var(--tap); }
.pd-diagram .tap-tick { stroke: var(--tap); stroke-width: 1.2; }
.pd-diagram .leader { stroke: var(--leader); stroke-width: .8; stroke-dasharray: 2 2; fill: none; }
.pd-diagram .lbl-box { fill: #0e1620; stroke: #2c3e4e; stroke-width: .8; }
.pd-diagram .lbl-name { fill: var(--ddim); font-size: 9.5px; letter-spacing: .04em; text-transform: uppercase; }
.pd-diagram .lbl-val { fill: var(--dtext); font-size: 15px; font-weight: 600; }
.pd-diagram .lbl-val.derived { fill: #7aa4b8; }
.pd-diagram .lbl-unit { fill: var(--vdim); font-size: 9px; }
.pd-diagram .lbl-note { fill: var(--vdim); font-size: 8px; font-style: italic; }
.pd-diagram .tmi .lbl-box { stroke: #3a5030; }
.pd-diagram .tmi .lbl-name { fill: #7a8a4a; }

/* secondary-loop diagram additions (steam/turbine/condenser) */
.pd-diagram { --steam:#b4c0cc; --wet:#84a2b2; --cond:#5e92ac; --cw:#4e90ae; }
.pd-diagram .flow.steam-dash { stroke-dasharray: 7 9; }
.pd-diagram .turbine-blade { stroke: #4a7088; stroke-width: 1.2; }
@keyframes bladeSweep { to { transform: translateX(-15px); } }
.pd-diagram #secTurbineRotor { animation: bladeSweep var(--blade-dur, .5s) linear infinite; }
.pd-diagram #secCondPump { transform-origin: 700px 450px; animation: spin var(--spin-dur, 1s) linear infinite; }
.pd-diagram #secFeedPump { transform-origin: 320px 330px; animation: spin var(--spin-dur, 1s) linear infinite; }
.pd-diagram .loop.stopped #secTurbineRotor,
.pd-diagram .loop.stopped #secCondPump,
.pd-diagram .loop.stopped #secFeedPump { animation-play-state: paused; }

/* full-plant diagram (two flow domains: primary + secondary, paused independently) */
.pd-diagram .flow.sec { animation: flow var(--flow-dur-sec, 1.1s) linear infinite; }
.pd-diagram .tube-flow.sec { animation: flow var(--flow-dur-sec, 1.1s) linear infinite; }
.pd-diagram .loop.stopped-pri .flow.pri, .pd-diagram .loop.stopped-pri .tube-flow.pri { animation-play-state: paused; }
.pd-diagram .loop.stopped-sec .flow.sec, .pd-diagram .loop.stopped-sec .tube-flow.sec { animation-play-state: paused; }
.pd-diagram #fpRcp { transform-origin: 400px 355px; animation: spin var(--spin-pri, 1s) linear infinite; }
.pd-diagram #fpTurbineRotor { animation: bladeSweep var(--blade-dur, .5s) linear infinite; }
.pd-diagram #fpCondPump { transform-origin: 840px 450px; animation: spin var(--spin-sec, 1s) linear infinite; }
.pd-diagram #fpFeedPump { transform-origin: 720px 385px; animation: spin var(--spin-sec, 1s) linear infinite; }
.pd-diagram .loop.stopped-pri #fpRcp { animation-play-state: paused; }
.pd-diagram .loop.stopped-sec #fpTurbineRotor,
.pd-diagram .loop.stopped-sec #fpCondPump,
.pd-diagram .loop.stopped-sec #fpFeedPump { animation-play-state: paused; }

/* ============================ Operator's Manual overlay (Phase 3) ========= */
.manual-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(4, 7, 9, 0.78);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.manual-overlay[hidden] { display: none; }
.manual-modal {
  width: min(1100px, 96vw); height: min(88vh, 900px);
  display: flex; flex-direction: column;
  background: var(--bg-plant); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: 0 12px 48px rgba(0,0,0,0.6); overflow: hidden;
}
.manual-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--bg-elevated);
}
.manual-title { font-size: 15px; font-weight: 700; color: var(--text); }
.manual-head-ctl { display: flex; align-items: center; gap: 10px; }
.manual-body { flex: 1 1 auto; display: flex; min-height: 0; }
.manual-nav {
  flex: 0 0 172px; border-right: 1px solid var(--border); background: var(--bg-strip);
  padding: 8px; display: flex; flex-direction: column; gap: 2px; overflow-y: auto;
}
.manual-nav button {
  text-align: left; background: transparent; border: none; color: var(--text-2);
  padding: 8px 10px; font-size: 12.5px; border-radius: 5px; cursor: pointer;
}
.manual-nav button:hover { color: var(--text); background: var(--bg-elevated); }
.manual-nav button.on { color: var(--text); background: var(--bg-elevated); font-weight: 600; box-shadow: inset 2px 0 0 var(--running); }
.manual-content { flex: 1 1 auto; overflow-y: auto; padding: 18px 22px; color: var(--text); font-size: 13px; line-height: 1.55; }
.manual-content h2 { font-size: 16px; margin: 0 0 10px; color: var(--text); }
.manual-content h3 { font-size: 13.5px; margin: 18px 0 6px; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.04em; }
.manual-content p { margin: 0 0 10px; color: var(--text); }
.manual-content .muted { color: var(--muted); font-size: 12px; }
.manual-content .one-liner { font-size: 14px; color: var(--running); margin-bottom: 12px; }
.m-table { width: 100%; border-collapse: collapse; margin: 6px 0 14px; font-size: 12px; }
.m-table th, .m-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.m-table th { color: var(--text-2); font-weight: 600; }
.m-table td.mono, .m-table .mono { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--text-2); }
.m-card { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin: 0 0 10px; background: var(--bg-strip); }
.m-card .m-h { font-weight: 600; color: var(--text); margin-bottom: 2px; }
.m-card .m-sub { font-size: 11px; color: var(--muted); font-family: ui-monospace, Consolas, monospace; }
.m-step { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px dashed var(--border); }
.m-step .n { flex: 0 0 22px; height: 22px; border-radius: 50%; background: var(--bg-elevated); color: var(--text-2);
  display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
.m-step .m-act { color: var(--text); }
.m-step .m-note { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.m-step .m-cmd { font-family: ui-monospace, Consolas, monospace; font-size: 11px; color: var(--running-muted); }
.m-pill { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 10.5px; border: 1px solid var(--border); color: var(--text-2); margin-right: 6px; }
.m-prio-critical { color: #F26; border-color: #63232f; }
.m-prio-warning { color: #FBBF24; border-color: #5a4718; }
.m-prio-caution { color: #38BDF8; border-color: #1d4657; }
.m-prio-status { color: var(--text-2); }
.m-sim-note { border-left: 3px solid var(--running-muted); background: var(--bg-strip); padding: 8px 12px; margin: 8px 0; font-size: 12px; color: var(--text-2); border-radius: 0 5px 5px 0; }

/* manual v2 — rich procedure steps + prereqs/cautions/outcome */
.m-step .m-meta { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 11px; }
.m-target { color: var(--text-2); }
.m-acc { color: var(--running-muted); font-family: ui-monospace, Consolas, monospace; }
.m-sub2 { font-size: 11px; color: var(--text-2); font-weight: 600; margin: 8px 0 2px; text-transform: uppercase; letter-spacing: .04em; }
.m-ul { margin: 0 0 8px; padding-left: 18px; color: var(--text-2); font-size: 12px; }
.m-ul li { margin: 2px 0; }
.m-caution { border-left: 3px solid #5a4718; background: rgba(251,191,36,0.06); color: #d7b25a; padding: 6px 10px; margin: 8px 0; font-size: 11.5px; border-radius: 0 4px 4px 0; }
.m-outcome { margin-top: 8px; color: var(--running); font-size: 12px; font-weight: 600; }
/* Collapsible step preview on the Procedures (live) selection menu. */
.m-steps { margin-top: 4px; }
.m-steps > summary { cursor: pointer; font-size: 11px; color: var(--text-2); font-weight: 600;
  list-style: none; padding: 4px 0; user-select: none; }
.m-steps > summary::-webkit-details-marker { display: none; }
.m-steps > summary:hover { color: var(--running); }
.m-steps[open] > summary { margin-bottom: 4px; }

.m-follow { float: right; margin: -2px 0 0 8px; font-size: 11px; padding: 3px 8px; color: var(--running); border-color: var(--running-muted); }
.m-follow:hover { color: var(--text); background: var(--running-muted); }

/* ---- packed markdown manual (Manuals/*.md via RD.MANUAL_MD) ---- */
.mnav-sep {
  margin: 10px 4px 2px; padding-top: 8px; border-top: 1px solid var(--border);
  color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
}
.mdoc { max-width: 880px; }
.mdoc h1 { font-size: 18px; margin: 0 0 12px; color: var(--text); }
.mdoc h2 { font-size: 15px; margin: 20px 0 8px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.mdoc h3 { font-size: 13.5px; margin: 16px 0 6px; color: var(--text-2); }
.mdoc h4, .mdoc h5, .mdoc h6 { font-size: 12.5px; margin: 12px 0 4px; color: var(--text-2); }
.mdoc p { margin: 0 0 10px; }
.mdoc ul, .mdoc ol { margin: 0 0 10px; padding-left: 22px; }
.mdoc li { margin: 2px 0; }
.mdoc blockquote {
  margin: 0 0 10px; padding: 6px 12px; border-left: 3px solid var(--stopped);
  background: var(--bg-strip); color: var(--text-2);
}
.mdoc hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
.mdoc code {
  font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 11.5px;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 3px; padding: 0 4px;
}
.mdoc .mdoc-code {
  font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 11.5px;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 5px;
  padding: 8px 10px; overflow-x: auto; margin: 0 0 10px; white-space: pre;
}
.mdoc .mdoc-tw { overflow-x: auto; margin: 6px 0 14px; }
.mdoc table { width: 100%; border-collapse: collapse; font-size: 12px; }
.mdoc th, .mdoc td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.mdoc th { color: var(--text-2); font-weight: 600; white-space: nowrap; }
.mdoc a { color: var(--running); text-decoration: none; }
.mdoc a:hover { text-decoration: underline; }

/* ============================================================ M6 Instructor —
   highlight glow (control the current beat/step points at) + Level Complete */
.instr-glow {
  animation: instrGlow 1.6s ease-in-out infinite;
  border-radius: 6px;
  position: relative;
  z-index: 5;
}
@keyframes instrGlow {
  0%, 100% { box-shadow: 0 0 0 1px rgba(122, 176, 255, 0.55), 0 0 10px 2px rgba(122, 176, 255, 0.25); }
  50%      { box-shadow: 0 0 0 2px rgba(122, 176, 255, 0.95), 0 0 18px 5px rgba(122, 176, 255, 0.45); }
}
/* Checklist hover glow — greener tint, distinct from the directive .instr-glow (blue).
   STEADY: hovering a NON-current step must not pulse (#607 item 3). */
.ckl-hoverable { cursor: pointer; }
/* THE PROSE IS PROSE, NOT A HIT TARGET (#726; owner #724 item 9, verbatim: "the walkthrough
   text has the mouse pointer finger and i cant select the text to copy it").

   `.ckl-hoverable` is applied to the WHOLE step card whenever the step names a highlight
   target — which is most steps — so the pointer finger was drawn over every word of the
   instruction, the why block, the done-when line and the cautions. MEASURED computed `cursor`
   before this rule, on the active `pwr_startup` step: .ckl-txt, .ckl-why, .ckl-sub and
   .ckl-crit all `pointer`.

   The card is still clickable; only the CURSOR moves off the text, and the icon column plus
   the card's own padding keep the pointer so the expand affordance is still discoverable.
   THE SELECTION HALF OF THAT REPORT IS NOT HERE AND WAS NEVER CSS — `user-select` measured
   `auto` on every one of those blocks. See ui/app.js `selectionInside`. */
.ckl-hoverable .ckl-body { cursor: text; }
.ckl-hoverable .ckl-body button,
.ckl-hoverable .ckl-body summary,
.ckl-hoverable .ckl-body a { cursor: pointer; }
/* ============================================ the WALKTHROUGH's attention family (#743)
   THE HUE IS CYAN AND THAT IS A RULING *(OWNER, 2026-09-13 playtest, #724: "I think we should
   make the color cyan instead of green. go back to the old glowing highlight and make it cyan.
   cyan is our user interaction color… You could try keeping some of the texture but bring the
   glow back. the glow looked much cleaner anyway. Lets use pulsing for a user control and steady
   for an indication to watch.")*. It was green (rgba(120,210,150)) from #244 through #685.

   ⚠ CYAN WAS ALREADY SPOKEN FOR TWICE ON THIS BOARD, so the family carries its own value and is
   held apart from both by GEOMETRY, never by hue alone — the same argument the bus's own note
   makes below, applied one level down:

     rgba( 91,179,196)  the HIGHLIGHT BUS (.hl-glow/.hl-pin, = --normal). Muted teal, STEADY,
                        SOLID and FLUSH (1-2px ring at offset 0). Answers "what is related to
                        what", and the player asked for it.
     rgba( 79,227,255)  the BOARD'S HOVER RING (.bd-btn:hover, pwr_board.css:310). Tight — 1px
                        ring, 9px blur, NO spread — and present only while the pointer is on it.
     rgba( 90,240,255)  THIS FAMILY. Brighter and lighter than either, with a much wider bloom
                        (12-20px blur, 3-5px spread), and the player did NOT ask for it: it is the
                        walkthrough pointing. Within the family, MOTION splits the two meanings —
                        pulsing = a control to act on, steady = an indication to watch — and the
                        steady one is additionally DASHED AND OFFSET, which nothing else cyan on
                        this board is. Nothing else cyan on this board pulses, either.

   Rendered with a watch ring, a bus halo and a hover ring lit at once before this shipped
   (inbox/glowcyan/, #743). Do not narrow the bloom or drop the dash without re-rendering that. */
.ckl-glow {
  box-shadow: 0 0 0 1.5px rgba(90, 240, 255, 0.62), 0 0 12px 3px rgba(90, 240, 255, 0.26);
  border-radius: 6px; position: relative; z-index: 5;
}
/* ACKNOWLEDGE *(OWNER, 2026-09-03, #619 item 4: "this button should flash green so the user
   knows thats the control that needs to be pressed to progres.")*. Green and pulsing.

   ⚠ THE SPLIT FROM THE BOARD'S CYAN IS RULED AND DELIBERATE — DO NOT "FIX" THIS TO CYAN
   *(OWNER RULING, 2026-09-13: "Keeping the acknowledge button green is fine.")*, settling the one
   item #743 shipped unresolved. It used to share `cklGlow` with the board's up-next pulse; #743
   took that glow CYAN and split this one off as `cklAckGlow`, the old green keyframe under a new
   name, byte-identical in geometry, so the two still read as the same GESTURE in two places.
   THE TWO COLOURS NOW CARRY A MEANING: cyan on the BOARD = "press this control"; green in the
   PANEL = "you are done with this step". Different acts, and one colour would lose the
   distinction. It is ruled TWICE — 2026-09-03 above and 2026-09-13 here — so a consistency
   argument does not reopen it.

   INFINITE, unlike the speed-dropout flash: this one is not reporting an event, it is the
   control the checklist is waiting on, and it must stay findable until pressed. Reduced motion
   drops the animation and keeps a static ring — the button still has to be obviously the one. */
.ckl-ack-row { display: flex; align-items: center; gap: 8px; margin: 5px 0 2px; flex-wrap: wrap; }
.ckl-ack {
  border-color: var(--running); color: var(--running); font-weight: 700;
  padding: 3px 12px; border-radius: 6px;
  animation: cklAckGlow 1.2s ease-in-out infinite;
}
.ckl-ack:hover { background: rgba(120, 210, 150, 0.12); }
.ckl-ack-note { font-size: 11px; color: var(--text-2); }
@media (prefers-reduced-motion: reduce) {
  .ckl-ack { animation: none; box-shadow: 0 0 0 2px rgba(120, 210, 150, 0.85); }
}
/* The ACKNOWLEDGE button's green, kept at the geometry `cklGlow` had when the two shared it.
   The green is RULED, not leftover *(OWNER RULING, 2026-09-13: "Keeping the acknowledge button
   green is fine.")* — see the `.ckl-ack` block above before changing any colour here. */
@keyframes cklAckGlow {
  0%, 100% { box-shadow: 0 0 0 1px rgba(120, 210, 150, 0.60), 0 0 10px 2px rgba(120, 210, 150, 0.28); }
  50%      { box-shadow: 0 0 0 2px rgba(120, 210, 150, 0.98), 0 0 18px 5px rgba(120, 210, 150, 0.50); }
}
/* The board's "act on this" pulse. CYAN since #743; the geometry is unchanged from the green it
   replaced, because the owner's complaint was the TEXTURE of #685's watch ring, not this.

   ⚠ THE PEAK IS CAPPED UNDER THE WATCH RING, AND THAT CAP IS THE WHOLE POINT OF THIS KEYFRAME
   *(OWNER RULING, 2026-09-15, #758, chosen from three drawn options: "Cap the pulse below the
   watch ring (Recommended)" —
       watch  |=========|  2.00 px flat, alpha 0.90
       press   |=====~~|   1.00 -> 1.60 px, peak alpha 0.85   (never reaches the watch ring)  )*

   IT INVERTED, MEASURED. This 50 % stop used to be `2px … 1` / `20px 5px … 0.55` — a ring EXACTLY
   as wide as `.ckl-watch-glow`'s flat 2.00 px, and brighter (alpha 1.00 against 0.90) with a wider
   bloom (20/5 against 16/4). So for the upper half of every 1.2 s cycle the ruled relationship
   REVERSED and the button-to-press became the heavier ring. A layman playthrough read it as "you
   cannot tell them apart" and stopped using board highlights around step 8.

   THE WATCH RING DOES NOT MOVE — the owner looked at 2.00 px / 0.90 on 2026-09-14 and approved it.
   Only this keyframe moves, and it must stay strictly under the watch ring on ALL THREE channels
   (ring width, ring alpha, bloom) at EVERY instant, not only at the peak. The 0 % stop is
   untouched, because `.ckl-step-glow.ckl-step-done` is byte-identical to it by design (the pulse
   stops, the glow stays) and that pairing is itself a ruling.

   Chosen margins at the peak, against watch's 2.00 px / 0.90 / 16px 4px / 0.42:
     ring width  1.60 px  (0.40 px under, 20 %)      ring alpha 0.80 (0.10 under, 11 %)
     bloom blur  14 px    (2 px under)               bloom spread 3 px (1 px under)
     bloom alpha 0.36     (0.06 under, 14 %)
   0.80 rather than the sketch's 0.85 on purpose: 0.85 leaves 0.05, which is inside the width of a
   future "make it a bit brighter" nudge. The amplitude 0.62 -> 0.80 still reads as a pulse.

   GATED AGAINST THE OTHER CUE, WHICH IS THE SECOND HALF OF THE DEFECT. `verify_board_cues` was
   39/39 while this shipped inverted, because every check read one class in isolation and an
   inversion is a RELATION. Its section 5 now puts both cues on the board at once, seeks this
   animation to 25 stops across the full cycle and asserts the three margins at each. Do not change
   a number here without re-running it. */
@keyframes cklGlow {
  0%, 100% { box-shadow: 0 0 0 1px rgba(90, 240, 255, 0.62), 0 0 10px 2px rgba(90, 240, 255, 0.30); }
  50%      { box-shadow: 0 0 0 1.6px rgba(90, 240, 255, 0.80), 0 0 14px 3px rgba(90, 240, 255, 0.36); }
}
/* PERSISTENT up-next glow *(#607 item 3 supersedes #244's "steady" call)*. The current
   step's controls PULSE with no mouseover so they stay findable; hover of a non-current
   step stays the steady halo above. */
.ckl-step-glow {
  animation: cklGlow 1.2s ease-in-out infinite;
  border-radius: 6px; position: relative; z-index: 4;
}
/* WATCH THIS, as opposed to PRESS THIS *(OWNER, 2026-09-09, #675 section B, built as #685:
   "Each step should highlight the important indications to watch with a non pulsing green
   glow.")*. The active step's `hl` targets keep the pulse above — that is #607 item 3 and it
   is not being undone here; this is the second treatment, for the step's `hl_watch` list.

   ⚠ THE DASH IS GONE, AND IT IS NOT COMING BACK ON THE DEFAULT PATH *(OWNER RULING, 2026-09-14,
   #755 items 7/10/19, given as a choice between drawn options: "Both glow; pulse + one static
   cue" —
       PRESS BUTTON      ((( soft pulsing glow )))   -> after press: steady glow, no pulse
       WATCH INDICATION  [ steady glow + 2px solid ring ]  (no dash, no motion)
   after *(OWNER, #755 item 10: "I dont like the dashed line look for the walkthrough highlights.
   go back to the glow and pulsing glow. i dont know what was wrong with them in the first
   place.")*.)

   SO THE TWO STILL DIFFER BY GEOMETRY, NOT ONLY BY MOTION, which is what #685 and DESIGN_CRITERIA
   question 4 (does it complicate things for the player — a VETO) required and what the dash used
   to carry: the RING WEIGHT is now the shape difference. Watch holds a flat 2 px ring; the pulse
   breathes 1 px -> 1.6 px and NEVER REACHES IT — that ceiling is the #758 ruling and it is the
   reason the weight difference is readable at all. This line used to read "1 px -> 2 px with a
   bloom half again as wide at its peak (20px/5px against 16px/4px)", which was the INVERSION
   written down as a design note: at the peak the press cue was the heavier and brighter ring on
   all three channels. See the `cklGlow` block above for the capped numbers and their margins.
   A colour-blind player and a `prefers-reduced-motion` player both still have a non-hue
   difference — under reduced motion the pulse falls back to a 3 px DASHED OUTLINE (at the end of
   this file) and this ring stays a solid box-shadow, so the two are a line style apart there and
   a weight apart everywhere else. Do not give this class a reduced-motion clause: it has nothing
   to reduce, and giving it the dash would put the dash back on the board.

   DO NOT TRIM THE BLOOM (#743's lesson, and it stands): as #685 shipped it the halo was
   `0 0 6px 1px … 0.22` — a quarter the spread and a fifth the alpha of the pulse's — which left a
   hairline doing all the work and the owner missed it on the board. The halo here stays within a
   whisker of the pulse's own so the two read as ONE family at two intensities.

   The box-shadow is deliberately the whole treatment rather than an outline plus a shadow: it is
   what makes the ring read as a glow at a glance rather than as a selection border, and
   `run_glow_stacking` reads the box-shadow + z-index pair to decide this class must be pinned in
   pwr_board.css.

   STEADY is a RULING, twice over — "Lets use pulsing for a user control and steady for an
   indication to watch" (2026-09-13 playtest) and the 2026-09-14 ruling above. */
.ckl-watch-glow {
  box-shadow: 0 0 0 2px rgba(90, 240, 255, 0.90), 0 0 16px 4px rgba(90, 240, 255, 0.42);
  border-radius: 6px; position: relative; z-index: 4;
}
/* THE ACT IS DONE, SO THE MOTION STOPS AND THE GLOW STAYS *(OWNER, 2026-09-14, #755 item 19:
   "During wlakthroughs, when a hightighted button the glow should stop pulsing."; settled by the
   same ruling as above — "after press: steady glow, no pulse")*.

   IT DOES NOT GO DARK, and that is the half that is easy to get wrong. The step is still the
   active step and the control the player pressed is still the control it is about — a cue that
   vanishes on the press says "you are on the wrong thing now", which is false until the step
   advances. What ends is the ASK, not the pointing.

   THE STEADY VALUE IS THE PULSE'S OWN 0% STOP, not a new one: the same ring, holding still. That
   also keeps it under the watch ring's weight (1 px against 2 px), so a board carrying both still
   reads press-then-watch rather than two equal rings.

   A COMPOUND SELECTOR ON PURPOSE. `run_glow_stacking` pins every SIMPLE lifting-glow class in this
   file to a `.bd-tile` override in pwr_board.css; this rule rides on `.ckl-step-glow`, which is
   already pinned, so the element's z-index behaviour is unchanged and no second entry is owed. */
.ckl-step-glow.ckl-step-done {
  animation: none;
  box-shadow: 0 0 0 1px rgba(90, 240, 255, 0.62), 0 0 10px 2px rgba(90, 240, 255, 0.30);
}
/* ============================================ the highlight bus (#444, spec §7)
   A soft CYAN halo — an outer GLOW, never a fill, tint or recolor. Cyan is the only
   board colour that is not a plant state: green, amber and red all carry condition
   meaning, so a halo in any of them could be misread as a state change. The
   element's own colours stay untouched underneath, which is what lets a haloed
   component in alarm still read as in alarm.

   It survives colour-vision deficiency because it is GEOMETRY, not hue: cyan and
   green can converge under deuteranopia, but a glow around an outline still reads
   as a glow.

   PINNED DIFFERS BY WEIGHT, NOT HUE. A second colour would be a second meaning, and
   the two states are the same relation — one of them is just holding still.

   IT DOES NOT PULSE. The instructor and checklist glows animate because they are
   telling you to look at something you did not choose; this one answers a question
   you asked by pointing, so it appears and stays. That also satisfies §7's
   reduced-motion requirement without a media query. */
.hl-glow {
  border-radius: 6px; position: relative; z-index: 5;
  box-shadow: 0 0 0 1px rgba(91, 179, 196, .70), 0 0 12px 3px rgba(91, 179, 196, .30);
}
.hl-pin {
  border-radius: 6px; position: relative; z-index: 5;
  box-shadow: 0 0 0 2px rgba(91, 179, 196, .95), 0 0 18px 5px rgba(91, 179, 196, .45);
}
/* The UI relation (spec §7): a control lighting the INTERFACE element it changes.
   Applied only where the target is not already obvious — a control sitting beside
   what it changes gains nothing from a halo but flicker. */
.hl-lane {
  box-shadow: inset 0 0 0 1px rgba(91, 179, 196, .55);
  border-radius: 3px;
}

.lc-msg { margin-bottom: 8px; }   /* endpoint beat commentary shown above the level-complete panel */
.lc-panel { display: flex; flex-direction: column; gap: 6px; }
.lc-title { font-weight: 700; font-size: 13px; color: var(--text); }
.lc-actions { display: flex; gap: 6px; margin-top: 4px; }
.lc-actions .btn { flex: 0 0 auto; }

/* ---- Scenario cards + walkthrough rows (Plant & Mission window) ---- */
.tr-card {
  border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; margin-bottom: 8px; background: var(--bg-elevated);
}
.tr-card.active { border-color: rgba(122, 176, 255, 0.6); }
.tr-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.tr-title { font-weight: 700; font-size: 12px; color: var(--text); }
.tr-badge {
  font-size: 10px; color: var(--muted); border: 1px solid var(--border);
  border-radius: 3px; padding: 0 5px; white-space: nowrap;
}
.tr-actions { margin-top: 6px; }
.tr-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 4px 2px; border-bottom: 1px dotted var(--border);
}
.tr-ptitle { font-size: 12px; color: var(--text); }

/* ---- Campaign (Plant & Mission window): acts, mission rows, progress ---- */
.camp-head {
  border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; margin-bottom: 8px; background: var(--bg-elevated);
}
.camp-title { font-weight: 700; font-size: 13px; color: var(--text); }
.camp-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 11px; color: var(--muted); }
.camp-bar { flex: 1; height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; }
.camp-fill { height: 100%; background: rgba(122, 176, 255, 0.85); transition: width 300ms ease; }
.camp-continue { margin-top: 8px; }
.camp-done { margin-top: 8px; font-weight: 700; font-size: 12px; color: var(--text); }
.camp-act {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted); margin: 10px 0 4px;
}
.camp-mission {
  display: grid; grid-template-columns: 18px 1fr auto; align-items: center;
  column-gap: 8px; padding: 4px 2px; border-bottom: 1px dotted var(--border);
}
.camp-mission .camp-teaches { grid-column: 2 / 4; font-size: 11px; color: var(--muted); padding-bottom: 2px; }
.camp-mark { font-size: 11px; text-align: center; }
.camp-mtitle { font-size: 12px; color: var(--text); }
.camp-mission.done .camp-mtitle { color: var(--muted); }
.camp-mission.done .camp-mark { color: #79d297; }
.camp-mission.locked { opacity: 0.55; }
.camp-mission.next { background: rgba(122, 176, 255, 0.07); border-radius: 4px; }

/* ---- First-run Hook invitation (prompted, never forced) ---- */
.hook-prompt {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(4, 8, 14, 0.55);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.hook-prompt[hidden] { display: none; }
.hook-card {
  width: min(460px, 92vw);
  background: var(--bg-plant); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 12px 48px rgba(0,0,0,0.6);
  padding: 18px 20px;
}
.hook-title { font-size: 15px; font-weight: 700; color: var(--text); margin-bottom: 8px; }
.hook-body { font-size: 12px; color: var(--muted); line-height: 1.5; margin-bottom: 14px; }
.hook-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ---- Strip-chart rewind picker (sandbox: click a moment to jump back) ---- */
.strip-chart.rewind-pick .chart-plot { cursor: crosshair; }
.strip-chart.rewind-pick { outline: 1px solid rgba(122, 176, 255, 0.5); border-radius: 4px; }
.rewind-hint {
  position: absolute; top: 4px; left: 50%; transform: translateX(-50%);
  background: rgba(10, 16, 26, 0.9); border: 1px solid rgba(122, 176, 255, 0.5);
  border-radius: 4px; padding: 2px 10px;
  font-size: 11px; color: #a8c6f0; white-space: nowrap; pointer-events: none; z-index: 6;
}
.rewind-hint[hidden] { display: none; }
.scrubber #chartRewindBtn { margin-left: 6px; padding: 0 8px; }
.scrubber #chartRewindBtn:disabled { opacity: 0.35; }

/* ============================================================ chat mode (TMI-2 M5)
   Multi-speaker transcript in the Instructor card. Bubbles stay quiet-board
   muted; color marks WHO is speaking, not urgency (SYS lines borrow caution). */
/* The persona header survives chat mode (#237): it is the always-visible
   collapse affordance and the mid-scenario orientation line (renderChat swaps
   its text to the scenario title — a scene, never a speaker). Only the
   prior-messages line is chat-hidden. */
#instructorCard.chat-mode .instr-prev { display: none; }
/* Collapsed chat card: a short sliver of the transcript, pinned to its LATEST
   lines by script scroll — the one-line nowrap ellipsis further up is for the
   free-play message card and would render a whole transcript as one line. */
.instructor.collapsed.chat-mode .instr-current {
  white-space: normal; overflow: hidden; max-height: 46px; text-overflow: clip;
}
.instructor.collapsed.chat-mode .chat-btns { display: none; }
.chat-log {
  display: flex; flex-direction: column; gap: 8px;
  max-height: 48vh; overflow-y: auto; padding-right: 4px; margin-bottom: 8px;
}
.chat-line { display: flex; flex-direction: column; gap: 2px; max-width: 94%; }
.chat-meta { font-size: 10px; letter-spacing: .05em; color: var(--muted); text-transform: uppercase; }
.chat-txt {
  font-size: 13px; line-height: 1.45; color: var(--text);
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px 9px; white-space: pre-line;
}
.chat-sup  .chat-txt { border-left: 3px solid var(--stopped); }
.chat-supx .chat-txt { border-left: 3px solid var(--normal); }      /* supervisor reacting to the player */
.chat-aux  .chat-txt { border-left: 3px solid var(--border); color: var(--text-2); }
.chat-chief .chat-txt { border-left: 3px solid #C084FC; }
.chat-sys  .chat-txt {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11.5px;
  color: var(--caution); border-left: 3px solid var(--caution); background: rgba(217,164,65,.06);
}
.chat-player { align-self: flex-end; align-items: flex-end; }
.chat-player .chat-txt { border-right: 3px solid var(--normal); border-left: 1px solid var(--border);
  background: rgba(91,179,196,.08); }
.chat-gap {
  text-align: center; color: var(--muted); font-size: 11px; font-style: italic;
  border-top: 1px dashed var(--border); margin-top: 4px; padding-top: 6px;
}
.chat-btns { display: flex; flex-direction: column; gap: 6px; }
.chat-btn { width: 100%; padding: 7px 10px; font-weight: 700; }
.chat-btn-ack { border-color: var(--stopped); }
.chat-btn-skip { border-color: var(--caution); color: var(--caution); }
.chat-ff { text-align: center; color: var(--caution); font-size: 11px; font-style: italic; }

/* ============================================================ auto-checklists (Path 3)
   Picker lives in Operate; the live checklist still renders in the Instructor card. */
.ckl-picker { margin: 2px 0 4px; }
.ckl-menu { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; max-height: 40vh; overflow-y: auto; }
.ckl-menu[hidden] { display: none; }
.ckl-menu button {
  text-align: left; background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text); padding: 6px 9px; font-size: 12.5px; cursor: pointer;
}
.ckl-menu button:hover { border-color: var(--stopped); }
.ckl-cat { display: inline-block; min-width: 62px; margin-right: 6px; color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .05em; }
.ckl-log { display: flex; flex-direction: column; gap: 8px; max-height: 48vh; overflow-y: auto; padding-right: 4px; margin-bottom: 8px; }
/* ...EXCEPT WHILE A CHECKLIST IS RUNNING, where it must reach the bottom of the column
   *(OWNER, 2026-09-02 playtest, M5->3 item 4: "The scroll window for the tabs should go all the
   way to the bottom of the column")*.
   MEASURED at 1600x900: `.ckl-log` resolved to 432 px (48vh) inside an `#instrLog` offering 604,
   so the steps scrolled in a box 172 px shorter than the space it sat in — with a second
   scrollbar around it, because #instrLog scrolls too. The cap is a fraction of the VIEWPORT and
   the slot is a fraction of the COLUMN, so the two only ever agree by accident; the taller the
   screen, the bigger the dead band.
   In chat-mode the log stops being a scroller and #instrLog — already `flex: 1 1 0` and filling
   the card — is the only one. `.instr-prev` is display:none in chat-mode, so the log is
   effectively alone in there. The 48vh cap stays for every other use of .ckl-log. */
.instructor.chat-mode .ckl-log { max-height: none; overflow-y: visible; }
.ckl-head { font-size: 13px; }
.ckl-step { display: flex; gap: 8px; align-items: flex-start; border: 1px solid var(--border); border-radius: 8px; padding: 6px 9px; background: var(--bg-elevated); }
.ckl-ico { font-weight: 700; width: 16px; text-align: center; flex: none; }
.ckl-body { flex: 1; min-width: 0; }
.ckl-txt { font-size: 12.5px; line-height: 1.4; color: var(--text); }
/* THE INCIDENT WALKTHROUGH'S NARRATIVE BLOCK (#670 Phase 1) — clock, what the crew saw, what
   they knew, what they did — ABOVE the numbered instruction. Quieter than the instruction and
   louder than a .ckl-sub: it is the reason the step exists, but the step is still the thing to
   do. The left rule borrows .ckl-why's idiom so the two read as the same kind of writing (one
   history, one plant lesson) rather than as two competing headlines. Every color is a token, so
   it follows the board's theme; nothing here is authored per-walkthrough. */
.ckl-story {
  margin-bottom: 6px; padding: 5px 0 5px 8px;
  border-left: 2px solid var(--border);
  font-size: 11.5px; line-height: 1.45; color: var(--text-2);
}
.ckl-story-l { margin-bottom: 2px; }
.ckl-story-l:last-child { margin-bottom: 0; }
/* the label column: small caps-ish legend, so four lines scan as a table without being one */
.ckl-story-l > span {
  display: inline-block; min-width: 86px; color: var(--muted);
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
}
/* `crew: true` — the step is history, not advice, and the tag says so beside the instruction
   rather than under it, where the details fold could hide it. Caution-coloured on purpose: the
   steps that carry it are the ones that made the accident. */
.ckl-crew {
  font-size: 10px; color: var(--caution); white-space: nowrap;
  border: 1px solid var(--caution); border-radius: 3px; padding: 0 4px; margin-left: 4px;
}
/* THE ACTIVE BLOCK, WHICH NOW HANGS BELOW THE INSTRUCTION (#628, owner 2026-09-04: "move the
   numbered step to always be the first part of the stack. then the rest of the step that appears
   when its active goes below it."). Its only job is the gap: the criteria line carries
   margin-bottom and .ckl-use zeroes its margin-top, so without this the block butts straight
   against the step text it belongs to. */
.ckl-act { margin-top: 5px; }
.ckl-sub { font-size: 11px; color: var(--text-2); margin-top: 2px; }
/* THE OPERATING TIP IS ITALIC *(OWNER, 2026-09-13: he renders the `note` in italics in his own
   step template — CHECKLIST_WRITING_GUIDE.md §15 T7 flagged it as a one-line CSS change pending
   his word)*. `.ckl-sub.muted` is the step `note` and ONLY the step `note`: the `muted` class is
   applied to exactly one `.ckl-sub` in the whole renderer (ui/app.js, the `st.note` line) and
   matches no other rule that reaches this card, so it is a free discriminator — the other five
   `.ckl-sub` users (checked by hand, overtaken, Watch for, the two wait lines) are untouched and
   must stay upright. Same idiom as `.chart-legend .chart-meta.muted` above. Italic, not a colour
   or a size, because the tip must stay QUIETER than .ckl-crit while reading as a different
   VOICE from the step's imperative — a grey note is where an instruction goes to die (§15). */
.ckl-sub.muted { font-style: italic; }
/* THE CHECK-OFF CRITERIA — first line of the active card, bold, its own color *(OWNER,
   #244 M5→3 item 4: "cyan or cobalt or a blue grey. something that isnt too loud but
   brings attention to it so people know this is the requirement the step is looking
   for.")*. A muted cobalt-blue, brighter when the requirement is met. */
.ckl-crit {
  font-size: 12.5px; font-weight: 700; color: #7fa8dd; line-height: 1.45;
  margin-bottom: 2px;
}
.ckl-crit-met { color: var(--running); }
/* THE LETTERED SUBSTEP PREFIX (#741) — `3a`, `3b`. A quiet fixed-width gutter, not a second
   headline: the row's own text is the thing to read and the letter only says "this is part of
   step 3". Muted rather than the row's cobalt so it does not compete with the ✓/○.
   IT STAYS MUTED WHEN THE ROW GOES GREEN, and that is the behaviour rather than an oversight: a
   direct declaration on the child beats the parent's inherited colour, so .ckl-crit-met's
   var(--running) never reaches it. The tick and the row text carry the met state; the letter is a
   gutter label and reads the same either way. (An earlier draft of this comment claimed the
   opposite AND contradicted itself in one sentence — #741 quality pass.)
   Drawn only when a step has more than one visible row. */
.ckl-crit-n {
  display: inline-block; min-width: 20px; color: var(--muted);
  font-weight: 600; font-variant-numeric: tabular-nums;
}
/* …AND THE DONE-WHEN UNDER IT, on an entry that carries an `ask`. The instruction is the bold
   cobalt row; this is what the sim is waiting for, so it is quieter and lighter — the same
   relationship .ckl-use has to the step text. Indented to the letter gutter so the two lines
   read as one row rather than two. */
.ckl-crit-when {
  font-weight: 400; font-size: 11px; color: var(--text-2);
  margin-left: 20px; line-height: 1.4;
}
/* A ROW THAT HAS NOT COME UP YET, on an `accs_ordered` step (#756). Muted, and with a · for a
   marker instead of the ○, because an ○ in the live cobalt is an invitation to act and this row
   cannot be acted on — the runtime will not latch it and its button is deaf until the row above
   is met. One treatment, not two: the row goes quiet and loses its done-when, which is the same
   move .ckl-sub.muted makes for a note, rather than a new colour on a card that already carries
   three (DESIGN_CRITERIA question 4). */
.ckl-crit-wait { color: var(--muted); font-weight: 600; }
/* THE OUT-OF-TURN REASON (#759, owner ruling 2026-09-15). Amber, not red: the press was not an
   error — the plant accepted it and the point really was plotted — it simply does not count
   toward a rung whose turn has not come. Red would say "you broke something", which is the
   misreading the line exists to prevent. Sits directly under the rows it is about. */
.ckl-oot { margin: 4px 0 0; padding: 3px 7px; border-radius: 4px; font-size: 11px; line-height: 1.35;
           color: var(--caution); background: rgba(217, 164, 65, 0.10);
           border-left: 2px solid rgba(217, 164, 65, 0.55); }
/* WHICH CONTROL, AND WHAT YOU ARE DRIVING IT TO (#598 item 13, owner ruling 2026-09-02).
   Promoted back out of the Details fold: measured, only 41 % of the steps carrying a control
   name it in their instruction text, so folding this away left 27 of them with no visible
   answer to "which knob". It sits between the criteria and the wait line — the #244 item 6
   workflow order, unchanged, though #628 moved the whole block BELOW the instruction rather
   than above it — and is deliberately a touch larger and lighter than a plain
   .ckl-sub, because it is part of the action rather than a footnote to it. The control name
   itself is the brightest thing on the line; that is the word the eye is hunting for. */
.ckl-use { font-size: 11.5px; color: var(--text-2); margin-bottom: 3px; margin-top: 0; }
.ckl-use b { color: var(--text); font-weight: 600; }
/* the "why" (#244 items 2/9) — teaching prose, visually quieter than the action. Since #660
   item 3 it is ALWAYS open on the active step, so it is no longer collapsible in practice and
   its length is paid on every step (which is what made CHECKLIST_WRITING_GUIDE F2 load-bearing).
   #692 item 3 gives it a legend: an unheaded grey paragraph read as part of the step, and the
   owner asked for it "presented as extra learning material". The legend deliberately matches
   .ckl-story-l > span — the other always-drawn supplementary block on the same card. */
/* A LABELLED BUBBLE, not a fourth grey (#653 product defect 4, #687 item 4) *(OWNER, 2026-09-09:
   "The why text needs to have some indication that its extra learning info not actually a step.
   Maby but it in a buttle and label it appropriately?")*. Both fresh-context reviews measured the
   same thing from the other side: `note` and `why` were "two near-identical greys" — both
   var(--text-2) at 11-11.5 px — while one carries contingencies the player must act on and the
   other is optional. The label (above) names it; this tint is what makes it one BLOCK rather than
   one more paragraph. Deliberately the quietest ground on the card: a hair of --bg-elevated over
   the panel, no border colour, because it must not compete with .ckl-crit or the wait rung. */
/* …AND SINCE #741 IT IS A REAL BOX, SET APART FROM THE WORK *(OWNER, 2026-09-13: "I also want to
   make the informational text block more obvious that it's not a work step but just there for
   education. Put it in its own box, move it slightly away from the work steps and label it so
   it's obvious what it is.")*.
   #692 gave it a label and a left rule and a tint, and he is asking again — so the label alone
   was not enough. Three changes, one per clause of the ruling:
     - ITS OWN BOX: a border on all four sides and a full radius, instead of a left rule and a
       tint that read as one more indented paragraph in a card built out of indented paragraphs.
     - MOVED AWAY: margin-top 6px -> 12px, twice the gap between any two working rows, so the
       separation is visible before a word is read. That gap IS the signal; the box only gives
       the gap something to be a gap between.
     - LABELLED: see .ckl-why-lbl. The wording is the owner's call and is pending.
   Still the quietest ground on the card, deliberately: this must not out-shout .ckl-crit, which
   is the row the player is being graded on. The border is --border, the same line the step card
   itself uses, so the box reads as "a different KIND of thing" rather than "an important thing". */
.ckl-why { font-size: 11.5px; color: var(--text-2); line-height: 1.45; margin-top: 12px;
  border: 1px solid var(--border); padding: 6px 9px;
  background: rgba(152,163,175,0.045); border-radius: 5px; }
.ckl-why-lbl {
  display: block; color: var(--muted);
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 3px;
}
/* THE LEG'S CAUTIONS, DRAWN DURING THE RUN (#653 product defect 1). Amber, because they are
   cautions and the board's own amber is what a player has been taught to read that way — the
   same idiom as .m-caution on the browse card, so the two screens agree. The summary line is a
   button (keyboard-reachable, unlike a bare <summary> we would have to restyle) and carries the
   count, which is the half that must survive the collapse. */
.ckl-cautions { margin-bottom: 6px; }
.ckl-caut-sum {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: rgba(251,191,36,0.06); border: 1px solid #5a4718; border-radius: 4px;
  color: #d7b25a; font-size: 11px; padding: 3px 8px; font-family: inherit;
}
.ckl-caut-sum:hover { background: rgba(251,191,36,0.11); }
.ckl-caut-chev { display: inline-block; width: 12px; color: var(--muted); }
.ckl-caut-l {
  font-size: 11px; line-height: 1.45; color: #c9a86a;
  border-left: 2px solid #5a4718; padding: 3px 0 3px 8px; margin: 3px 0 0 6px;
}
/* ⚰ `.ckl-why-btn`, `.ckl-why-all` and `.ckl-expand-hint` WERE HERE and are gone (#739/#741).
   All three styled the per-step why TOGGLE and the card's click-to-expand, which #737 deleted by
   owner ruling — the rules outlived the only markup that ever carried them.
   PROVEN, not assumed: zero emitters in `ui/`, and across `ui/` + `test/` the only non-comment
   reference to any of the three is `test/verify_flags_ui.js:203`, which queries `.ckl-why-btn`
   and is itself a hollow check (an element that no longer exists) — workbench owns that file.
   `dist/Reactor_Dynamics_Alpha_1.7.2.html` and `1.7.3.html` still contain them: those are FROZEN
   RELEASE OUTPUT, a snapshot of the source at build time, not a consumer of it. A rule is live
   because something in the tree emits it, and nothing does.
   `.ckl-why` and `.ckl-why-lbl` are LIVE and stay — `ui/app.js` emits them on every active step
   and `verify_ckl_relevance` asserts both. `.ckl-back` is live (the "All walkthroughs" button). */
.ckl-back { font-size: 11px; padding: 2px 8px; margin-bottom: 6px; }
/* THE SPEED SUGGESTION IS NOT A CAUTION (#653 product defect 4). It wore var(--caution), the
   board's amber for "abnormal", so on a step whose `note` carries the plant's own limit — the
   heatup's 100 degF/hr, the ascension's 590 degF ceiling — the loudest line on the card was the
   advice about which speed button to press. Both reviewers read the orange as the warning and
   the grey as the footnote, which is exactly backwards. Amber now belongs to .ckl-caut-*, which
   is where the real cautions are; the rung line keeps its own colour, a step up from .ckl-sub
   so it still reads as advice rather than prose, and the ⏩ glyph carries the meaning. */
.ckl-wait { color: #8fa3b4; }
.ckl-next { border-color: var(--running); font-weight: 700; }
.ckl-done { opacity: 0.62; border-left: 3px solid var(--running); }
.ckl-done .ckl-ico { color: var(--running); }
.ckl-active { border-left: 3px solid var(--caution); }
.ckl-active .ckl-ico { color: var(--caution); }
.ckl-pend { opacity: 0.45; }
.ckl-mark { margin-top: 6px; font-size: 11px; padding: 3px 8px; }
.ckl-complete { text-align: center; border: 1px solid var(--running); border-radius: 8px; padding: 8px; color: var(--running); }
.ckl-btns { display: flex; gap: 6px; }
.ckl-btns .btn { flex: 1; }

/* ============================ Plant & Mission window ======================
   Sim tab → ⚛️ Plant & Mission… — plant column (left), then mode tabs +
   nested content (right). Reuses the .tr-* / .camp-* blocks above. */
.mission-overlay {
  position: fixed; inset: 0; z-index: 210;
  background: rgba(4, 7, 9, 0.78);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.mission-overlay[hidden] { display: none; }
.mission-modal {
  width: min(940px, 96vw); height: min(84vh, 760px);
  display: flex; flex-direction: column;
  background: var(--bg-plant); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: 0 12px 48px rgba(0,0,0,0.6); overflow: hidden;
}
.mission-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.mission-title { font-weight: 700; font-size: 14px; color: var(--text); }
.mission-body { flex: 1; display: grid; grid-template-columns: 260px 1fr; min-height: 0; }
/* THE PLANT COLUMN IS GONE (#688, owner 2026-09-09) — .mp-plants and the whole .mplant-*
   family went with it. The 260px|1fr default above STAYS: the chart-settings window (#454)
   reuses .mission-body wholesale and needs that left track, as does .fl-body's override
   below by counter-example. The Plant & Mission window collapses to one column here. */
.mission-body.mp-body { grid-template-columns: 1fr; }
/* The clock carries the halt for as long as it stands (#520). Dismissing the dialog
   must not lose the fact that the model has stopped — that IS the defect #520 is
   about — and the clock is the right home because a held plant's clock is the one
   thing still moving. */
.clock.clk-held {
  color: var(--alarm); border: 1px solid var(--alarm); border-radius: 3px;
  padding: 0 5px; position: relative;
}
.clock.clk-held::after {
  content: 'HELD'; margin-left: 7px; font-size: 9px; font-weight: 700; letter-spacing: 1px;
}
/* ---- the simulation-halted dialog (#520) ------------------------------------
   Sits on .mission-overlay / .mission-modal, so the frame, the backdrop and the
   head are the Plant & Mission window's. Only the body and the footer are new:
   this modal is PROSE plus a decision, where that one is a chooser.
   Height is content-sized rather than the 84vh the mission modal fixes, because
   four short paragraphs in a 760px-tall box reads as an error page. */
/* Above the Plant & Mission overlay (210): a halt can be raised while that window is
   open, and the halt is the thing that needs answering first. */
#haltOverlay { z-index: 214; }
.halt-modal { height: auto; max-height: min(84vh, 620px); }
.halt-modal .mission-title { color: var(--alarm); }
.halt-body { padding: 14px 18px; overflow-y: auto; color: var(--text); font-size: 12px; line-height: 1.55; }
.halt-body p { margin: 0 0 10px; }
.halt-lead { font-size: 13px; }
.halt-why {
  border-left: 2px solid var(--alarm); padding: 6px 10px; margin: 12px 0;
  background: rgba(0, 0, 0, 0.25); font-size: 11px; color: var(--caution);
}
.halt-why:empty { display: none; }
.halt-note { color: var(--text-dim, var(--caution)); }
.halt-foot {
  display: flex; gap: 10px; justify-content: flex-end;
  padding: 10px 14px; border-top: 1px solid var(--border);
}
/* The reset is the ACTION, so it is the one that looks like one. Not red: red would
   read as "danger, do not press", and pressing it is the way out. */
.halt-go { border-color: var(--accent); color: var(--accent); font-weight: 700; }

/* The "selection screen (#443)" header that stood here described .mplant-card, deleted with
   the plant column (#688); its own rules had already gone with the start screen. */
/* Settings and the Scanner's long form: modal class, so they pause (#439, spec §1).
   Narrower than the Plant & Mission window — both are a column of rows, not a picker. */
.set-modal { height: auto; max-height: min(84vh, 700px); width: min(560px, 96vw); }
.set-body { padding: 12px 16px 16px; overflow-y: auto; }
.set-body p { font-size: 13px; line-height: 1.55; color: var(--text-2); margin: 0 0 10px; }
.set-body .scan-hint { color: var(--muted); }

/* Checklists — its own tab since #439 (spec §9: it ranks above the Manual, and it is
   a live operating aid, so it never pauses). */
.ckl-picker[hidden] { display: none; }
.ckl-busy { color: var(--muted); font-style: italic; }

/* ---- relevance ordering (#443, spec §9): SORT, DO NOT FILTER -----------------
   A demoted procedure keeps its place in the list and states its gating condition.
   That is the whole value of demoting rather than hiding: the demotion becomes
   instruction, and a player who saw a checklist yesterday can still find it. */
.ckl-gate {
  display: block; font-size: 10px; color: var(--muted); font-style: italic;
  margin-top: 1px;
}
.ckl-gated { opacity: .72; }
.ckl-later { margin-top: 6px; border-top: 1px solid var(--border); padding-top: 4px; }
.ckl-later > summary {
  font-size: 11px; color: var(--muted); cursor: pointer; padding: 2px 0;
  list-style: none;
}
.ckl-later > summary::before { content: '▸ '; }
.ckl-later[open] > summary::before { content: '▾ '; }

/* The free-play Instructor's launcher — it had static help copy in the shell's most
   valuable slot; this gives it a job (#443, spec §9). Since #598 item 16 it is ONE BAR that
   opens the Checklists tab, not a second ranked list of the same procedures that tab already
   lists (owner directive; and two menus answering one question is the duplicate-authority
   case DESIGN_CRITERIA Q4 exists to veto). The .instr-launch-t / .instr-launch-more rules that
   dressed the old list went with it. */
.instr-launch { margin-bottom: 10px; }
.instr-launch .btn { display: block; width: 100%; text-align: left; margin-bottom: 3px; font-size: 12px; }
.instr-launch-bar {
  text-align: center !important; font-size: 12.5px; font-weight: 600; padding: 9px 12px;
  color: var(--accent, #7fd1c0);
  border-color: color-mix(in srgb, var(--accent, #7fd1c0) 45%, transparent);
  background: color-mix(in srgb, var(--accent, #7fd1c0) 10%, transparent);
}
.instr-launch-bar:hover {
  background: color-mix(in srgb, var(--accent, #7fd1c0) 18%, transparent);
  border-color: var(--accent, #7fd1c0);
}

.start-overlay { z-index: 215; }
.start-modal { height: auto; max-height: min(84vh, 620px); width: min(720px, 96vw); }
.start-head { padding: 18px 18px 6px; }
.start-title { font-size: 20px; font-weight: 700; color: var(--text); letter-spacing: .01em; }
.start-title .a { font-size: 16px; }
.start-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.start-body { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 12px 18px; min-height: 0; overflow-y: auto; }
.start-body[hidden] { display: none; }
.start-plants { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.start-acts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.start-act {
  display: flex; gap: 8px; align-items: flex-start; cursor: pointer;
  border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px;
  background: var(--bg-elevated);
}
.start-act:hover { border-color: rgba(122, 176, 255, 0.4); }
.start-act.on { border-color: rgba(122, 176, 255, 0.7); background: rgba(122, 176, 255, 0.08); }
.sa-name { display: block; font-size: 13px; font-weight: 700; color: var(--text); }
.sa-desc { display: block; font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.4; }
.start-foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 12px 18px 16px; border-top: 1px solid var(--border);
}
.start-go { font-size: 13px; padding: 8px 14px; border-color: var(--running); color: var(--text); }
.start-foot .btn[hidden] { display: none; }

/* ---- reset, in the session footer of the Plant & Mission window (#443) ---- */
.mp-session { flex: 0 0 auto; padding: 10px 14px 12px; border-top: 1px solid var(--border); }
.mp-reset { margin-top: 8px; }
.mp-reset.mp-reset-armed { border-color: var(--caution); color: var(--caution); }

/* ---- coach marks (#443): a dot that waits, and retires on first use -------- */
.unvisited { position: relative; }
.unvisited::after {
  content: ''; position: absolute; top: -3px; right: -3px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--normal); box-shadow: 0 0 0 2px var(--bg-plant);
  pointer-events: none;
}

/* step 2 — mode tabs; step 3 — the nested content */
.mp-right { display: flex; flex-direction: column; min-height: 0; }
.mp-modes { display: flex; border-bottom: 1px solid var(--border); }
.mp-modes button {
  flex: 1; padding: 9px 4px; background: none; border: none;
  border-bottom: 2px solid transparent; color: var(--text-2);
  font: inherit; font-size: 12px; cursor: pointer;
}
.mp-modes button:hover { color: var(--text); }
.mp-modes button.on { color: var(--text); border-bottom-color: rgba(122, 176, 255, 0.85); }
/* THE GREEN [NEW] ON THE WALKTHROUGHS TAB (#688, owner 2026-09-09). #79d297 is the same
   green the deleted plant column used for its ● active mark — brighter than --running
   (#46A35E), which is sized for a filled board tile and reads dim at 9 px. Deliberately NOT
   the caution amber of the COMING SOON badge below: this one says "here now". */
.mp-modes .mp-new {
  margin-left: 6px; display: inline-block; vertical-align: 1px;
  border: 1px solid #79d297; color: #79d297; background: rgba(121, 210, 151, .10);
  border-radius: 3px; padding: 0 4px;
  font-size: 9px; font-weight: 700; letter-spacing: .08em;
}
.mp-content { flex: 1; overflow-y: auto; padding: 12px 14px; }
/* campaign mission rows in the wide window: pin the Start button to the title
   row (the teaches line spans beneath it), instead of wrapping to its own row */
.mp-content .camp-mission > .btn { grid-column: 3; grid-row: 1; white-space: nowrap; }
/* Coming-soon panel — an area whose content this channel does not offer yet
   (site/flags.js, #241). Amber, and it says "being finished", not "broken". It used to share
   its badge with the plant column's not-yet-built cards; that column went at #688. */
.mp-soon {
  border: 1px dashed var(--border); border-radius: 6px;
  padding: 18px 16px; text-align: center; background: var(--bg-elevated);
}
.mp-soon-tag {
  display: inline-block; border: 1px solid var(--caution); color: var(--caution);
  border-radius: 4px; padding: 2px 8px; font-size: 10px; font-weight: 700;
  letter-spacing: 1px; margin-bottom: 8px;
}
.mp-soon .m-note { max-width: 46ch; margin: 0 auto; line-height: 1.5; }

/* ---- Features window (development flag toggles, #241) ---- */
/* Reuses the mission modal, but it has no plant column — .mission-body's
   260px | 1fr grid would otherwise squeeze the whole board into 260px. */
.mission-body.fl-body { grid-template-columns: 1fr; }
.fl-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px;
  margin: 10px 0 4px; background: var(--bg-elevated);
}
.fl-bar .k { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-2); }
.fl-bar .btn { margin-left: auto; }
.fl-warn {
  border: 1px solid var(--caution); color: var(--caution); border-radius: 6px;
  padding: 6px 10px; font-size: 11.5px; margin-bottom: 4px;
}
.fl-row {
  display: grid; grid-template-columns: 46px 1fr auto auto auto; align-items: center;
  column-gap: 8px; padding: 4px 2px; border-bottom: 1px dotted var(--border);
}
.fl-row.off .fl-name, .fl-row.off .fl-id { color: var(--muted); }
.fl-sw {
  font: inherit; font-size: 9.5px; font-weight: 700; letter-spacing: .08em;
  padding: 2px 0; border-radius: 3px; cursor: pointer;
  border: 1px solid var(--border); background: none; color: var(--muted);
}
.fl-sw.on { border-color: rgba(70,163,94,.65); color: #6FD089; background: rgba(70,163,94,.08); }
.fl-name { font-size: 12px; color: var(--text); }
.fl-id { font-size: 10px; color: var(--text-2); }
.fl-stage {
  font-size: 9px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; color: var(--muted);
}
.fl-stage.fl-public { border-color: rgba(70,163,94,.55); color: #6FD089; }
.fl-stage.fl-preview { border-color: rgba(217,164,65,.55); color: var(--caution); }
.fl-ov { font-size: 10px; color: var(--caution); cursor: pointer; }

/* Free Play starting-condition rows */
.init-row {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px;
  cursor: pointer; font-size: 12px; color: var(--text); background: var(--bg-elevated);
}
.init-row:hover { border-color: rgba(122, 176, 255, 0.4); }
.init-row.on { border-color: rgba(122, 176, 255, 0.7); background: rgba(122, 176, 255, 0.08); }
.init-dot { color: rgba(122, 176, 255, 0.9); }
.mp-start { display: block; width: 100%; margin-top: 12px; padding: 10px; font-weight: 700; }

/* ---- 1/M startup plot (draggable window — ui/panels/one_over_m.js) ---- */
/* ONE FORM, FLOATING AND DRAGGABLE *(OWNER RULING, 2026-09-13: "let's make the card floating and
   dragable like it was originally")*. This supersedes #724 item 7's docked layout, which lived for
   one day in `.right-col` and before that (#660 item 13) in `.bottom-row`. Every `.oom-docked` rule
   is deleted; nothing keys on a host any more.

   WHAT THE OWNER WAS COMPLAINING ABOUT SURVIVES THE RULING, because the dock was only the means.
   Item 7's other two sentences — "The 1/m plot is too small" and "make the predicted criticality
   text large enough to read and obvious" — are answered here, and floating answers the first one
   BETTER: the plotted-data rect measures 308x220 px floating against 321x160 docked, 68,000 px^2
   against 51,000, because a floating window's height is its own rather than a share of somebody
   else's column.

   A GRID, NOT THE FLEX COLUMN THIS WAS, and for exactly one reason: the buttons go ABOVE the plot
   (the owner, item 7) while the DOM order stays head, svg, foot, pred, msg, help. Grid areas move
   the visual slot without touching one_over_m.js's markup — the same trick the retired dock used,
   kept because the requirement it serves was not what got overruled. Rows are all `auto`: the svg
   has `width: 100%` and no height, so its height follows the viewBox aspect and a longer readout
   makes the WINDOW taller instead of the plot shorter. (That is why the dock needed a
   ResizeObserver on the readout and this does not — see one_over_m.js.)

   THE WIDTH IS CLAMPED AT EVERY WIDTH, not just narrow ones. `right: 340px; width: 356px` is a
   position measured from a 1500 px control room, and it was latent for as long as narrow layouts
   had a dock to fall back on: MEASURED at a 400 px viewport it put the window's left edge at
   -296 px, most of it off the left of the screen. With the dock gone this is the only form there
   is, so the guard is load-bearing everywhere. */
.oom-win {
  position: fixed; top: 90px; z-index: 250;
  /* `right: 340px` alone is a position measured from a 1500px control room, and it was a
     defect at every narrow width: MEASURED at a 400px viewport it put the window's left edge
     at -296px, most of it off the screen with the plot unreachable. It was LATENT while
     narrow layouts had a dock to fall back on and became live the moment they did not.
     `max(12px, min(340px, 100vw - 380px))` keeps the 340px gutter wherever there is room for
     it, pulls the window back toward the right edge when there is not, and never lets it
     leave the viewport at all — 340px at 1500px and 1920px wide, 20px at 400px, 12px at
     300px. One rule rather than a breakpoint, because a breakpoint is a claim about which
     widths are narrow and this is arithmetic that holds at all of them. */
  right: max(12px, min(340px, calc(100vw - 380px)));
  width: min(356px, calc(100vw - 24px));
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: 0 12px 48px rgba(0,0,0,0.6);
  font-size: 12px; color: var(--text);
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "head" "foot" "svg" "pred" "msg" "help";
}
.oom-win[hidden] { display: none; }   /* the grid display above must not beat [hidden] */
.oom-win > .oom-head { grid-area: head; }
.oom-win > .oom-svg  { grid-area: svg; }
.oom-win > .oom-foot { grid-area: foot; }
.oom-win > .oom-pred { grid-area: pred; }
.oom-win > .oom-msg  { grid-area: msg; }
.oom-win > .oom-help { grid-area: help; }

.oom-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 10px; border-bottom: 1px solid var(--border);
  font-weight: 600; user-select: none; touch-action: none;
}
.oom-x { padding: 0 8px; line-height: 20px; }
.oom-svg { display: block; width: 100%; background: var(--bg-plant); }
/* The button bar, ABOVE the plot (owner, #724 item 7 — the one part of that item the 2026-09-13
   ruling leaves standing). Equal widths so it reads as one control group rather than three sizes,
   and a rule under it separating the controls from the drawing. */
.oom-foot {
  display: flex; gap: 6px; align-items: center;
  padding: 6px 10px; border-bottom: 1px solid var(--border);
}
.oom-foot .btn { flex: 1 1 0; min-width: 0; padding: 5px 4px; white-space: nowrap; }
/* THE PREDICTION, LARGE AND OBVIOUS *(OWNER, #724 item 7: "make the predicted criticality text
   large enough to read and obvious")*. That sentence is untouched by the 2026-09-13 ruling, which
   moved the window and nothing else, so the readout keeps the size the dock gave it: 15 px, 600,
   and in `--caution`, the SAME colour the plot draws its own critical marker and label in
   (.oom-crit / .oom-critlab below), so the readout and the line on the plot read as one fact. It
   was 11 px / 400 / --text-2 at the tail of the button bar — the smallest type in the panel, for
   the one number the tool exists to produce.

   IT WRAPS RATHER THAN OVERFLOWS. The string is the longest in this panel
   ("predicted criticality ~ step 1234 (12.3% withdrawn)") and #712 is open precisely because no
   gate stops a readout overflowing its box. `white-space: normal` on an auto-height grid row means
   a wider font stack (CI's DejaVu is ~15 % wider than Segoe UI on this kind of label) costs a
   second line, which the row absorbs — it cannot push text out of the panel. */
.oom-pred {
  padding: 7px 10px 3px;
  font-size: 15px; font-weight: 600; line-height: 1.25; text-align: center;
  color: var(--caution); white-space: normal;
}
.oom-pred:empty { display: none; }
.oom-msg { min-height: 16px; padding: 0 10px 8px; color: var(--muted); font-size: 11px; }
.oom-msg.warn { color: var(--caution); }
/* HELP, IN PLACE *(OWNER, 2026-09-03, #619 item 23)*. The window grows downward instead of
   opening a modal — the Scanner's idiom, chosen by directive on 2026-08-11 for the same reason
   that applies here: a modal covers the plot the player is asking about.

   CAPPED AND SCROLLING, like the Scanner's expanded box. The window is draggable and lives at a
   fixed top, so an uncapped panel could run off the bottom of a short viewport with the plot
   stranded above it. `.on` marks the button while it is open so the toggle reads as a state. */
.oom-help {
  padding: 2px 10px 10px; max-height: 260px; overflow-y: auto;
  border-top: 1px solid var(--border); color: var(--text-2); font-size: 11.5px; line-height: 1.45;
}
.oom-help p { margin: 8px 0 0; }
.oom-help p:first-child { margin-top: 8px; }
.oom-help b { color: var(--text); }
.oom-help-btn.on { color: var(--text); box-shadow: inset 0 0 0 1px var(--normal); }
.oom-frame { fill: none; stroke: var(--border); }
.oom-grid { stroke: var(--border); stroke-dasharray: 2 3; }
.oom-zero { stroke: var(--text-2); }
.oom-tick, .oom-lab { fill: var(--muted); font-size: 8px; }
.oom-lab { font-size: 9px; }
.oom-fit { stroke: var(--normal); stroke-dasharray: 5 4; stroke-width: 1.2; }
.oom-crit { stroke: var(--caution); stroke-width: 1.2; }
.oom-critlab { fill: var(--caution); font-size: 10px; font-weight: 600; }
.oom-pt { fill: var(--text); }

/* An expanded card (e.g. the NIS section at a startup lineup) can overflow its
   layout slot under a neighbor; raising the hovered card keeps every control
   reachable, and the neighbor is back on top the moment you move to it. */
.app.pwr-synoptic .plant-card:hover { z-index: 5; }

/* Feedback modal sizes to its form. (.help-modal's height:auto is defeated by
   .mission-modal's fixed height — that rule comes later in the file — so the
   compound selector here outranks it.) */
.mission-modal.fb-modal { height: auto; max-height: min(84vh, 760px); }

/* ===================================================================================
   THE INSTRUCTOR AS A TAB, AND ITS TRANSCRIPT (2026-08-11, owner items 4 and 7)
   =================================================================================== */

/* The pane fills the tab body and scrolls INSIDE the log, not as a whole — the persona
   header and the walkthrough nav have to stay put while the messages move under them. */
/* THE INSTRUCTOR PANE OWNS ITS HEIGHT, and the LOG is what scrolls — not .tab-body.

   .tab-body is `overflow-y: auto` and .tabpane.on is `display: block`, so every other pane
   is content-sized and the tab body scrolls it. That is right for a list of failures and
   wrong for a transcript: a `flex: 1 1 0` log inside a content-sized parent resolves to
   ZERO height and every message overflows an invisible box. Measured: log clientHeight 0
   against scrollHeight 1270, four messages in the DOM and one visible.

   `.instr-mode` is set on .tab-body by selectTab so this does not depend on :has(). Since
   `overflow: hidden` still makes this a live, unclamped SCROLLPORT (#723, sibling of #717):
   in the control-room grid's stacked row (`.app.pwr-synoptic` at <=1200px, see the
   `grid-template-rows` fix below) this wrapper used to be squeezed to ~20px against ~62px
   of content and could be PANNED the other 42px with no way back. `overflow: clip` is the
   same #717 backstop — it never scrolls by design, so clip costs nothing real. */
.tab-body.instr-mode { overflow: hidden; overflow: clip; display: flex; flex-direction: column; }
.tabpane.instructor { display: none; flex-direction: column; min-height: 0; }
.tabpane.instructor.on { display: flex; flex: 1 1 auto; min-height: 0; }
.tabpane.instructor .instr-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* NO PERSONA HEADER WHILE A WALKTHROUGH RUNS (#687 item 1, OWNER 2026-09-09: "Remove this
   Walkthrough text, its not needed"). The class is set by instrHeaderless() on the walkthrough
   branch of renderInstructorInner and cleared by setInstrRole, so every other panel state keeps
   its header. Removing the word alone would have left a 32 px bordered strip carrying nothing;
   the row goes with it, and the walkthrough card gets the height back. */
#instructorCard.wt-headerless .persona { display: none; }
/* END WALKTHROUGH AT THE FOOT OF THE PANEL (#687 item 2). What puts it at the bottom of the
   SPACE rather than the bottom of the card is `.instr-log { flex: 1 1 0 }` above: the log is the
   growing child, it eats every pixel of free space in this column, and this row is what is left
   at the bottom. MEASURED at 1500x950: row bottom 921, panel floor 931, transcript bottom 889.

   `margin-top: auto` IS A FALLBACK AND IS INERT TODAY — say it plainly rather than crediting it,
   because #687 named it as the mechanism in three places. Auto margins only take free space that
   flex-grow left over, and the log leaves none: MEASURED at viewport heights 950 / 1200 / 760 /
   640, `getComputedStyle(#cklBtns).marginTop` resolves to `0px` at every one, and setting it to
   0 moves the row 0 px. It earns its keep only if the log ever stops growing (#612 is about that
   column) — forced to `flex: 0 0 auto`, the same measurement has it absorb 96 / 282 / 14 px. Keep
   it; do not cite it — and note the pair is REDUNDANT BY DESIGN, which verify_e2e_ui measures:
   with the log's grow removed the auto margin holds the row down on its own (96 / 282 / 14 px at
   those heights), and only removing BOTH floats the row 97 px off the floor. */
.tabpane.instructor .instr-body > #cklBtns { flex: 0 0 auto; margin-top: auto; padding-top: 8px; }
.tabpane.instructor .instr-body > #cklBtns[hidden] { display: none; }

/* THE RUNNING WALKTHROUGH REACHES THE FLOOR OF THE COLUMN *(OWNER, 2026-09-14, #755 item 3: "Some
   of the walkthroughs windows were short making them use a scroll bar when there was a bunch of
   room below them. the tab windows should go to the bottom of the right column.")*.

   #612 fixed exactly this — for the CHECKLISTS pane, which is where the run lived at the time.
   #660 moved the run back into the INSTRUCTOR pane (`#cklRun` is a child of `.instr-body` in
   shell.html) and it inherited none of it, so the two halves of the old defect came back on the
   pane the player actually uses:
     1. `.ckl-log`'s 48vh cap applied again — a VIEWPORT fraction inside a COLUMN slot, which only
        agree by accident — so the steps scrolled in a short box.
     2. `.instr-log` below it is `flex: 1 1 0` and takes every remaining pixel, and `instrLogTick`
        returns early while a walkthrough runs (`if (cklState.key) … return`), so that growing box
        is EMPTY. That is the "bunch of room below them".
   So the run becomes the growing child and the transcript stops growing, but only under
   `.wt-headerless`, which is set for exactly the duration of a run — every other instructor state
   keeps the transcript as the grower it has always been.

   `#cklBtns` STAYS AT THE FLOOR either way, and verify_e2e_ui measures that rather than the
   mechanism: with `.instr-log` no longer growing, the row's own `margin-top: auto` takes the
   leftover — the second of the two mechanisms that test proved are redundant (2026-09-11). Here
   `#cklRun` leaves no leftover, so the row lands on the floor with no margin at all.

   THE `[hidden]` GUARD IS NOT OPTIONAL: `#cklRun` is `hidden` whenever no walkthrough is running,
   and a `display: flex` in a rule this specific beats the UA's `[hidden] { display: none }`. */
#instructorCard.wt-headerless .instr-body > #cklRun {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
#instructorCard.wt-headerless .instr-body > #cklRun[hidden] { display: none; }
#instructorCard.wt-headerless .instr-body > #cklRun .ckl-log { flex: 1 1 0; min-height: 0; max-height: none; }
#instructorCard.wt-headerless .instr-body > #instrLog { flex: 0 1 auto; }

/* THE CHECKLISTS PANE, same treatment, same reason (#612, owner playtest 2026-09-03: "the
   checklist scroll window keeps bouncing to the top and back to the step" and "the right column
   scroll window should go to the bottom of the column. There is currently an unused black space
   below it.").

   #607 moved the RUNNING checklist out of the instructor pane and into this one, which is a
   plain content-sized `.tabpane` — so it inherited neither half of the rule above. Two
   consequences, and the owner reported both as separate bugs:

     1. TWO NESTED SCROLLERS. `.tab-body` is `overflow-y: auto` and scrolled the whole pane while
        `.ckl-log` scrolled inside it at its 48vh cap. `scrollIntoView` scrolls every scrollable
        ancestor, so a step advance moved both, and #605's save/restore covers the log alone —
        the outer one snapped to the top. Hence "to the top and back to the step".
     2. DEAD SPACE. Content-sized means the pane stops where its content stops. Measured at
        1600x950: pane bottom y=724 against a 950 px viewport, 226 px of unused black below it,
        with the log capped at 456 px.

   Giving the pane the height makes `.ckl-log` the ONLY scroller and lets it reach the bottom of
   the column, which is one fix for both reports. `.ckl-mode` is set on .tab-body by selectTab,
   exactly as `.instr-mode` is, so this does not depend on :has(). The identical construct is the
   identical scrollport risk (#723) — clamp it too. */
.tab-body.ckl-mode { overflow: hidden; overflow: clip; display: flex; flex-direction: column; }
.tabpane[data-pane="checklists"] { flex-direction: column; min-height: 0; }
.tabpane[data-pane="checklists"].on { display: flex; flex: 1 1 auto; min-height: 0; }
.tabpane[data-pane="checklists"] #cklRun { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* The 48vh cap is right everywhere .ckl-log is content-sized; inside the running host it is
   what stopped the log filling the column, so it is lifted HERE ONLY. */
.tabpane[data-pane="checklists"] #cklRun .ckl-log { flex: 1 1 0; min-height: 0; max-height: none; }

/* THE LOG. `flex: 1 1 0` with `min-height: 0` is what makes it the scrolling element
   rather than growing the pane — without the min-height a flex child refuses to shrink
   below its content and the scrollbar silently moves to an ancestor. */
.instr-log {
  flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 6px; padding-right: 2px;
}
/* A frozen message. Quieter than the live one: it is history, and the eye should land on
   the newest bubble. Left rule instead of a full bubble outline — at 350px wide a boxed
   chat bubble spends most of its width on the box. */
.instr-msg {
  border-left: 2px solid var(--border); padding: 2px 0 2px 8px;
  color: var(--text-2); font-size: 12px; opacity: .85;
}
.instr-msg .m-note { font-size: 11px; }
/* The live bubble is the last child and keeps its existing look; it just stops being the
   only thing in the panel. */
.instr-log .instr-current { flex: 0 0 auto; }

/* THE TAB'S ATTENTION CUE. The pane is off screen when this fires, so the count goes on
   the button. Same grammar as the board's TRIP BLOCKS badge: a count plus a brief pulse,
   never a colour that could read as a plant condition. */
#tabbar button[data-unseen]::after {
  content: attr(data-unseen);
  margin-left: 5px; padding: 0 4px; border-radius: 7px;
  background: var(--accent, #2f81f7); color: #fff; font-size: 9px; font-weight: 700;
}
@keyframes tabAttn { 0%, 100% { box-shadow: none; } 50% { box-shadow: 0 0 0 2px var(--accent, #2f81f7) inset; } }
#tabbar button.tab-attn { animation: tabAttn .7s ease-in-out 2; }

/* THE PLAY BUTTON FLASHES WHILE PAUSED *(OWNER DIRECTIVE, 2026-08-11: "When the sim is
   paused flash the play button.")*. It carries the cue the removed SIMULATION PAUSED veil
   used to carry, which is why the veil could go: the state is still announced, by the
   control that changes it. Steady pulse rather than a few blinks — this is a standing
   condition, not an event, and it stops the moment the plant runs. */
@keyframes playPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(90, 173, 124, .55); }
  50%      { box-shadow: 0 0 0 5px rgba(90, 173, 124, 0); }
}
#playBtn.paused { animation: playPulse 1.5s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
  #playBtn.paused { animation: none; outline: 2px solid var(--running, #5aad7c); }
  #tabbar button.tab-attn { animation: none; }
}

/* ================================= CHART SETTINGS WINDOW (#454, 2026-08-11) ==============
   The anchored .chart-opts popover that stood here is GONE. It was 300 px, opened upward out
   of the chart, and deliberately did not pause; the owner's follow-up reversed all three
   ("like the plant selection menu … large and pause the sim"). The window now reuses
   .mission-overlay / .mission-modal / .mission-body wholesale, so only the two columns'
   contents need rules of their own.

   Twice in one day the popover shipped with its channel list unreachable — first clipped by
   the chart, then below the fold — and both times a check that counted elements passed. That
   is why verify_e2e_ui now measures the list's RECT rather than its row count. */
.strip-chart { position: relative; }
#chartOptsBtn.on { background: var(--bg-elevated); border-color: var(--accent, #2f81f7); }

/* left column — the chart's own options, in .mission-body's 260 px track */
.cs-opts {
  border-right: 1px solid var(--border); overflow-y: auto;
  padding: 10px 12px; display: flex; flex-direction: column; gap: 4px;
}
.cs-opts .seg { flex: 0 0 auto; }
.cs-opt { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 2px 0; }
.cs-count { font-size: 12px; color: var(--text-2); }
.cs-note { font-size: 11px; color: var(--muted); line-height: 1.45; margin: 8px 0 0; }
.cs-note b { color: var(--text-2); font-weight: 600; }

/* right column — the channel list. The FILTER and the column heads are pinned and the list
   scrolls under them: with 120 rows, a head that scrolls away leaves two unlabelled columns
   of selectors, which is the one thing this layout cannot afford to be ambiguous about. */
.cs-right { display: flex; flex-direction: column; min-height: 0; padding: 10px 12px; gap: 6px; }
.cs-filter {
  flex: 0 0 auto; box-sizing: border-box; padding: 5px 8px; font: inherit; font-size: 12px;
  background: var(--bg-elevated); color: var(--text); border: 1px solid var(--border);
  border-radius: 4px;
}
/* ONE GRID, DECLARED ONCE, shared by the head and every row — otherwise the head's columns
   and the rows' columns are two independent claims that drift apart the first time a label
   gets long. */
/* The physics column is WIDER than the indication column because its readings are longer:
   it carries the curated physics prose where one exists, and "3.12 % · 9.4 MWt" or
   "563 °F · 0 °F vs Tavg" is two facts, not one number. At equal widths the second fact
   ellipsised away on every such row — which are exactly the rows worth reading. */
.cs-head, .cs-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 116px 178px; gap: 8px; align-items: center;
}
.cs-head {
  flex: 0 0 auto; font-size: 9px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); padding: 0 6px 4px; border-bottom: 1px solid var(--border);
}
.cs-head span + span, .cs-row .cs-pick { justify-self: start; }
.cs-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px; }
.cs-grp h5 {
  margin: 10px 0 2px; font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-2); font-weight: 600;
}
.cs-row { font-size: 12px; padding: 3px 6px; border-radius: 3px; }
.cs-row:hover { background: rgba(255,255,255,.04); }
.cs-row.on { background: rgba(122, 176, 255, 0.07); }
.cs-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE SELECTOR SITS NEXT TO ITS VALUE *(OWNER DIRECTIVE: "Put a radial next to each value to
   let the user choose")* — one per side, so "both" is simply both lit. They are checkboxes
   styled as radios on purpose: the owner's word was "radial", but the required behaviour is
   two independent choices, and a real radio group cannot express both-at-once. */
.cs-pick { display: flex; align-items: center; gap: 6px; cursor: pointer; min-width: 0; }
.cs-pick input {
  flex: 0 0 auto; appearance: none; -webkit-appearance: none;
  width: 13px; height: 13px; border-radius: 50%; margin: 0;
  border: 1px solid var(--border); background: var(--bg-elevated); cursor: pointer;
}
.cs-pick input:checked { border-color: var(--accent, #7ab0ff); background: var(--accent, #7ab0ff); box-shadow: inset 0 0 0 2.5px var(--bg-panel, #121821); }
.cs-pick input:disabled { opacity: .3; cursor: default; }
.cs-pick.na { cursor: default; }
/* Tabular figures: the value beside a selector changes every broadcast, and a proportional
   font makes the whole column twitch sideways as digits change. Same rule as the lane values. */
.cs-val {
  font-family: var(--mono, ui-monospace, monospace); font-variant-numeric: tabular-nums;
  font-size: 11px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; min-width: 0;
}
.cs-pick input:checked + .cs-val { color: var(--text); }
.cs-row.diverged .cs-val { color: var(--caution, #D9A441); }

/* Walkthrough step card (#660 items 15-18): "Step X of N", a Continue that only lights when the
   step is satisfied, and the walkthrough's own one-step Rewind beside it. */
.ckl-stepno { float: right; font-size: 11px; color: var(--text-2); font-weight: normal; }
.wt-continue:not(.ready) { opacity: 0.45; animation: none; box-shadow: none; cursor: default; }
.wt-continue:not(.ready):hover { background: transparent; }
.wt-rewind[disabled] { opacity: 0.45; cursor: default; }

/* (The 1/M plot's DOCK lived here and is gone — OWNER RULING, 2026-09-13: "let's make the card
   floating and dragable like it was originally". It docked into `.right-col` for one day under
   #724 item 7, and into `.bottom-row` before that under #660 item 13. Everything the dock was
   carrying that the ruling did NOT overrule — the buttons above the plot, the 15 px prediction
   readout — moved up into the base `.oom-win` rules; `git log ui/shell.css` has the rest.
   Do not re-add a dock without a newer ruling than this one. #713.) */

/* ===================================================================================
   REDUCED MOTION, IN ONE PLACE AND AT THE END OF THE FILE (#740)
   *(OWNER RULING, 2026-09-13: "1:A, 2:A, 3:a now. I will playtest after you make these changes."
   — item 2 was #740, and option A was "distinct static geometry per signal under reduced motion,
   matching the convention the board already uses".)*

   MEASURED BEFORE THIS EXISTED (inbox/738/rm_browser.js, a real browser with
   `emulateMedia({ reducedMotion })`, reading computed `animationName` off live elements): SIX
   animations went on running for a player who had asked them not to — `gauge-alarm-flash`,
   `pulse` on `button.armed`, `scram-flash`, `status-flash`, `cklGlow` and `instrGlow`. Four others
   already stopped correctly.

   ⚠ WHY THEY ARE ALL HERE, AT THE BOTTOM, RATHER THAN NEXT TO THE RULES THEY OVERRIDE. A MEDIA
   QUERY ADDS NO SPECIFICITY. `@media (prefers-reduced-motion) { .x { animation: none } }` placed
   ABOVE `.x { animation: flash … }` loses outright, and both forms read as correct. That cost this
   repo three separate defects in one cycle — the `.bd-info` cascade in #738's parent, my own
   `bdMsgFlash` override one rule later, and these six. Rules that must WIN go last; keeping them
   together is what makes that checkable at a glance instead of per-rule.

   THE EXISTING PER-RULE OVERRIDES ARE LEFT WHERE THEY ARE. They work (their animations are
   declared above them) and moving correct code to prove a point risks more than it buys; the ones
   below are the ones that were broken. */
@media (prefers-reduced-motion: reduce) {
  .gauge.alarm .g-value,
  .pd-row.subcool .pv.alarm,
  .sys-slot.state-alarm .slot-dot,
  .sys-slot.state-alarm .slot-badge,
  .pd-scram.fired { animation: none; }

  /* `button.armed` IS A BUTTON, so it gets a ring like the other button-shaped signals rather than
     just losing its pulse. RIDGE, which is the fifth distinct line style on the board and reads as
     "raised" — apt for a control waiting on a confirming second press. */
  button.armed {
    animation: none;
    outline: 3px ridge #a13a3a;
    outline-offset: 2px;
  }

  /* THE FALLBACK IS WHERE THE BOARD LEANS HARDEST ON HUE, so these do not merely stop.
     Stopping the six was option D and the owner did not take it: take the motion away and a HUE
     becomes the only difference between "critical alarm", "a protection latch is holding this" and
     "there is a message" — the pairs a red/green or blue/yellow deficiency compresses. So each
     signal gets a distinct OUTLINE STYLE, which is geometry and survives both greyscale and any
     hue collapse.

     THE VOCABULARY IS THE BOARD'S OWN, not a fourth one: the highlight bus is a halo,
     `.ckl-watch-glow` is a dashed offset outline, and `#playBtn.paused`'s own fallback (at :2894,
     ABOVE this block) is already `outline: 2px solid`.

     DOUBLE is the loudest and belongs to the critical alarm; SOLID is the protection latch (see
     pwr_board.css); DOTTED is a message; DASHED belongs to the WALKTHROUGH pointing something out,
     in two widths — 3px "act on this", 1px "watch this".

     ⚠ THE MESSAGE WAS DASHED IN THE FIRST CUT AND THAT WAS A COLLISION. `.ckl-watch-glow` was
     `1px dashed` at an offset, all but identical, and it means something else entirely; both land
     on board tiles through `revealControl`. It is a SOLID RING since the 2026-09-14 ruling (#755
     items 7/10) so that particular collision is gone, but the message keeps DOTTED: the reason it
     moved was never the dash alone, it was that two walkthrough-adjacent signals on one board blur
     a vocabulary, and 59 steps still use one. Do not move the message back onto dashed. */
  .alarm-tile.unack.crit {
    animation: none;
    /* 5px, NOT 3px, AND THE DIFFERENCE IS VISIBLE RATHER THAN THEORETICAL. A `double` outline
       splits its width three ways, so 3px is 1px-1px-1px and the two lines merge into one thin
       stroke at button size — rendered and looked at (inbox/740/grey/), 3px came out FAINTER than
       the protection latch's 2px solid, which is backwards for the loudest signal on the board.
       5px gives 2px-1px-2px and reads as two lines. Do not trim this without re-rendering it. */
    outline: 5px double var(--critical);
    outline-offset: -5px;
    box-shadow: none;
  }

  /* THE PULSE'S FALLBACK IS THE DASH, AND THAT SURVIVES THE 2026-09-14 RULING ON PURPOSE. That
     ruling took the dash off `.ckl-watch-glow` — the DEFAULT path, which is what the owner was
     looking at — and replaced it with a solid 2 px ring, so press and watch are now separated by
     MOTION plus ring weight. Here there is no motion to separate them with, and a weight-only
     difference between two cyan rings is the hue-only failure this whole block exists to prevent.
     So the pulse keeps a 3 px dashed OUTLINE and the watch ring stays a solid BOX-SHADOW: a line
     style apart, which survives greyscale and any hue collapse. Nothing dashed reaches a player
     who has not asked for stillness.

     `.ckl-watch-glow` still needs no clause of its own — it is not animated, and giving it one
     would put the dash back on the board.

     THE PRESSED STATE (#755 item 19) IS THE SAME OUTLINE AT 1px, its own width in the same family:
     with the motion gone, "act on this" and "you did it" would otherwise be identical. Widths, not
     styles, because they are two strengths of one meaning — exactly the argument the dashed pair
     used to carry between press and watch.

     ⚠ `.ckl-step-glow` IS A BOARD-SURFACE CLASS WEARING A CHECKLIST PREFIX. It is applied through
     `revealControl` to board TILES, and since #727 to the trip-block popover rows as well — it is
     not part of the walkthrough panel's own `.ckl-*` styling. Do not re-split it on the prefix. */
  .ckl-step-glow {
    animation: none;
    outline: 3px dashed rgba(90, 240, 255, 0.95);
    outline-offset: 2px;
    box-shadow: none;
  }
  .ckl-step-glow.ckl-step-done {
    outline-width: 1px;
    box-shadow: none;
  }

  /* The recommended speed rung (#743). Its pulse is an INSET, so its fallback is too — an outer
     ring here would be clipped by `.speed`'s `overflow: hidden` and the fallback would be invisible
     for exactly the player who most needs it to be static. 2px inset against `.on`'s 1px, so the
     recommendation still outweighs plain selection with the motion gone; `.ckl-speed-rung.on`
     already drops to `.on` alone, and that rule does not need repeating here because it kills the
     animation rather than the shadow.

     ⚠ THE SECOND STOP IS NOT DECORATION — a single `inset 0 0 0 2px` COLLIDED with the instructor
     attention ring, which is already exactly that, and the gate caught it (this runner, 17/18, the
     first time it was run against this change). Two signals with the same geometry are hue-only to
     each other, which is the precise thing #740 exists to prevent. The bloom stop also keeps the
     fallback reading as the same GLOW the pulse is, rather than as a border. */
  .speed button.ckl-speed-rung {
    animation: none;
    box-shadow: inset 0 0 0 2px rgba(90, 240, 255, 0.95), inset 0 0 8px 2px rgba(90, 240, 255, 0.40);
  }

  /* The highlight bus. It is a HALO by design (see its own note above — cyan, never a fill, so a
     haloed component in alarm still reads as in alarm), and the halo is already geometry; only the
     pulse goes. */
  .instr-glow {
    animation: none;
    box-shadow: 0 0 0 2px rgba(122, 176, 255, 0.9);
  }
}
