/* One sans for reading, one mono for anything the engine counted. Leaf green is
   the only accent, and it means "you chose this" or "this got filled". Ask-side
   data is graphite, so the two sides never both look like the accent. */
:root {
  color-scheme: light;
  --paper: #f7f8f4;
  --panel: #ffffff;
  --ink: #1b211d;
  --ink-2: #3a423c;
  --muted: #536058;
  --subtle: #6b726b;
  --rule: #d9ded7;
  --rule-soft: #e9ece6;
  --grid: #ecefe8;
  --leaf: #426849;
  --leaf-deep: #315638;
  --leaf-wash: #e9f0e8;
  --graphite: #252c27;
  --graphite-2: #3d453f;
  /* The only text that sits on graphite. Named so the one dark surface stays
     one surface rather than becoming a place where colours are invented. */
  --band-label: #8fbf9d;
  --band-text: #dfe4de;
  --refused: #7c5341;
  --refused-wash: #f6efe9;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  /* The chart bands are trimmed so a value at the deepest bar tip has room
     before the queue text starts. chartBand() in app.js reads this same value,
     so the drawn geometry and the gutter cannot drift apart. */
  --chart-span: 86%;
  font-family: var(--sans);
  color: var(--ink);
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  /* A ruled sheet behind the panels. The panels are opaque, so the grid only
     shows in the gutters and never runs through chart data. */
  background-color: var(--paper);
  background-image:
    linear-gradient(to right, var(--grid) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
  background-size: 68px 68px;
}

button,
input,
select {
  font: inherit;
}

a {
  color: var(--leaf-deep);
}

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

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- page frame ---------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1320px;
  min-height: 58px;
  margin: 0 auto;
  padding: 0 32px;
}

.wordmark {
  color: var(--ink);
  font-size: 17px;
  font-weight: 750;
  letter-spacing: -0.04em;
  text-decoration: none;
}

.wordmark span {
  color: var(--leaf);
}

.repo-link {
  display: inline-block;
  padding: 7px 12px;
  border-radius: 0;
  background: var(--ink);
  color: #fff;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.01em;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
}

.repo-link:hover {
  background: #000;
}

.page-shell {
  width: min(100% - 64px, 1240px);
  margin: 0 auto;
  padding: 18px 0 40px;
}

h1,
h2,
p {
  margin-top: 0;
}

h1 {
  max-width: 20ch;
  margin-bottom: 20px;
  font-size: clamp(32px, 3.6vw, 47px);
  font-weight: 780;
  letter-spacing: -0.038em;
  line-height: 1.03;
}

.inline-link {
  color: var(--leaf-deep);
  text-decoration-color: var(--leaf);
  text-underline-offset: 3px;
}

/* Two columns at wide widths: one narrow column of prose leaves half the page
   empty and pushes the transport below the fold. */
.intro-copy {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 36px;
  max-width: 1120px;
}

.intro-copy p {
  margin-bottom: 0;
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.62;
}

/* The counterfactual, offered where the intro makes the claim it supports. It
   is hidden until the second trace is in hand, and both traces are fetched
   before the first render, so the row does not arrive late and push the
   transport down the page. */
.intro-action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin: 16px 0 0;
}

.intro-action[hidden] {
  display: none;
}

.scenario-note {
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
}

.scenario-switch {
  display: inline-flex;
  gap: 4px;
}

.scenario-button {
  min-height: 27px;
  padding: 0 11px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink-2);
  cursor: pointer;
  font-size: 12px;
  transition: background-color 130ms ease, border-color 130ms ease, color 130ms ease;
}

.scenario-button:hover:not([aria-pressed="true"]) {
  border-color: var(--subtle);
  color: var(--ink);
}

/* Pressed is the current run, and it is the only thing on this row that carries
   the accent, so which of the two is showing reads without colour blindness
   help: the two labels are different words. */
.scenario-button[aria-pressed="true"] {
  border-color: var(--leaf);
  background: var(--leaf-wash);
  color: var(--leaf-deep);
  font-weight: 600;
}

/* ---------- transport ---------- */

/* One card, one row. The counter and the hint sit at the far end of the same
   line as the controls and wrap below them only when the row runs out of room,
   which is what keeps the card a single object instead of a bar with a caption
   under it. */
.transport {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin-top: 26px;
  padding: 12px 16px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--panel);
}

.transport-buttons {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}

.control-button {
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink-2);
  cursor: pointer;
  font-size: 12px;
  transition: background-color 130ms ease, border-color 130ms ease, color 130ms ease;
}

.control-button:hover:not(:disabled) {
  border-color: var(--leaf);
  background: var(--leaf-wash);
  color: var(--leaf-deep);
}

.control-button:disabled {
  color: var(--subtle);
  cursor: not-allowed;
}

.play-button {
  min-width: 62px;
  border-color: var(--leaf);
  background: var(--leaf);
  color: #fff;
}

.play-button:hover:not(:disabled) {
  border-color: var(--leaf-deep);
  background: var(--leaf-deep);
  color: #fff;
}

.progress-control {
  display: grid;
  grid-template-columns: auto minmax(90px, 1fr);
  align-items: center;
  gap: 14px;
  flex: 1 1 320px;
  max-width: 460px;
}

.progress-label {
  color: var(--subtle);
  font-size: 11px;
  white-space: nowrap;
}

input[type="range"] {
  width: 100%;
  height: 20px;
  margin: 0;
  accent-color: var(--leaf);
  cursor: pointer;
}

/* The counter sits immediately after the slider, so the pair reads as one
   control rather than as two ends of a stretched bar. */
.step-counter {
  margin: 0 0 0 auto;
  text-align: right;
  flex: 0 0 auto;
}

.step-number {
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  white-space: nowrap;
}

/* Holds the arrow-key hint at the far end of the row. The current event is not
   repeated here: the tape, the selected card and its summary already state it,
   and the card's live region is what announces a step. */
.transport-foot {
  display: flex;
  flex: 1 1 auto;
  align-items: baseline;
  justify-content: flex-end;
  gap: 16px;
  color: var(--subtle);
  font-size: 11.5px;
  white-space: nowrap;
}

.keyboard-hint {
  white-space: nowrap;
}

/* ---------- workbench ---------- */

.workbench {
  display: grid;
  grid-template-columns: minmax(248px, 292px) minmax(0, 1fr);
  align-items: start;
  gap: 16px;
  margin-top: 20px;
}

.rail {
  display: grid;
  gap: 16px;
}

.panel {
  min-width: 0;
  padding: 16px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--panel);
}

.panel-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  min-height: 14px;
}

/* One label style for the whole page, used three or four times per screen at
   most. Nothing else is uppercase. */
.panel-label {
  margin: 0;
  color: var(--subtle);
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* ---------- event tape ---------- */

.event-list {
  display: grid;
  gap: 2px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.event-choice {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 9px;
  width: 100%;
  min-height: 33px;
  padding: 7px 9px 7px 8px;
  border: 0;
  border-left: 3px solid transparent;
  border-radius: 0 4px 4px 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  text-align: left;
  transition: background-color 130ms ease, border-color 130ms ease, color 130ms ease;
}

.event-choice:hover {
  background: var(--paper);
  color: var(--ink);
}

.event-choice[aria-current="step"] {
  border-left-color: var(--leaf);
  background: var(--leaf-wash);
  color: var(--ink);
}

.event-index {
  color: var(--subtle);
  font-family: var(--mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

.event-choice[aria-current="step"] .event-index {
  color: var(--leaf-deep);
}

.event-label {
  overflow: hidden;
  font-size: 12.5px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A refusal is stated, not just tinted, so it survives without colour. */
.event-flag {
  padding: 2px 6px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  color: var(--refused);
  background: var(--refused-wash);
  font-size: 10px;
  letter-spacing: 0.04em;
}

/* ---------- selected event ---------- */

/* This panel and the fills panel below it are the two whose content varies by
   step, and a box that resizes while the reader is stepping is a box they watch
   instead of the data, so their heights are fixed before stepping starts.
   measureRailBoxes() in app.js decides them, from the tallest step in the run on
   screen. Hard-coding the worst case any run reaches kept them still but left
   40px of dead space at the densest step of the default run and 120px at the
   emptiest, which is most of the page's slack. Nothing here sets a height, so
   the panels are content-sized until the trace arrives. */

.event-summary {
  margin: 13px 0 8px;
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -0.022em;
  line-height: 1.34;
}

.animate-step .event-summary {
  animation: event-arrive 200ms ease-out both;
}

.event-explanation {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.58;
}

/* The question to answer before pressing Next. It only appears when the next
   event fills against more than one resting order, so it is never asked when
   there is no queue decision to make.
   Hidden with visibility rather than display, so the panel it lives in keeps
   one height and the rail never shifts as the reader steps. */
.prediction {
  display: grid;
  gap: 5px;
  visibility: hidden;
  margin: 14px 0 0;
  padding: 11px 13px;
  border-left: 3px solid var(--leaf);
  border-radius: 0 4px 4px 0;
  background: var(--leaf-wash);
  color: var(--leaf-deep);
  font-size: 12.5px;
  line-height: 1.5;
}

.prediction.is-asked {
  visibility: visible;
}

.prediction-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* ---------- fills ---------- */

/* Height set the same way as the selected panel above. The worst case in any
   reachable scenario is two receipts, and this run may have none at all. */

.empty-trades {
  margin: 13px 0 0;
  color: var(--subtle);
  font-size: 12.5px;
}

.trade-list {
  display: grid;
  gap: 0;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.trade-item {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  min-height: 38px;
  padding: 6px 0 6px 9px;
  border-top: 1px solid var(--rule-soft);
  border-left: 3px solid var(--leaf);
}

.trade-list.is-animating .trade-item {
  animation: trade-in 240ms ease-out both;
}

.trade-index {
  color: var(--leaf);
  font-family: var(--mono);
  font-size: 10px;
}

.trade-counterparty {
  color: var(--ink);
  font-size: 12.5px;
}

.trade-values {
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- order book ---------- */

/* No side padding, so the price table and the two charts run the panel's full
   width. The band that used to close this panel now opens the source section
   below it, so the panel carries its own bottom padding again. */
.book-panel {
  padding: 16px 0 18px;
}

.book-heading {
  align-items: center;
  padding: 0 16px;
}

.best-prices {
  margin: 0;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Five columns: the queue tickets sit in the outer columns and the chart owns
   the inner bands, so a value printed at a bar tip can never land on an order
   id. The price column between them is the chart's shared axis, drawn as a
   continuous band the bars grow out of. */
.book-columns,
.book-row,
.depth-axis {
  display: grid;
  grid-template-columns:
    minmax(0, 1fr) minmax(0, 1.2fr) 66px minmax(0, 1.2fr) minmax(0, 1fr);
  align-items: stretch;
}

.book-columns {
  align-items: center;
  margin-top: 16px;
  padding: 0 16px 9px;
  border-bottom: 1px solid var(--rule);
  color: var(--subtle);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.book-columns > :first-child {
  grid-column: 1 / span 2;
}

.book-columns > :last-child {
  grid-column: 4 / span 2;
}

.book-columns .price-heading {
  text-align: center;
}

.ask-heading {
  text-align: left;
}

.price-ladder {
  display: grid;
}

/* Tall enough for three orders resting at one price, which is the most any
   shipped scenario reaches. A row that grows when a third order arrives moves
   the caption, the history chart and the source section under it by 4px, so the
   floor is set at the measured worst case rather than at one ticket's worth. */
.book-row {
  min-height: 70px;
  padding: 0 16px;
  border-bottom: 1px solid var(--rule-soft);
}

.book-row:last-child {
  border-bottom: 0;
}

.ladder-wrap {
  position: relative;
}

.depth-chart {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.depth-area-buy {
  fill: var(--leaf-wash);
}

.depth-area-ask {
  fill: #eef0ed;
}

.depth-edge {
  fill: none;
  stroke-width: 1.25;
}

.depth-edge-buy {
  stroke: var(--leaf);
}

.depth-edge-ask {
  stroke: var(--graphite-2);
}

.depth-max-line {
  stroke: var(--rule-soft);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* The value at each bar tip. Muted and heavier than the queue text on purpose:
   at full depth the tip sits a few pixels from an order id, and at the same
   size and colour in the same font the two read as one token. */
.depth-tip {
  fill: var(--muted);
  font-family: var(--mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.depth-ghost {
  opacity: 0;
  transition: opacity 520ms ease-out;
}

.depth-ghost.is-visible {
  opacity: 0.5;
}

/* The new edge grows out from the price axis. app.js measures the path, sets
   the dash lengths, and releases the offset on the next frame. */
.depth-edge.is-drawing,
.history-line.is-drawing {
  transition: stroke-dashoffset 380ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Where depth left the book, and at which price. The stroke covers exactly the
   span the previous profile had and the new one does not, so it is read as
   size that was taken rather than as decoration on the row. */
.sweep-line {
  stroke-width: 2;
  stroke-linecap: round;
}

.sweep-line-buy {
  stroke: var(--leaf);
}

.sweep-line-ask {
  stroke: var(--graphite-2);
}

.depth-sweep .sweep-line {
  animation: sweep-draw 620ms ease-out both;
}

.history-dot.is-arriving {
  animation: dot-arrive 260ms ease-out both;
}

.side-cell {
  position: relative;
  min-width: 0;
  display: grid;
  align-content: center;
  gap: 5px;
  padding: 8px 12px;
}

.side-cell.buy-side {
  text-align: right;
}

/* The drawn value, kept in the accessibility tree and in the DOM for the chart
   test, since the visible label lives inside the SVG. */
.depth-value {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ticket-list {
  display: grid;
  gap: 3px;
  justify-items: start;
}

.buy-side .ticket-list {
  justify-items: end;
}

/* One resting order. The position number is the queue the page is about, so it
   is stated in text rather than implied by colour. */
.ticket {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-height: 16px;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

.ticket-position {
  min-width: 9px;
  color: var(--subtle);
  font-size: 10px;
  text-align: center;
}

.buy-side .ticket-orders {
  color: var(--leaf);
}

.sell-side .ticket-orders {
  color: var(--graphite-2);
}

/* The size this order just gave up. The slot is always present so nothing in
   the row moves sideways when a fill arrives. */
.ticket-delta {
  min-width: 20px;
  color: var(--leaf);
  font-size: 11px;
  font-weight: 650;
}

.chart-cell {
  display: block;
  align-self: stretch;
}

.price-cell {
  display: grid;
  place-items: center;
  background: var(--paper);
  border-inline: 1px solid var(--rule-soft);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  text-align: center;
}

.animate-step .book-row.is-touched .price-cell {
  animation: price-touch 700ms ease-out 120ms both;
}

.depth-axis {
  align-items: center;
  padding: 6px 16px 0;
}

/* The scale is the replay's deepest depth, parked where a maximum bar ends.
   --chart-span keeps it there when the bands are trimmed. */
.axis-scale {
  width: var(--chart-span);
  color: var(--subtle);
  font-family: var(--mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.buy-axis {
  grid-column: 2;
  margin-left: auto;
  text-align: left;
}

.sell-axis {
  grid-column: 4;
  text-align: right;
}

.axis-caption,
.history-caption {
  margin: 8px 0 0;
  padding: 0 16px;
  color: var(--subtle);
  font-size: 11px;
  line-height: 1.45;
}

/* The − note explains a mark only some steps carry. It is revealed rather than
   shown, because below 660px it wraps to a second line, and a caption that
   grows by a line when a fill lands moves everything under it. */
.caption-note {
  visibility: hidden;
}

.caption-note.is-shown {
  visibility: visible;
}

.depth-history {
  margin: 18px 0 0;
}

/* The caption is the chart's label, so it sits above it and the panel ends on
   the drawing rather than on a line of grey text. */
.history-caption {
  margin: 0 0 4px;
}

.history-chart {
  display: block;
  width: calc(100% - 32px);
  height: 96px;
  margin: 0 16px;
  overflow: visible;
}

.history-line {
  fill: none;
  stroke-width: 1.5;
  stroke-linejoin: round;
}

.history-line-bid {
  stroke: var(--leaf);
}

.history-line-ask {
  stroke: var(--graphite-2);
}

.history-dot {
  stroke-width: 1.25;
}

.history-dot-bid {
  fill: var(--panel);
  stroke: var(--leaf);
}

.history-dot-ask {
  fill: var(--panel);
  stroke: var(--graphite-2);
}

.history-cursor {
  stroke: var(--ink);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* ---------- the code behind the numbers ---------- */

/* The document sits on the page, not on a card, the way the title and the intro
   sit on the page. A card here put a white canvas under a column of text and
   turned the margin beside it into space the card had failed to fill, which is
   what the empty right of this section was. The only chrome left is the band. */
.code-panel {
  margin-top: 34px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

/* Square corners, because it is a section band and not a card, and every card
   on this page is 6px. */
.code-band {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 5px 12px;
  padding: 14px 17px 16px;
  background: var(--graphite);
}

.band-label {
  flex: 0 0 auto;
  margin: 0;
  color: var(--band-label);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.code-band p {
  margin: 0;
  color: var(--band-text);
  font-size: 12.5px;
  line-height: 1.45;
}

.code-band code {
  font-family: var(--mono);
  font-size: 11.5px;
}

/* A document rather than a card grid: a heading, a paragraph, the excerpt, and
   where it came from. The rules between items run the full width of the card and
   the text sits in a column under them, so the empty right of the section reads
   as the measure a document is set to rather than as a card that ran out. */
/* No side padding, so the document lines up with the title, the cards and the
   band above it rather than sitting inset inside something. */
.code-body {
  padding: 22px 0 0;
}

.code-item + .code-item,
.code-tradeoff {
  margin-top: 22px;
  padding-top: 20px;
  /* A rule rather than rule-soft: on the page ground, beside the faint grid, a
     soft rule is nearly invisible and the sections run together. */
  border-top: 1px solid var(--rule);
}

/* The excerpt sits beside the prose rather than under it, so the paragraph reads
   as a note on the code instead of a caption above it. The heading spans both,
   because it is the subject of the whole item. The prose track has a floor of
   320px, which is where the two-column form stops fitting and the excerpt moves
   back under its own paragraph. */
.code-item {
  display: grid;
  /* A fixed width for the code column, not max-content: the excerpts are each as
     wide as their own longest line, so a content-sized column would right-align
     them and every item would start its code at a different x. */
  grid-template-columns: minmax(320px, 1fr) minmax(0, 640px);
  grid-template-areas:
    "heading heading"
    "prose   excerpt";
  align-items: start;
  gap: 12px 30px;
}

.code-heading {
  grid-area: heading;
  margin: 0;
  color: var(--ink);
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.012em;
}

.code-prose {
  grid-area: prose;
  max-width: 580px;
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.58;
}

.code-excerpt {
  grid-area: excerpt;
  display: grid;
  justify-items: start;
  gap: 7px;
  min-width: 0;
  margin: 0;
}

.code-ref {
  color: var(--subtle);
  font-family: var(--mono);
  font-size: 11px;
}

.code-ref:hover {
  color: var(--leaf-deep);
}

/* Each block is as wide as its own longest line, so a short excerpt is a short
   block and the right of the section is a rag instead of a row of empty boxes.
   It scrolls rather than wraps: a wrapped line of C++ is a line the reader has
   to reconstruct. */
.code-block {
  width: fit-content;
  max-width: min(660px, 100%);
  margin: 0;
  padding: 11px 13px;
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: 6px;
  /* White, like every other card on the page, because an excerpt is code and
     code is a thing you look at inside a box rather than a plate laid on the
     page. The rest of this section is prose on the page. */
  background: var(--panel);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.6;
  tab-size: 2;
}

/* Four tones out of the page's own greys, and weight. The leaf accent already
   means something specific, so the highlight is structure in grey: keywords and
   calls carry the skeleton, types step down, comments recede. */
.tok-kw {
  color: var(--ink);
  font-weight: 650;
}

.tok-fn {
  color: var(--ink);
}

.tok-type {
  color: var(--muted);
}

.tok-comment {
  color: var(--subtle);
  font-style: italic;
}

.tok-str {
  color: var(--muted);
}

/* ---------- parameters (loopback dev server only) ---------- */

/* Full width between the transport and the replay, not a rail card. The book
   column cannot grow to match a card this tall, so in the rail it left the
   whole right column empty below the book. */
.parameter-panel {
  margin-top: 16px;
}

.parameter-panel[hidden] {
  display: none;
}

.parameter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 10px 14px;
  margin-top: 14px;
}

.parameter-grid label,
.parameter-toggle {
  display: grid;
  gap: 5px;
  color: var(--muted);
  font-size: 11px;
  line-height: 1.3;
}

.parameter-grid input,
.parameter-grid select {
  width: 100%;
  min-height: 30px;
  padding: 0 7px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
}

.parameter-grid input:disabled {
  background: var(--paper);
  color: var(--subtle);
}

.parameter-toggle {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  font-size: 12px;
}

.parameter-toggle input {
  width: 15px;
  height: 15px;
  accent-color: var(--leaf);
}

.parameter-note {
  margin: 11px 0 0;
  color: var(--subtle);
  font-size: 11px;
  line-height: 1.5;
}

/* ---------- error ---------- */

.load-error {
  margin: 20px 0 0;
  padding: 13px 15px;
  border: 1px solid var(--refused);
  border-left-width: 3px;
  border-radius: 4px;
  background: var(--refused-wash);
  color: #4d3327;
  font-size: 12.5px;
}

@keyframes event-arrive {
  from { opacity: 0.4; transform: translateY(5px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes price-touch {
  0% { background-color: var(--panel); }
  26% { background-color: var(--leaf-wash); }
  100% { background-color: var(--panel); }
}

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

/* Draws the swept span in from the old tip and fades it out, so the reader sees
   the size leave the book before it is gone. */
@keyframes sweep-draw {
  0% { opacity: 0; }
  22% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes dot-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (max-width: 1080px) {
  .page-shell {
    width: min(100% - 48px, 1000px);
  }

  .workbench {
    grid-template-columns: minmax(230px, 258px) minmax(0, 1fr);
    gap: 14px;
  }

  /* Below here the prose track would fall under its 320px floor, so the excerpt
     goes back under its own paragraph and the two read as a document again. */
  .code-item {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "heading"
      "prose"
      "excerpt";
  }

}

/* Below this the ladder's five columns stop being legible, so the rail
   dissolves and the page becomes one column: pick an event, read the book,
   then read what it did. */
@media (max-width: 900px) {
  .workbench {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "tape" "book" "selected" "trades";
    gap: 14px;
    margin-top: 16px;
  }

  .rail {
    display: contents;
  }

  .event-panel { grid-area: tape; }
  .book-panel { grid-area: book; }
  .selected-panel { grid-area: selected; }
  .trades-panel { grid-area: trades; }

  /* The tape turns on its side here. Above this width it is a column beside the
     book, so a step and the graph are already side by side; below it the stack
     sat between the buttons and the book and pushed the graph most of a phone
     screen away from the control that steps it. Sideways it is one row, and the
     buttons, the steps and the ladder are on screen at once.

     The padding is for the focus ring, which the scroller would otherwise clip:
     a 2px outline with a 2px offset needs 4px on every side. The negative
     horizontal margin puts the first chip back on the panel's text edge. */
  .event-list {
    display: flex;
    gap: 3px;
    margin: 10px -4px 0;
    padding: 5px 4px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .event-list::-webkit-scrollbar {
    display: none;
  }

  .event-list > li {
    flex: none;
  }

  /* Sized by its own label rather than by the row, because the row is now a
     scroller and a chip that stretched would scroll forever. The height matches
     the transport buttons, so the two rows of the band hit the same. */
  .event-list .event-choice {
    width: auto;
    min-height: 32px;
    padding: 5px 9px 5px 8px;
    grid-template-columns: 20px max-content auto;
  }

  .event-list .event-label {
    max-width: 150px;
  }

  .intro-copy {
    max-width: none;
  }

  /* One step down keeps the longest quote inside its block on a phone. Below
     about 620px even this scrolls, which is what code should do. */
  .code-block {
    font-size: 11px;
  }
}

@media (max-width: 660px) {
  .topbar {
    min-height: 52px;
    padding: 0 16px;
  }

  .page-shell {
    width: calc(100% - 32px);
    padding-top: 20px;
  }

  h1 {
    max-width: none;
    font-size: 33px;
  }

  .intro-copy {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px 0;
  }

  .transport {
    gap: 9px 14px;
    margin-top: 22px;
    padding: 12px 13px;
  }

  .progress-control {
    /* Two rows on a phone, not three: the slider keeps its label beside it and
       the counter joins it at the end, which is where the reader looks after
       dragging. The buttons keep a row of their own and fill it. */
    flex: 1 1 150px;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 10px;
  }

  .step-counter {
    margin-left: auto;
  }

  .transport-buttons .control-button {
    flex: 1;
  }

  .panel {
    padding: 14px;
  }

  .book-panel {
    padding: 14px 0 16px;
  }

  .book-heading {
    align-items: flex-start;
    flex-direction: column;
    gap: 5px;
    padding: 0 14px;
  }

  .best-prices {
    text-align: left;
  }

  /* The five columns get their share from a phone width, not a laptop one. The
     queue text wins the extra room, because the queue is the lesson and the bar
     is only a shape for the number printed at its tip. */
  .book-columns,
  .book-row,
  .depth-axis {
    grid-template-columns:
      minmax(0, 1.4fr) minmax(0, 0.85fr) 44px minmax(0, 0.85fr) minmax(0, 1.4fr);
  }

  .code-block {
    font-size: 11px;
  }

  .book-columns,
  .book-row,
  .depth-axis,
  .axis-caption,
  .history-caption {
    padding-right: 14px;
    padding-left: 14px;
  }

  /* The panel's own padding is set below, so the band still runs edge to edge
     and the padding moves inside to 14px. */
  .code-panel {
    padding: 0;
  }

  .code-body {
    padding: 18px 0 0;
  }

  .book-columns {
    font-size: 8.5px;
    letter-spacing: 0.05em;
  }

  /* The row floor stays at 70px here. A phone wants the ladder shorter, but the
     narrow ticket is the same height, and a row that grows when a third order
     lands moves everything under it. */

  .side-cell {
    padding: 8px 5px;
  }

  .ticket {
    gap: 5px;
    font-size: 10.5px;
  }

  .ticket-position {
    min-width: 8px;
    font-size: 9.5px;
  }

  .ticket-delta {
    min-width: 16px;
    font-size: 10.5px;
  }

  /* An order id and its size must never break apart, even if the row gets
     tighter than the design widths. */
  .ticket-orders,
  .ticket-position,
  .ticket-delta {
    white-space: nowrap;
  }

  /* There are no arrow keys at this width, so the hint and its row are gone and
     the card is buttons, slider, counter. */
  .transport-foot {
    display: none;
  }

  /* Narrow columns leave the deepest tip label only a few pixels from the queue
     text, so trim the bands further to buy the gutter back. */
  :root {
    --chart-span: 78%;
  }

  .history-chart {
    width: calc(100% - 28px);
    margin: 0 14px;
  }

  .event-summary {
    margin-top: 12px;
  }

  /* A phone fits about two of these at a time, so the chip gives up some of its
     chrome. A truncated label would be worse than a shorter run of them. Both
     selectors match the strip rules above, so these are the values that apply
     at this width. */
  .event-list .event-choice {
    grid-template-columns: 17px max-content auto;
    gap: 7px;
  }

  .event-list .event-label {
    max-width: 104px;
    font-size: 12px;
  }
}

/* 320px is the floor. The queue still reads top to bottom and the column header
   still says oldest first, so the position number is the first thing to go
   rather than the order id or its size. */
@media (max-width: 360px) {
  .book-columns,
  .book-row,
  .depth-axis {
    grid-template-columns:
      minmax(0, 1.55fr) minmax(0, 0.7fr) 40px minmax(0, 0.7fr) minmax(0, 1.55fr);
  }

  .ticket-position {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }
}
