/* ============================================================
   Interactive planner page.
   Reads as a technical plate set into the editorial page: hairline
   rules, ticked corners, tabular numerals, no card soup.
   ============================================================ */

.runner-page { background: var(--paper); }

.runner {
  max-width: 1320px;
  margin-inline: auto;
  padding: clamp(28px, 5vw, 64px) clamp(20px, 4vw, 56px) 0;
}

/* ── header ─────────────────────────────────────────────── */
.runner__head { position: relative; padding-bottom: 34px; margin-bottom: 34px; }
.runner__head::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: var(--rule);
}
.runner__back {
  display: inline-flex; align-items: center; gap: 8px;
  font: 400 15px/1 var(--sans); letter-spacing: -.015em; text-transform: none;
  color: var(--ink-3); text-decoration: none; margin-bottom: 26px;
  transition: color var(--dur-hover) var(--ease);
}
.runner__back .ico { transition: transform var(--dur-hover) var(--ease); }
@media (hover: hover) and (pointer: fine) {
  /* was animating `gap`, which is a layout property -- this is the same
     read with a transform, so it stays off the layout path */
  .runner__back:hover { color: var(--signal); }
  .runner__back:hover .ico { transform: translateX(-3px); }
}

.runner__title {
  font-size: clamp(2.4rem, 5.4vw, 4rem);
  line-height: .98; letter-spacing: -.028em; margin-top: 10px;
}
.runner__lede {
  margin-top: 20px; max-width: 70ch;
  font-size: 1.0625rem; color: var(--ink-2);
}

/* contents: a ruled index, not a nav bar */
/* Four columns, because there are four demos. It was three, which put the
   fourth on a second row where its own top rule ran directly under the first
   item's and read as a stray line through the block. A contents list should
   have as many columns as it has entries, or a number that divides them. */
.runner__toc {
  list-style: none; margin: 34px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 24px; max-width: 1000px; counter-reset: toc;
}
.runner__toc li {
  counter-increment: toc;
  border-top: 1px solid var(--ink); padding: 12px 22px 0 0;
}
.runner__toc li + li { border-top-color: var(--rule-2); }
.runner__toc a {
  display: block; text-decoration: none; color: var(--ink);
  font-size: 1rem; line-height: 1.25;
  transition: color .16s var(--ease);
}
.runner__toc a::before {
  content: "0" counter(toc);
  display: block; margin-bottom: 6px;
  font: 600 13px/1 var(--sans); letter-spacing: -.01em; color: var(--signal);
}
.runner__toc a:hover { color: var(--signal); }
.runner__toc span {
  display: block; margin-top: 5px;
  font: 400 14px/1.4 var(--sans); letter-spacing: -.016em; color: var(--ink-3);
}
/* 4 -> 2 -> 1, all of which divide four evenly, so no entry is ever left
   stranded on a row of its own. */
@media (max-width: 1080px) {
  .runner__toc { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .runner__toc { grid-template-columns: 1fr; }
  .runner__toc li { padding-right: 0; }
}

/* ── layout: controls beside a wide plate ───────────────── */
.runner__grid {
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  gap: clamp(24px, 3vw, 44px);
  align-items: start;
}

/* ── control panel ──────────────────────────────────────── */
.panel { position: sticky; top: 26px; }
.panel__row { padding-bottom: 22px; margin-bottom: 22px; border-bottom: 1px solid var(--rule); }
.panel__row:last-of-type { border-bottom: 0; }
.panel__label {
  font: 600 13px/1 var(--sans); letter-spacing: -.01em; text-transform: none;
  color: var(--signal); margin-bottom: 12px;
}
.panel__hint { margin-top: 10px; font-size: .8125rem; line-height: 1.5; color: var(--ink-3); }

/* Segmented control: a recessed track with one raised segment riding in it.
   The selected item is a surface that has come forward, not a slab of ink --
   which is both the platform idiom and the honest reading, since selecting a
   planner raises it above the others rather than inverting it. Dividers only
   sit between unselected neighbours, so the raised segment is never boxed in
   by a line it should be floating above. */
.seg {
  display: flex; gap: 2px;
  background: var(--paper-2);
  border: 0; border-radius: var(--r-md);
  padding: 2px;
}
.seg__btn {
  flex: 1; appearance: none; border: 0; background: transparent; cursor: pointer;
  padding: 9px 4px; border-radius: calc(var(--r-md) - 3px);
  font: 500 13px/1 var(--sans); letter-spacing: -.01em; color: var(--ink-2);
  position: relative;
  transition: background var(--dur-hover) var(--ease),
              color var(--dur-hover) var(--ease),
              box-shadow var(--dur-hover) var(--ease);
}
.seg__btn + .seg__btn::before {
  content: ""; position: absolute; left: -2px; top: 20%; bottom: 20%;
  width: 1px; background: var(--rule-2);
  transition: opacity var(--dur-hover) var(--ease);
}
.seg__btn.is-on::before,
.seg__btn.is-on + .seg__btn::before { opacity: 0; }
.seg__btn.is-on {
  background: var(--paper-3); color: var(--ink);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .10), 0 0 0 .5px rgba(0, 0, 0, .04);
}

.btnrow { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.tbtn {
  appearance: none; cursor: pointer;
  border: 1px solid var(--rule-2); border-radius: var(--r-pill); background: transparent;
  padding: 10px 6px;
  font: 500 12px/1 var(--sans); letter-spacing: -.005em; color: var(--ink);
  transition: border-color .16s var(--ease), background .16s var(--ease);
}

.run {
  width: 100%; appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 15px 18px; border: 1px solid var(--ink); border-radius: var(--r-pill);
  background: var(--ink); color: var(--paper);
  font: 500 14px/1 var(--sans); letter-spacing: -.01em;
  transition: background .18s var(--ease), border-color .18s var(--ease);
}
.run:disabled { opacity: .45; cursor: progress; }

.chk {
  display: flex; align-items: center; gap: 9px; margin-top: 14px;
  font: 400 14px/1 var(--sans); letter-spacing: -.016em; color: var(--ink-2); cursor: pointer;
}
.chk input { accent-color: var(--signal); width: 14px; height: 14px; }

/* readout */
.stats { margin: 0; }
.stats > div {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 9px 0; border-top: 1px solid var(--rule);
}
.stats > div:last-child { border-bottom: 1px solid var(--rule); }
.stats dt {
  font: 400 12px/1.4 var(--sans); letter-spacing: -.005em; text-transform: none;
  color: var(--ink-3);
}
.stats dd {
  margin: 0; font-family: var(--serif); font-size: 1.25rem;
  font-variant-numeric: tabular-nums; line-height: 1;
}

/* ── the plate ──────────────────────────────────────────── */
/* The two sharp L-brackets that used to sit on opposite corners of the map are
   gone. They were viewfinder decoration -- they marked nothing and did nothing
   -- and a 90-degree bracket sitting on a rounded corner is just wrong. */
.mapwrap { position: relative; }

#map {
  display: block; width: 100%; height: auto;
  border: 0; border-radius: var(--r-lg);
  background: var(--paper-3);
  box-shadow: var(--lift-1);
  cursor: crosshair;
  touch-action: none;
}

.maplegend {
  display: flex; flex-wrap: wrap; gap: 18px; margin-top: 12px;
  font: 400 13px/1 var(--sans); letter-spacing: -.01em; color: var(--ink-3);
}
.maplegend span { display: inline-flex; align-items: center; gap: 7px; }
.maplegend .sw { width: 11px; height: 11px; border-radius: 3px; display: block; }
.sw--wall  { background: var(--ink); }
.sw--exp   { background: color-mix(in srgb, var(--accent) 34%, var(--paper)); }
.sw--path  { background: var(--signal); }
.sw--start { background: var(--accent); border-radius: 50% !important; }
.sw--goal  { background: var(--signal); border-radius: 50% !important; }

/* ── console ────────────────────────────────────────────── */
.console { margin-top: clamp(36px, 5vh, 60px); }
.log {
  margin: 0; padding: 18px 20px;
  background: var(--night); color: var(--night-ink);
  border-radius: var(--r-lg);
  font: 400 12.5px/1.7 var(--mono);
  max-height: 240px; overflow: auto;
  white-space: pre-wrap; word-break: break-word;
}
.log::selection { background: var(--signal); color: #fff; }
.console__note {
  margin-top: 14px; font-size: .8125rem; line-height: 1.65;
  color: var(--ink-3); max-width: 76ch;
}

/* ── responsive ─────────────────────────────────────────── */
@media (max-width: 1000px) {
  .runner__grid { grid-template-columns: 1fr; }
  .panel { position: static; }
  .panel__row:last-of-type { border-bottom: 1px solid var(--rule); }
}
@media (max-width: 620px) {
  .seg__btn { font-size: 10.5px; padding: 8px 2px; }
  .stats > div { padding: 7px 0; }
  .stats dd { font-size: 1.0625rem; }
}

/* ── cursor chase ───────────────────────────────────────── */
.chase { margin-top: clamp(44px, 6vh, 76px); }
.chase__head { max-width: 74ch; margin-bottom: 22px; }
.chase__title { font-size: clamp(1.8rem, 3.4vw, 2.6rem); line-height: 1.05; margin-top: 6px; }
.chase__lede { margin-top: 14px; font-size: 1rem; color: var(--ink-2); }
.chase__lede strong { color: var(--ink); font-weight: 500; }

.chase__key {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 20px; margin-top: 20px;
  font: 400 13px/1 var(--sans); letter-spacing: -.01em; color: var(--ink-3);
}
.chase__key span { display: inline-flex; align-items: center; gap: 8px; }
.kx { width: 16px; height: 2px; display: block; flex: none; }
.kx--plan { background: repeating-linear-gradient(90deg, var(--ink) 0 4px, transparent 4px 7px); opacity: .55; }
.kx--fan  { background: var(--accent); opacity: .45; height: 1px; }
.kx--best { background: var(--signal); height: 3px; border-radius: 2px; }
.kx--infl { background: color-mix(in srgb, var(--signal) 26%, var(--paper)); height: 10px; width: 10px; }
.kx--look { background: var(--accent); height: 8px; width: 8px; border-radius: 50%; }
.chase__pickl { color: var(--ink-3); margin-right: -8px; }
.tbtn--sm { padding: 6px 11px; font-size: 10.5px; margin-left: auto; }

.chase__lede em { font-style: italic; color: var(--ink); }

/* ── race: one toggle per controller ─────────────────────── */
/* The chips sit in the same key row as the swatches, so they opt out of the
   margin-left:auto that pushes a lone button to the end. */
.racer {
  margin-left: 0; display: inline-flex; align-items: center; gap: 8px;
  opacity: .45; text-transform: lowercase;
}
.racer .kx { height: 9px; width: 9px; border-radius: 1px; }
.racer.is-on { opacity: 1; border-color: var(--ink-3); }
#race-run { margin-left: auto; }
/* Every stage canvas is 1204 CSS pixels wide by its width attribute, which is
   wider than a phone. Scaling to the column keeps the backing store at full
   resolution and costs nothing: every pointer handler already converts through
   getBoundingClientRect().width, so a scaled canvas still maps clicks correctly. */
#chase, #race-canvas, #arm {
  display: block; width: 100%; height: auto;
  border: 0; border-radius: var(--r-lg);
  background: var(--paper-3);
  box-shadow: var(--lift-1);
}
/* The chase tracks the pointer 1:1, which on touch means the browser must not
   also read the drag as a scroll. This replaces a preventDefault inside a
   touchmove handler -- same behaviour, declared where it belongs, and it is
   what lets the pointermove path be the only one. */
#chase { touch-action: none; }
.chase__lede--note { font-size: .94rem; color: var(--ink-3); }


/* ── motion polish ────────────────────────────────────────────────────
   Same rules as style.css. The controls on this page are pressed often --
   tens of times in a session -- so their motion is near-imperceptible by
   design: 120ms, transform only, no travel.
   ------------------------------------------------------------------- */
.tbtn, .seg__btn, .run {
  transition: transform var(--dur-press) var(--ease),
              background var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease),
              color var(--dur-hover) var(--ease),
              opacity var(--dur-hover) var(--ease);
}
.tbtn:active, .seg__btn:active { transform: scale(0.97); }
.run:active:not(:disabled) { transform: scale(0.99); }

@media (hover: hover) and (pointer: fine) {
  .tbtn:hover { border-color: var(--ink); background: var(--paper-3); }
  .seg__btn:hover:not(.is-on) { background: var(--paper-3); color: var(--ink); }
  .er-src:hover { opacity: .85; }
  .er-src.is-on:hover { opacity: 1; }
  .racer:hover { opacity: .85; }
  .racer.is-on:hover { opacity: 1; }
  .run:hover:not(:disabled) { background: var(--signal); border-color: var(--signal); color: #fff; }
}
/* The canvases fade in when their runtime is ready rather than popping
   from an empty plate. Opacity only -- a moving canvas is a repaint. */
#map, #chase, #arm, #race-canvas {
  opacity: 0;
  transition: opacity 320ms var(--ease);
}
/* Never leave a plate blank because a script did not run. */
.no-js #map, .no-js #chase, .no-js #arm, .no-js #race-canvas { opacity: 1; }
#map.is-live, #chase.is-live, #arm.is-live, #race-canvas.is-live { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .tbtn:active, .seg__btn:active, .run:active:not(:disabled) { transform: none; }
  #map, #chase, #arm, #race-canvas { transition: opacity 150ms var(--ease); }
}


/* ── the three vendored demos ─────────────────────────────────────────
   Same vocabulary as the four above -- .chase sections, .seg controls,
   .chase__read for the live line. What is new here is a table, a strip of
   32 dimensions, and a list of failure modes, none of which the first four
   needed.
   ------------------------------------------------------------------- */

/* The header's contents list is one grid per family, because the rule that
   keeps it from growing a stray rule through the block is "as many columns as
   entries". Four fetched, three copied; seven in one grid divides by nothing. */
.runner__tocl {
  margin: 30px 0 -22px;
  font: 500 11px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3);
}
.runner__toc--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Five course names or seven embodiment names do not fit one track each on a
   narrow column, so this variant wraps. The dividers are dropped with it: a
   vertical hairline between segments that have wrapped onto different rows
   points at nothing. */
.seg--wrap { display: inline-flex; flex-wrap: wrap; gap: 3px; vertical-align: middle; }
.seg--wrap .seg__btn { flex: 0 0 auto; padding: 8px 11px; }
.seg--wrap .seg__btn + .seg__btn::before { display: none; }

/* The first four take the hues the quadruped repository's own figures give
   them, so the plate and its README read as the same picture. The oracle is a
   dashed rule rather than a fifth hue: it is a privileged ceiling, not a
   candidate, and the table under the plate names every row for anyone four
   hues do not separate. */
.kx--nav2  { background: var(--signal); height: 3px; border-radius: 2px; }
.kx--react { background: var(--sun); height: 3px; border-radius: 2px; }
.kx--learn { background: #2a6ebb; height: 3px; border-radius: 2px; }
.kx--irl   { background: var(--accent); height: 3px; border-radius: 2px; }
.kx--orc   { background: repeating-linear-gradient(90deg, var(--ink-2) 0 5px, transparent 5px 8px); height: 3px; }
.kx--body  { background: var(--ink); height: 3px; border-radius: 2px; }

#qlc-canvas, #oba-canvas {
  display: block; width: 100%; height: auto;
  border: 0; border-radius: var(--r-lg);
  background: var(--paper-3);
  box-shadow: var(--lift-1);
}
#qlc-canvas, #oba-canvas { opacity: 0; transition: opacity 320ms var(--ease); }
.no-js #qlc-canvas, .no-js #oba-canvas { opacity: 1; }
#qlc-canvas.is-live, #oba-canvas.is-live { opacity: 1; }

/* The outcome table. Tabular numerals and a hairline per row, matching the
   benchmark figures on the index rather than inventing a second table style. */
/* Seven columns of nowrap numbers do not fit a 390 px phone, and the honest
   fix for a table is a scroller rather than a smaller font or fewer columns.
   The wrapper is what scrolls, so the page itself never gains a sideways
   scrollbar. */
.lab-tablewrap { overflow-x: auto; margin-top: 14px; }
.lab-table {
  width: 100%; border-collapse: collapse;
  font: 400 13px/1.4 var(--sans); font-variant-numeric: tabular-nums;
}
.lab-table:empty { display: none; }
.lab-table th, .lab-table td {
  text-align: right; padding: 9px 8px; border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}
.lab-table thead th {
  font: 500 11px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-3); border-bottom-color: var(--rule-2);
}
.lab-table thead th:first-child { text-align: left; }
.lab-table tbody th {
  text-align: left; font-weight: 500; color: var(--ink);
  display: flex; align-items: center; gap: 8px;
}
.lab-table tbody th .kx { flex: none; width: 14px; }
.lab-table td { color: var(--ink-2); }
.lab-table .is-bad { color: var(--signal); font-weight: 500; }

/* The 32-D action space. One block per slice, one cell per dimension: a
   dimension that is masked out of the loss is drawn as an empty well rather
   than hidden, because "this exists and your robot does not use it" is the
   thing the layout is for. */
.lab-strip {
  display: flex; flex-wrap: wrap; gap: 10px 18px; margin-top: 18px;
  padding: 18px; border-radius: var(--r-lg);
  background: var(--paper-2);
}
.lab-strip:empty { display: none; }
.lab-slice { min-width: 0; }
.lab-slice.is-off { opacity: .42; }
.lab-slice__n {
  margin: 0 0 6px; font: 500 11px/1.2 var(--mono); color: var(--ink);
}
.lab-slice__n span { display: block; margin-top: 3px; font-weight: 400; color: var(--ink-3); }
.lab-slice__d { display: flex; gap: 3px; }
.lab-dim {
  width: 11px; height: 20px; border-radius: 3px; display: block;
  background: transparent; box-shadow: inset 0 0 0 1px var(--rule-2);
  transition: background var(--dur-hover) var(--ease);
}
.lab-dim.is-live { background: var(--ink); box-shadow: none; }

.lab-h3 {
  margin: 40px 0 0; font-size: 1.25rem; letter-spacing: -.02em;
}
.chase__read--flush { margin-top: 12px; }

/* Sliders. One accent, and the thumb is the only thing in the block that is
   the accent colour, because it is the only thing you can drag. */
.lab-dials {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 16px 26px; margin-top: 20px;
}
.lab-dial { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; }
.lab-dial > span {
  grid-column: 1 / -1;
  font: 500 12px/1.35 var(--sans); color: var(--ink-2);
}
.lab-dial > span em { font-style: normal; color: var(--ink-3); }
.lab-dial input[type="range"] { width: 100%; accent-color: var(--signal); }
.lab-dial output {
  font: 500 12px/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--ink);
  min-width: 4ch; text-align: right;
}

/* The watchlist. A tripped mode gets the accent and its consequence text; an
   untripped one keeps the text, because the consequence is the reason the
   threshold is where it is and hiding it would leave a row of bare numbers. */
.lab-modes { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 1px; }
.lab-fm {
  padding: 12px 14px; border-radius: var(--r-sm);
  background: var(--paper-2);
  border-left: 3px solid transparent;
}
.lab-fm.is-trace { opacity: .5; }
.lab-fm.is-tripped { background: var(--signal-w); border-left-color: var(--signal); }
.lab-fm__h {
  margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  font: 400 13.5px/1.3 var(--sans); color: var(--ink);
}
.lab-fm__h b { font: 500 11px/1 var(--mono); color: var(--ink-3); }
.lab-fm.is-tripped .lab-fm__h b { color: var(--signal); }
.lab-fm__t {
  margin-left: auto; font: 400 11px/1 var(--mono); color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.lab-fm__v {
  font: 500 11px/1 var(--mono); color: var(--signal); font-variant-numeric: tabular-nums;
}
.lab-fm__c { margin: 6px 0 0; font-size: .8125rem; line-height: 1.5; color: var(--ink-3); }

.lab-pre {
  margin-top: 16px; padding: 16px 18px; border-radius: var(--r-lg);
  background: var(--night); color: var(--night-ink);
  font: 400 11.5px/1.6 var(--mono); overflow-x: auto;
}

@media (max-width: 640px) {
  .runner__toc--3 { grid-template-columns: 1fr; }
  .lab-table { font-size: 12px; }
  .lab-table th, .lab-table td { padding: 7px 5px; }
  .lab-strip { padding: 14px; gap: 10px 14px; }
  .lab-dim { width: 9px; }
}

@media (prefers-reduced-motion: reduce) {
  #qlc-canvas, #oba-canvas { transition: opacity 150ms var(--ease); }
}

/* A second prose block inside a section, after a full-width widget. The
   section's first .chase__head sets the reading measure; the later ones keep
   it without re-opening the gap the first one leaves under the title. */
.chase__head--again { margin-top: 34px; }

/* Two entries in the copied family now, so the contents grid divides again. */
.runner__toc--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* The course takes a click, and the crosshair says so before the caption does.

   `pan-y`, not `none`. The plate is full-width and about 440 px tall on a
   phone, so `none` made it a region you could not scroll past -- a drag that
   started on it was swallowed and the page stayed put. `pan-y` gives vertical
   drags back to the scroller and keeps taps, which is all this needs: the goal
   is set by a tap, never by a drag. */
#qlc-canvas { cursor: crosshair; touch-action: pan-y; }

/* Detail that would otherwise sit between the reader and the plate. Closed by
   default: the demo is the argument, and the paragraphs are for whoever wants
   to check it. */
.lab-more { margin-top: 18px; }
.lab-more > summary {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px; border: 1px solid var(--rule-2); border-radius: var(--r-pill);
  font: 500 12px/1 var(--sans); color: var(--ink-2);
  transition: border-color .16s var(--ease), color .16s var(--ease);
}
.lab-more > summary::-webkit-details-marker { display: none; }
.lab-more > summary::before {
  content: "+"; font: 500 13px/1 var(--mono); color: var(--signal);
}
.lab-more[open] > summary::before { content: "\2212"; }
.lab-more > summary:hover { border-color: var(--ink); color: var(--ink); }
.lab-more > *:not(summary) { margin-top: 14px; }

/* The action space is a reference block, not a demo, and is set back to say so. */
.ref { margin-top: clamp(48px, 7vh, 88px); padding-top: clamp(28px, 4vh, 44px); position: relative; }
.ref::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0;
  height: 1px; background: var(--rule);
}
.ref .chase__title { font-size: clamp(1.4rem, 2.4vw, 1.9rem); }

/* A segment with nothing behind it yet. It has to look unavailable, or it is
   the same lie as a button that silently declines. */
.seg__btn:disabled { opacity: .38; cursor: not-allowed; }
.seg__btn:disabled.is-on { opacity: .6; }
