/* Precision Instrument — machine-readable design tokens.
   design/TOKENS.md is the source of truth. Keep them in step. */

:root {
  --ground: #0b0d0f;
  --panel: #121519;
  --raised: #1a1f25;
  --etch: #2b3641;
  --blueprint: #3e6e8e;
  --steel-dim: #8a97a3;
  --steel: #c9d3db;
  --brass: #b9924a;
  --signal: #ff5a1f;

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, sans-serif;

  --t-11: 11px;
  --t-13: 13px;
  --t-15: 15px;
  --t-17: 17px;
  --t-20: 20px;

  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;

  --ease: cubic-bezier(0.2, 0, 0, 1);
  --tick: 320ms;
}

@media (prefers-color-scheme: light) {
  :root {
    --ground: #e4e7ea;
    --panel: #edeff1;
    --raised: #f7f8f9;
    --etch: #bfc9d1;
    --blueprint: #33627f;
    --steel-dim: #56646f;
    --steel: #141a20;
    --brass: #7e5f1e;
    --signal: #c93c09;
  }
}

:root[data-theme="light"] {
  --ground: #e4e7ea;
  --panel: #edeff1;
  --raised: #f7f8f9;
  --etch: #bfc9d1;
  --blueprint: #33627f;
  --steel-dim: #56646f;
  --steel: #141a20;
  --brass: #7e5f1e;
  --signal: #c93c09;
}

:root[data-theme="dark"] {
  --ground: #0b0d0f;
  --panel: #121519;
  --raised: #1a1f25;
  --etch: #2b3641;
  --blueprint: #3e6e8e;
  --steel-dim: #8a97a3;
  --steel: #c9d3db;
  --brass: #b9924a;
  --signal: #ff5a1f;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
}

body {
  background: var(--ground);
  color: var(--steel);
  font-family: var(--sans);
  font-size: var(--t-15);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

/* Every number on screen is mono and tabular. Numbers must never reflow as they change. */
.num,
.metric b,
.readout b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.label {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--steel-dim);
}

/* ---------- shell ---------- */

#app {
  display: grid;
  grid-template-rows: auto 1fr;
  height: 100%;
  max-width: 1100px;
  margin: 0 auto;
}

header {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--etch);
  flex-wrap: wrap;
}

.mark {
  font-family: var(--mono);
  font-size: var(--t-17);
  letter-spacing: 0.34em;
  color: var(--steel);
}

header select {
  margin-left: auto;
  background: var(--panel);
  color: var(--steel);
  border: 1px solid var(--etch);
  border-radius: 2px;
  font-family: var(--mono);
  font-size: var(--t-13);
  /* 44px minimum, same rule as buttons. A control you cannot hit does not exist. */
  min-height: 44px;
  padding: 0 var(--s-2);
}

main {
  display: grid;
  grid-template-rows: 1fr auto;
  min-height: 0;
}

#board-wrap {
  min-height: 0;
  min-width: 0;
  padding: var(--s-4);
}

/*
 * CSS owns the canvas size; script reads it back and matches the backing store to it.
 *
 * The other way round does not work: setting style.width and also allowing max-width to clamp
 * lets the displayed size drift from the size the view maths was computed with, which squashes
 * the drawing and — much worse — makes every pointer click land on the wrong cell.
 */
canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  border: 1px solid var(--etch);
  border-radius: 3px;
  background: var(--panel);
}

/* ---------- dock ---------- */

.dock {
  border-top: 1px solid var(--etch);
  background: var(--panel);
  padding: var(--s-3) var(--s-4);
  display: grid;
  gap: var(--s-3);
}

.row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}

button {
  font-family: var(--mono);
  font-size: var(--t-13);
  letter-spacing: 0.08em;
  color: var(--steel);
  background: var(--raised);
  border: 1px solid var(--etch);
  border-radius: 2px;
  /* 44px minimum: a control you cannot hit is a control that does not exist. */
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--s-3);
  cursor: pointer;
  transition: border-color var(--tick) var(--ease), background var(--tick) var(--ease);
}

button:hover {
  border-color: var(--steel-dim);
}

button[aria-pressed="true"] {
  border-color: var(--brass);
  color: var(--brass);
}

/* Role 3 of the accent: the Run control. */
button.run[data-running="true"] {
  border-color: var(--signal);
  color: var(--signal);
}

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

input[type="range"] {
  flex: 1;
  min-width: 140px;
  accent-color: var(--brass);
  height: 44px;
}

.metric {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--steel-dim);
  font-variant-numeric: tabular-nums;
}

.metric b {
  color: var(--steel);
  font-weight: 500;
}

/* ---------- status ---------- */

#status {
  font-size: var(--t-13);
  min-height: 1.5em;
  color: var(--steel-dim);
}

#status[data-kind="failed"] {
  color: var(--signal); /* Role 2 of the accent: an error. */
}

#status[data-kind="complete"] {
  color: var(--brass);
}

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

@media (max-width: 640px) {
  header {
    padding: var(--s-2) var(--s-3);
  }
  .dock {
    padding: var(--s-2) var(--s-3);
  }
}

/*
 * On a short screen the board wins.
 *
 * Adding the margin note grew the dock enough to push the board entirely off an iPhone SE, which
 * qa:touch caught. The dock scrolls instead: every control stays reachable at full size, and the
 * thing the game is about stays on screen.
 */
@media (max-height: 720px) {
  .dock {
    max-height: 46vh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  #board-wrap {
    padding: var(--s-2);
  }
  .margin {
    font-size: var(--t-11);
  }
}

/* Chapter brief. One sentence, set apart, never in the way. */
.brief {
  font-size: var(--t-13);
  color: var(--steel-dim);
  border-left: 2px solid var(--brass);
  padding-left: var(--s-3);
  min-height: 1.4em;
}
.brief:empty {
  display: none;
}

button:disabled {
  opacity: 0.35;
  cursor: default;
}
button:disabled:hover {
  border-color: var(--etch);
}

/* Hints are free and unlimited (docs/CHARTER.md), so they get a calm, permanent home rather
   than a modal that implies scarcity. */
.hint {
  font-size: var(--t-13);
  color: var(--steel);
  background: var(--raised);
  border-left: 2px solid var(--blueprint);
  padding: var(--s-2) var(--s-3);
  border-radius: 0 2px 2px 0;
}
.hint[data-shown="false"] {
  display: none;
}

/* Elegance badges. Cosmetic, never a lock — brass, because they mark craft rather than progress. */
.badge {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brass);
  border: 1px solid var(--brass);
  border-radius: 2px;
  padding: 2px var(--s-2);
  cursor: help;
}

/* Histograms. The bar you occupy is brass; the rest are etched structure. Ember is reserved. */
#charts {
  display: flex;
  gap: var(--s-5);
  flex-wrap: wrap;
  align-items: flex-end;
}
.chart {
  display: grid;
  gap: var(--s-1);
  min-width: 150px;
}
.bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 40px;
}
.bars i {
  flex: 1;
  min-width: 3px;
  background: var(--etch);
  border-radius: 1px 1px 0 0;
}
.bars i[data-you="true"] {
  background: var(--brass);
}
.chart-foot {
  font-size: var(--t-11);
  color: var(--steel-dim);
}
.chart-note {
  font-size: var(--t-11);
  color: var(--steel-dim);
  align-self: flex-end;
  max-width: 22ch;
  line-height: 1.35;
}

/*
 * A failure you cannot navigate to is half a failure, so this sits with the sentence that
 * explains it rather than off in the controls.
 *
 * It keeps the full 44px height. I first shrank it to 32px on the reasoning that an inline button
 * can be smaller; qa:touch failed on five devices and was right to. The rule has no small-button
 * exemption, and inventing one for the first inconvenient case is how a rule stops existing.
 */
#jump {
  min-height: 44px;
  min-width: 0;
  padding: 0 var(--s-3);
  font-size: var(--t-11);
  border-color: var(--signal);
  color: var(--signal);
}
#jump[hidden] {
  display: none;
}

/* Visually hidden but available to assistive technology. Not display:none — that removes it from
   the accessibility tree entirely, which is the opposite of the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

canvas:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
}

/* ---------- V.H.'s margin ----------
   The game's entire narrative surface. Set apart from the UI by material: a paler plate, an
   indented rule, and a hand that is not the interface's voice. Never a modal — a modal makes the
   story an interruption you dismiss. */

:root {
  --ink1: var(--steel-dim);
  --ink2: var(--brass);
  --ink3: #c98b5e;
}
@media (prefers-color-scheme: light) { :root { --ink3: #a2542a; } }
:root[data-theme="light"] { --ink3: #a2542a; }
:root[data-theme="dark"] { --ink3: #c98b5e; }

.margin {
  border-left: 2px solid var(--etch);
  padding: var(--s-2) 0 var(--s-2) var(--s-3);
  display: grid;
  gap: 2px;
  max-width: 62ch;
}
.margin:empty { display: none; }

.margin .bay {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--steel-dim);
}

.margin .hand {
  font-family: var(--mono);
  font-size: var(--t-13);
  line-height: 1.75;
}
.margin .hand[data-ink="1"] { color: var(--ink1); }
.margin .hand[data-ink="2"] { color: var(--ink2); }
.margin .hand[data-ink="3"] { color: var(--ink3); }

.margin .hand s {
  opacity: 0.5;
  text-decoration-thickness: 1px;
}

.margin .sig {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.14em;
  color: var(--ink3);
  text-align: right;
  max-width: 62ch;
}

/* ---------- the commissioning chart ----------
   The level map, refused as candy. Bays are plates; machines are nodes on a drawn line; the
   states speak the board's own language: etch = locked, steel = open, brass = signed off. */

#chart-screen {
  overflow-y: auto;
  padding: var(--s-4);
  display: grid;
  gap: var(--s-4);
  align-content: start;
}
#chart-screen[hidden] { display: none; }
#game-screen[hidden] { display: none; }

.chart-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-3);
}
.chart-count { font-size: var(--t-13); color: var(--steel); }

.bay-plate {
  background: var(--panel);
  border: 1px solid var(--etch);
  border-radius: 3px;
  padding: var(--s-4);
  display: grid;
  gap: var(--s-3);
}
.bay-plate[data-accessible="false"] { opacity: 0.55; }

.bay-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-3);
}
.bay-name {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--steel-dim);
}
.bay-tally { font-size: var(--t-13); color: var(--brass); }

.bay-opener {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-13);
  line-height: 1.7;
  max-width: 58ch;
}

.bay-field {
  position: relative;
  /* Height scales with node count via the buttons' percentage positions. */
  height: 420px;
}

.bay-path {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.path-seg {
  fill: none;
  stroke: var(--etch);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 3 5;
}
.path-seg[data-lit="true"] {
  stroke: var(--blueprint);
  stroke-dasharray: none;
}
/* The one-time draw-in when a segment lights. Tick-locked, plays once, never on revisit. */
.path-seg[data-fresh="true"] {
  stroke-dashoffset: 0;
  animation: seg-draw 320ms var(--ease);
}
@keyframes seg-draw {
  from { stroke-dasharray: 200; stroke-dashoffset: 200; }
  to { stroke-dasharray: 200; stroke-dashoffset: 0; }
}

.chart-node {
  position: absolute;
  transform: translate(-50%, -50%);
  min-width: 48px;
  min-height: 48px;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  padding: 0;
  background: var(--raised);
  border: 1px solid var(--etch);
  display: grid;
  place-items: center;
}
.chart-node[data-state="locked"] { color: var(--steel-dim); }
.chart-node[data-state="open"] { border-color: var(--steel); }
.chart-node[data-state="solved"] { border-color: var(--brass); color: var(--brass); }
.chart-node[data-state="par"] {
  border-color: var(--brass);
  color: var(--brass);
  /* The par ring: a second etched circle, the chart's only ornament. */
  box-shadow: 0 0 0 4px var(--panel), 0 0 0 5px var(--brass);
}
.chart-node[data-next="true"] { border-width: 2px; border-color: var(--steel); }
.chart-node[data-fresh="true"] { animation: node-in 320ms var(--ease); }
@keyframes node-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.node-name {
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.06em;
  color: var(--steel-dim);
  pointer-events: none;
}
.chart-node[data-state="solved"] .node-name,
.chart-node[data-state="par"] .node-name { color: var(--brass); }
.chart-node[data-state="open"] .node-name { color: var(--steel); }

#rig {
  border-top: 1px solid var(--etch);
  padding-top: var(--s-3);
  display: grid;
  gap: var(--s-2);
}

/* ---------- the field notes (manual) ---------- */
#manual {
  background: var(--panel);
  color: var(--steel);
  border: 1px solid var(--brass);
  border-radius: 3px;
  max-width: 620px;
  width: calc(100% - var(--s-5));
  max-height: 82vh;
  padding: 0;
  overflow: hidden;
}
#manual::backdrop { background: rgba(0, 0, 0, 0.6); }
.manual-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--etch);
  position: sticky;
  top: 0;
  background: var(--panel);
}
#manual-body { padding: var(--s-4); overflow-y: auto; max-height: calc(82vh - 60px); display: grid; gap: var(--s-4); }
.manual-head { display: grid; gap: var(--s-1); padding-bottom: var(--s-3); border-bottom: 1px solid var(--etch); }
.manual-head .hand { font-size: var(--t-17); }
.manual-entry { display: grid; gap: var(--s-1); }
.manual-entry .hand { margin: 0; font-family: var(--mono); font-size: var(--t-13); line-height: 1.75; }
.manual-entry .hand[data-ink="1"] { color: var(--ink1, var(--steel-dim)); }
.manual-entry .hand[data-ink="2"] { color: var(--ink2, var(--brass)); }
.manual-entry .hand[data-ink="3"] { color: var(--ink3, #c98b5e); }
.manual-entry .hand s { opacity: 0.5; }
.manual-head .sig { font-family: var(--mono); font-size: var(--t-11); letter-spacing: 0.1em; color: var(--steel-dim); }

/* ---------- bay title page (interstitial) ---------- */
#interstitial {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--ground);
  display: grid;
  place-items: center;
  padding: var(--s-5);
  cursor: pointer;
}
#interstitial[hidden] { display: none; }
.inter-plate {
  max-width: 46ch;
  display: grid;
  gap: var(--s-4);
  justify-items: start;
  text-align: left;
}
.inter-plate .label { font-size: var(--t-13); letter-spacing: 0.24em; }
.inter-plate .hand {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-20);
  line-height: 1.7;
}
.inter-plate .hand[data-ink="1"] { color: var(--ink1, var(--steel-dim)); }
.inter-plate .hand[data-ink="2"] { color: var(--ink2, var(--brass)); }
.inter-plate .hand[data-ink="3"] { color: var(--ink3, #c98b5e); }
#inter-go {
  border-color: var(--steel);
  color: var(--steel);
}
.inter-skip { color: var(--steel-dim); opacity: 0.6; }

/* ---------- subsystem identity (Plan II phase 8) ----------
   Each bay is a different bay of the same instrument. No new colours — lint:design forbids them.
   Identity comes from material: the plate edge, the header rule, the corner treatment. Same
   palette throughout; a different metal caught at a different angle. */

.bay-plate[data-chapter="1"] { border-top: 2px solid var(--steel-dim); }
.bay-plate[data-chapter="2"] { border-top: 2px solid var(--blueprint); }
.bay-plate[data-chapter="3"] { border-top: 2px solid var(--brass); }
.bay-plate[data-chapter="4"] {
  border-top: 2px solid var(--brass);
  background: linear-gradient(180deg, color-mix(in srgb, var(--brass) 5%, var(--panel)), var(--panel) 40%);
}
.bay-plate[data-chapter="5"] {
  border-top: 2px solid var(--steel);
  background: linear-gradient(180deg, color-mix(in srgb, var(--steel) 6%, var(--panel)), var(--panel) 40%);
}
/* The unfinished bay: unfilled. An open frame, no fill, a dashed edge — a room left deliberately
   bare. It should read as different from every completed bay at a glance. */
.bay-plate[data-chapter="6"] {
  border: 1px dashed var(--brass);
  border-top-width: 2px;
  background: transparent;
}
.bay-plate[data-chapter="6"] .bay-name { color: var(--brass); }

/* ---------- the chassis (Plan II phase 7) ----------
   The board is set into a plate, not floated in a border. Milled edge, chamfered inner, four
   fastener notches, the serial etched into the metal. This is the game's furniture. */

/*
 * The chassis fills the board area; the canvas fills the chassis interior. Every element in the
 * chain has a definite size, so the sizing does not collapse — an earlier version used
 * place-items:center and shrank the canvas to 124px, because a centred grid item is content-sized
 * and the canvas was 100% of that content-sized box.
 */
#board-wrap { display: grid; min-height: 0; min-width: 0; padding: var(--s-3); }
#chassis {
  position: relative;
  min-height: 0;
  min-width: 0;
  padding: var(--s-4);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--steel) 4%, var(--panel)), var(--panel) 60%);
  border: 1px solid var(--etch);
  border-radius: 5px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--steel) 8%, transparent),
              inset 0 1px 0 color-mix(in srgb, var(--steel) 10%, transparent);
  display: grid;
}
#chassis canvas {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  border: 1px solid var(--etch);
  border-radius: 2px;
  background: var(--ground);
}
.chassis-serial {
  position: absolute;
  left: var(--s-4);
  bottom: calc(var(--s-4) - 16px);
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: 0.28em;
  color: var(--steel-dim);
  opacity: 0.75;
  pointer-events: none;
  user-select: none;
}
.fastener {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--steel) 12%, var(--panel));
  box-shadow: inset 0 0 0 1px var(--etch);
}
.fastener.tl { top: 5px; left: 5px; }
.fastener.tr { top: 5px; right: 5px; }
.fastener.bl { bottom: 5px; left: 5px; }
.fastener.br { bottom: 5px; right: 5px; }

/* ---------- workshop ambience (Plan II phase 9) ----------
   A faint light-shaft across the chassis and a slow-drifting mote, at the very edge of notice.
   Nothing on the board; nothing that could be mistaken for a token. Paused entirely while the
   simulation runs — the `data-running` attribute on #chassis stops all of it, so ambient motion
   can never be read as causality. Also honours prefers-reduced-motion. */

#chassis::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 5px;
  pointer-events: none;
  background: linear-gradient(
    115deg,
    transparent 30%,
    color-mix(in srgb, var(--steel) 3%, transparent) 45%,
    transparent 60%
  );
  background-size: 260% 100%;
  background-position: 130% 0;
  animation: shaft 12800ms linear infinite; /* qa-motion-ok: ambient, pauses during sim, 40 ticks */
  opacity: 0.7;
}
@keyframes shaft {
  from { background-position: 140% 0; }
  to { background-position: -40% 0; }
}
/* A single drifting mote, top-right, barely there. */
#chassis::after {
  content: "";
  position: absolute;
  top: 12%;
  right: 14%;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--steel) 40%, transparent);
  pointer-events: none;
  animation: mote 25600ms ease-in-out infinite; /* qa-motion-ok: ambient, pauses during sim, 80 ticks */
  opacity: 0.5;
}
@keyframes mote {
  0% { transform: translate(0, 0); }
  50% { transform: translate(-8px, 14px); }
  100% { transform: translate(0, 0); }
}
/* The instant the simulation runs, all ambience stops. This is the rule that keeps it honest. */
#chassis[data-running="true"]::before,
#chassis[data-running="true"]::after {
  animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
  #chassis::before,
  #chassis::after { animation: none; }
}

/* ---------- the binder / journal (Plan II phases 15, 17) ---------- */
#journal-screen { overflow-y: auto; padding: var(--s-4); display: grid; gap: var(--s-4); align-content: start; }
#journal-screen[hidden] { display: none; }
.journal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--s-4);
}
.journal-card {
  margin: 0;
  background: var(--panel);
  border: 1px solid var(--etch);
  border-radius: 3px;
  overflow: hidden;
  display: grid;
}
.journal-thumb { width: 100%; height: auto; display: block; background: var(--ground); border-bottom: 1px solid var(--etch); }
.journal-card figcaption { padding: var(--s-3); display: grid; gap: var(--s-2); }
.journal-name { font-family: var(--mono); font-size: var(--t-13); letter-spacing: 0.06em; color: var(--steel); }
.journal-metrics { font-size: var(--t-11); color: var(--steel-dim); }
.journal-badges { display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* The part budget readout. Over budget shows in ember — role 2, an error, which it is. */
.budget[data-over="true"] b, .budget[data-over="true"] { color: var(--signal); }
.budget { color: var(--brass); }
