/* ==========================================================================
   Feasible Region  .  site.css  .  the single stylesheet
   Distill-grade explainer, space-opera frame. Buildless, no CDN, no framework.
   Sections in this file:
     1  Design tokens (light)         6  Vignette, signal, off-ramp, notes
     2  Dark theme override           7  Figures, figure-box, legend, badge
     3  Reset and base                8  Controls (scrubber, buttons)
     4  Typography and rhythm         9  Cite tooltips, TOC
     5  Layout: body / page / screen 10  Focus, reduced motion, print
   The persistent color language (constraint 1..5, objective, trail) is defined
   ONCE here (tokens) and referenced by every figure.
   ========================================================================== */

/* 1. DESIGN TOKENS  (light theme is the default :root)  ==================== */
:root {
  color-scheme: light;

  /* Type stacks. System faces only (no webfont fetch, no CDN). */
  --font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
                Georgia, Cambria, "Times New Roman", serif;
  --font-sans: "Avenir Next", Avenir, "Segoe UI", "Helvetica Neue", Helvetica,
               system-ui, Roboto, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
               "Liberation Mono", "Roboto Mono", monospace;

  /* Fluid type scale */
  --fs-body: clamp(1.05rem, 1rem + 0.35vw, 1.2rem);
  --fs-small: 0.86rem;
  --fs-h1: clamp(2.1rem, 1.5rem + 2.9vw, 3.4rem);
  --fs-h2: clamp(1.55rem, 1.25rem + 1.5vw, 2.25rem);
  --fs-h3: clamp(1.2rem, 1.05rem + 0.7vw, 1.5rem);
  --lh-body: 1.66;
  --lh-tight: 1.16;

  /* Spacing scale */
  --sp-1: 0.25rem; --sp-2: 0.5rem;  --sp-3: 0.75rem; --sp-4: 1rem;
  --sp-5: 1.5rem;  --sp-6: 2rem;    --sp-7: 3rem;    --sp-8: 4.5rem;

  /* Layout widths */
  --measure: 42rem;      /* the reading column, roughly 700px  */
  --page-w: 62rem;       /* wider figures                      */
  --gutter: clamp(1.1rem, 4vw, 2rem);
  --note-w: 13.5rem;     /* margin-note column in the gutter    */
  --note-gap: 2rem;
  --bp-wide: 1000px;     /* margin notes float above this width */
  --radius: 0.5rem;

  /* Surfaces and ink (distill classic: pure white page, near-black ink) */
  --bg: #ffffff;
  --bg-glow: none;
  --surface: #ffffff;
  --surface-2: #f5f5f5;
  --ink: #1a1a1a;
  --ink-soft: #3d3d3d;
  --faint: #6e6e6e;
  --fainter: #a3a3a3;
  --hairline: #e8e8e8;
  --hairline-strong: #d4d4d4;

  /* Wireframe depth pair for the three 3D stages (S3, S5, S6): front edges
     lift to at least 3:1 on the figure surface, back edges recede lighter and
     dashed. Both stay subordinate to the heavier, darker walk trail. */
  --edge-front: #7d7d7d;
  --edge-back: #bdbdbd;

  /* Chrome accent: restrained distill link blue. Not a data hue. */
  --accent: #2660a4;
  --accent-soft: #4a7fc0;
  --accent-contrast: #ffffff;
  --focus-ring: rgba(38, 96, 164, 0.42);
  /* First-use gloss underline. Dotted term affordance; ratio 6.4:1 on the
     light surface, well past the 3:1 non-text floor of WCAG 1.4.13. */
  --gloss-underline: #2660a4;
  --shadow: 0 6px 20px -8px rgba(24, 26, 34, 0.22);

  /* ======================================================================
     THE COLOR LANGUAGE
     Five constraint hues drawn from the Okabe and Ito colorblind-safe set,
     tuned here for crisp lines on paper. Each constraint KEEPS its hue from
     S1 to S6. Objective is the one arrow; trail is the visited-vertex path.
     Hue identity is held across themes; only lightness shifts in dark mode.
       1 antimatter budget    vermillion
       2 chargrid hours       ocean blue
       3 hull-frame stock     teal green
       4 stabilizer mass      orchid
       5 crystal chamber cap  goldenrod
     ====================================================================== */
  --constraint-1: #c34818;
  --constraint-2: #0a6fb0;
  --constraint-3: #0a7f65;
  --constraint-4: #a83f80;
  --constraint-5: #9d6400;
  --objective: #7a2fb2;   /* the optimize-toward arrow, reserved violet slot */
  --trail: #6f6660;       /* accumulating visited-vertex trail (warm graphite) */

  /* Soft fills of each hue for shaded polygons / active washes.
     Same hue, low alpha, so a face and its line always read as one thing. */
  --constraint-1-fill: rgba(195, 72, 24, 0.16);
  --constraint-2-fill: rgba(10, 111, 176, 0.15);
  --constraint-3-fill: rgba(10, 127, 101, 0.15);
  --constraint-4-fill: rgba(168, 63, 128, 0.15);
  --constraint-5-fill: rgba(157, 100, 0, 0.16);
  --objective-fill: rgba(122, 47, 178, 0.14);
  --wash: rgba(181, 116, 0, 0.16);   /* neutral cross-highlight wash */
  /* The shaded feasible region in the hero and formulation figures. One token,
     four render paths (two JS figures, two no-JS stills), so hydrated and
     fallback paint stay in lockstep. Matches the light wash exactly. */
  --region-fill: rgba(181, 116, 0, 0.16);
}

/* 2. DARK THEME (explicit opt-in only; the canonical distill look is light) == */
html[data-theme="dark"] {
    color-scheme: dark;
    --bg: #0e1015;
    --bg-glow: radial-gradient(125% 62% at 50% -12%, #171b26 0%, transparent 58%);
    --surface: #171a21;
    --surface-2: #1e222c;
    --ink: #ecebe4;
    --ink-soft: #c6c7cf;
    --faint: #9aa0aa;
    --fainter: #6f727c;
    --hairline: #2a2f3a;
    --hairline-strong: #3b4250;
    --edge-front: #7d8492;
    --edge-back: #434a58;

    --accent: #9fbaf0;
    --accent-soft: #c4d5f5;
    --accent-contrast: #0e1015;
    --focus-ring: rgba(159, 186, 240, 0.5);
    /* Lifted for deep charcoal; ratio near 9.8:1 on the dark surface. */
    --gloss-underline: #9fbaf0;
    --shadow: 0 8px 26px -10px rgba(0, 0, 0, 0.7);

    /* Same hues, lifted for contrast on deep charcoal. */
    --constraint-1: #ff8557;
    --constraint-2: #59b0f2;
    --constraint-3: #34cfa6;
    --constraint-4: #e78fc6;
    --constraint-5: #f0bd3e;
    --objective: #c08cf5;
    --trail: #b3a89c;

    --constraint-1-fill: rgba(255, 133, 87, 0.2);
    --constraint-2-fill: rgba(89, 176, 242, 0.2);
    --constraint-3-fill: rgba(52, 207, 166, 0.2);
    --constraint-4-fill: rgba(231, 143, 198, 0.2);
    --constraint-5-fill: rgba(240, 189, 62, 0.2);
    --objective-fill: rgba(192, 140, 245, 0.2);
    --wash: rgba(240, 189, 62, 0.18);
    /* Region fill runs darker than the wash here: a page-sized field at the
       full wash strength glows too loud on deep charcoal. Same gold, lower
       alpha, so the hue identity holds and the region recedes to a tint. */
    --region-fill: rgba(240, 189, 62, 0.12);
}

/* 3. RESET AND BASE  ====================================================== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5rem;   /* clear the sticky TOC on anchor jumps */
}

body {
  font-family: var(--font-serif);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
  background-color: var(--bg);
  background-image: var(--bg-glow);
  background-repeat: no-repeat;
  overflow-x: clip;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  font-kerning: normal;
  font-variant-numeric: oldstyle-nums proportional-nums;
}

img, svg, canvas, video { max-width: 100%; }
svg, canvas { display: block; }

a {
  color: var(--accent);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.14em;
  text-decoration-color: var(--hairline-strong);
  transition: text-decoration-color 0.15s ease, color 0.15s ease;
}
a:hover { text-decoration-color: var(--accent); }

strong, b { font-weight: 650; }
::selection { background: var(--wash); }

/* 4. TYPOGRAPHY AND RHYTHM  =============================================== */
h1, h2, h3, h4 {
  font-family: var(--font-serif);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); letter-spacing: -0.022em; }
h2 { font-size: var(--fs-h2); margin-top: var(--sp-2); }
/* An h3 binds to the text it introduces: more air above than below. The
   universal reset zeroes heading margins, so the rhythm is restored here. */
h3 { font-size: var(--fs-h3); margin-top: var(--sp-5); margin-bottom: var(--sp-2); }

p, ul, ol, blockquote { font-size: 1em; }
p + p { margin-top: var(--sp-4); }
/* overflow-wrap lets a long unbreakable token (a mono path or a code fragment)
   break rather than push the line past a narrow viewport (no 380px scroll). */
p, li, figcaption { overflow-wrap: break-word; }
p { text-wrap: pretty; hanging-punctuation: first; }

ul, ol { padding-left: 1.4em; }
li + li { margin-top: var(--sp-2); }

/* Inline code fragment (a path, an API call). Breakable so it never overflows. */
.code-inline {
  font-family: var(--font-mono);
  font-size: 0.9em;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Eyebrow: small-caps section label ("SECTION 03  .  THE SIGNATURE") */
.eyebrow {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.17em;
  color: var(--faint);
  display: block;
  margin-bottom: var(--sp-2);
}

/* Section heads sit under an eyebrow; give sections air and a scroll anchor */
section { margin-top: var(--sp-8); scroll-margin-top: 5rem; }
section > h2 { margin-bottom: var(--sp-4); }

/* Cold open: a non-fiction hook, set apart with a drop cap */
.cold-open { margin-top: var(--sp-6); }
/* The paragraphs sit inside the .l-body wrapper, so the child combinator must
   step through it or the drop cap never renders. */
.cold-open .l-body > p:first-of-type::first-letter {
  float: left;
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: 3.5em;
  line-height: 0.76;
  padding: 0.04em 0.12em 0 0;
  color: var(--accent);
}

/* 5. LAYOUT: BODY / PAGE / SCREEN  ======================================== */
/* Every content block declares its width. Three tiers, all centred. */
.l-body, .l-page {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.l-body { max-width: calc(var(--measure) + var(--gutter) * 2); }
.l-page { max-width: calc(var(--page-w) + var(--gutter) * 2); }
.l-screen { width: 100%; margin-inline: 0; }

/* 6. VIGNETTE / SIGNAL / OFF-RAMP / MARGIN NOTES  ========================= */

/* Vignette: the 2 to 4 sentence italic opener for each S-section.
   Italic serif, subtly inset behind a hairline. A reader who skips every
   one of these loses zero technical content. */
.vignette {
  font-style: italic;
  font-size: 1.04em;
  line-height: 1.55;
  color: var(--ink-soft);
  border-left: 2px solid var(--hairline-strong);
  padding: 0.1em 0 0.1em 1.15em;
  margin: var(--sp-4) 0 var(--sp-5);
  max-width: 34rem;
}
.vignette p + p { margin-top: var(--sp-3); }

/* Signal traffic: ship-to-ship exchanges, styled as tight-beam transcripts.
   One exchange per section, except the closing section, which carries two: the
   race exchange and the docking sign-off. */
.signal {
  font-family: var(--font-mono);
  font-size: 0.82em;
  line-height: 1.55;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
  background: var(--surface-2);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 0.85em 1em;
  margin: var(--sp-5) 0;
  max-width: 38rem;
}

.signal-head {
  display: block;
  padding-left: 1.65em;
  font-family: var(--font-mono);
  font-size: 0.88em;
  font-weight: 600;
  line-height: 1.55;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.signal-mark {
  display: inline-block;
  width: 1.1em;
  height: 1.1em;
  margin-left: -1.65em;
  margin-right: 0.55em;
  vertical-align: -0.16em;
  fill: var(--trail); /* var() cannot resolve in an SVG presentation attribute */
}

.signal-body {
  display: block;
  max-width: 34rem;
  margin-top: 0.45em;
  margin-left: 1.65em;
  line-height: 1.6;
  letter-spacing: 0;
  color: var(--ink-soft);
}

.signal-msg + .signal-msg {
  margin-top: 0.85em;
  padding-top: 0.85em;
  border-top: 1px solid var(--hairline-strong);
}

/* Off-ramp: the "if you stop here, you can already read a tableau" exit,
   after S3 and S4. A clean, framed hand-off. */
.offramp {
  font-family: var(--font-sans);
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-5);
  margin: var(--sp-6) 0;
  box-shadow: var(--shadow);
}
.offramp::before {
  content: "IF YOU STOP HERE";
  display: block;
  font-size: 0.7rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  color: var(--accent);
  margin-bottom: var(--sp-2);
}

/* Margin notes: first-person builder asides. Float into the right gutter on
   wide viewports; collapse inline (or into a details) on narrow. */
.margin-note {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--faint);
}
/* Narrow default: an inset aside, or a collapsible details */
aside.margin-note {
  border-left: 2px solid var(--hairline);
  padding-left: 0.9em;
  margin: var(--sp-4) 0;
}
details.margin-note {
  border-left: 2px solid var(--hairline);
  padding-left: 0.9em;
  margin: var(--sp-4) 0;
}
details.margin-note > summary {
  cursor: pointer;
  font-weight: 650;
  color: var(--accent);
  list-style: none;
}
details.margin-note > summary::-webkit-details-marker { display: none; }
details.margin-note > summary::before { content: "\271A  "; color: var(--accent); }
details.margin-note[open] > summary { margin-bottom: var(--sp-2); }

@media (min-width: 1000px) {
  .margin-note {
    float: right;
    clear: right;
    width: var(--note-w);
    margin-right: calc(-1 * (var(--note-w) + var(--note-gap)));
    margin-top: 0.2em;
    margin-bottom: var(--sp-3);
    border-left: 0;
    padding-left: 0;
    position: relative;
  }
  .margin-note::before {
    content: "";
    position: absolute;
    left: calc(-1 * var(--note-gap) + 0.6rem);
    top: 0.15em;
    bottom: 0.15em;
    width: 1px;
    background: var(--hairline);
  }
  details.margin-note { padding-left: 0; }
}

/* frame-note (Culture aside): a one-time, third-person note explaining the
   narrative frame. Kept visually distinct from .margin-note, which is reserved
   for first-person builder asides. Budget-exempt stage-setting:
   a small labelled card that floats into the gutter on wide viewports. */
.frame-note {
  font-family: var(--font-sans);
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--ink-soft);
}
aside.frame-note {
  border-left: 2px solid var(--hairline-strong);
  background: var(--surface-2);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 0.7em 1em;
  margin: var(--sp-4) 0;
}
aside.frame-note::before {
  content: "THE FRAME";
  display: block;
  font-size: 0.66rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  color: var(--accent);
  margin-bottom: var(--sp-2);
}
@media (min-width: 1000px) {
  aside.frame-note {
    float: right;
    clear: right;
    width: var(--note-w);
    margin-right: calc(-1 * (var(--note-w) + var(--note-gap)));
    margin-top: 0.2em;
    margin-bottom: var(--sp-3);
  }
}

/* 7. FIGURES / FIGURE-BOX / LEGEND / ENGINE BADGE  ======================== */
figure {
  margin: var(--sp-6) 0;
}
figure > svg, figure > canvas { width: 100%; height: auto; }

figcaption {
  font-family: var(--font-sans);
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--faint);
  margin-top: var(--sp-3);
  /* Measure cap: figures ride the wide 62rem column, but a caption line at
     that width runs past 150 characters. At this font size, 34rem holds a
     line near 88 characters, inside the roughly-90 reading bound. */
  max-width: 34rem;
}
figcaption strong, figcaption b { color: var(--ink-soft); font-weight: 650; }

/* A guiding "what to notice" prompt sitting above a figure */
.notice {
  font-family: var(--font-sans);
  font-size: 0.84rem;
  font-style: italic;
  color: var(--faint);
  margin-bottom: var(--sp-3);
}

/* figure-box: reserves its space BEFORE hydration so CLS stays near zero
   Set --fig-ar inline to change the reserved shape. Reserved
   shells show a faint plotting grid and a "coming in milestone B" note. */
.figure-box {
  position: relative;
  width: 100%;
  aspect-ratio: var(--fig-ar, 16 / 10);
  min-height: 12rem;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  contain: layout paint;
}
.figure-box > svg, .figure-box > canvas { width: 100%; height: 100%; }

/* Reserved-figure treatment: a dot grid hints "a chart lives here" */
.figure-box.is-reserved {
  background-color: var(--surface);
  background-image:
    radial-gradient(var(--hairline) 1px, transparent 1.2px);
  background-size: 22px 22px;
  background-position: center;
}
.coming-soon {
  font-family: var(--font-sans);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fainter);
  text-align: center;
  padding: var(--sp-4);
  background: var(--bg);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius);
}

/* Color-language legend. Every figure inherits this one legend.
   Swatch colors are read straight from the tokens above, never redefined. */
.legend {
  list-style: none;
  padding: 0;
  margin: var(--sp-3) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  font-family: var(--font-sans);
  font-size: 0.8rem;
  color: var(--ink-soft);
  max-width: var(--measure); /* legends hold the body measure, not the figure column */
}
.legend li { margin: 0; display: inline-flex; align-items: center; gap: 0.45em; }
.swatch {
  width: 0.95em;
  height: 0.95em;
  border-radius: 3px;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.swatch[data-c="1"] { background: var(--constraint-1); }
.swatch[data-c="2"] { background: var(--constraint-2); }
.swatch[data-c="3"] { background: var(--constraint-3); }
.swatch[data-c="4"] { background: var(--constraint-4); }
.swatch[data-c="5"] { background: var(--constraint-5); }
.swatch[data-c="obj"] { background: var(--objective); border-radius: 50%; }
.swatch[data-c="trail"] { background: var(--trail); border-radius: 50%; }

/* Legend glyphs: constraints read as lines and the objective as an arrow,
   matching the on-chart color language; corners stay the round .swatch dot. */
.legend-glyph { width: 1.55em; height: 0.72em; flex: none; overflow: visible; }
.legend-glyph line { stroke-width: 2.2; stroke-linecap: round; }
.legend-glyph[data-c="1"] line { stroke: var(--constraint-1); }
.legend-glyph[data-c="2"] line { stroke: var(--constraint-2); }
.legend-glyph[data-c="obj"] line { stroke: var(--objective); }
.legend-glyph[data-c="obj"] polygon { fill: var(--objective); }

/* One pill primitive. The status and chrome pills collapse onto this
   single boxed chrome: the engine badge, the oracle badge, and the hood chip
   all carry .pill and add only their own type and color. The data-bearing
   constraint-hued tags (fm-slack, mf-cut, sp-route) are a deliberately distinct
   second family and keep their own hue rules. */
.pill {
  display: inline-flex;
  align-items: center;
  line-height: 1;
  white-space: nowrap;
  padding: 0.32em 0.65em;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: var(--surface-2);
}

/* Engine badge: tiny monospace pill telling the truth about the live engine
   Three states: live (WASM), trace (replay), geometric (from the
   trace geometry). A status dot inherits the state color. Chrome from .pill. */
.engine-badge {
  gap: 0.45em;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}
.engine-badge::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.engine-badge[data-engine="live"] { color: var(--constraint-3); }
.engine-badge[data-engine="trace"] { color: var(--constraint-5); }
.engine-badge[data-engine="geometric"] { color: var(--objective); }

/* Oracle-verified trust badge for the references section. Chrome from .pill;
   this rule sets only its own type, color, and slightly wider padding. */
.oracle-badge {
  gap: 0.5em;
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.4em 0.8em;
  color: var(--constraint-3);
  background: var(--surface);
  text-decoration: none;
}
.oracle-badge::before { content: "\2713"; font-weight: 700; }

/* 8. CONTROLS: SCRUBBER + BUTTONS  ======================================== */
/* One iteration scrubber per figure, a native range so keyboard just works
   Arrows step, Home/End jump, focus is visible. */
.scrubber-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
}

input[type="range"].scrubber {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 12rem;
  height: 1.5rem;
  background: transparent;
  cursor: pointer;
}
input[type="range"].scrubber::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--hairline-strong);
}
input[type="range"].scrubber::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--hairline-strong);
}
input[type="range"].scrubber::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  margin-top: -6px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
input[type="range"].scrubber::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg);
}
input[type="range"].scrubber:focus-visible { outline: none; }
input[type="range"].scrubber:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--focus-ring);
}
input[type="range"].scrubber:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--focus-ring);
}

.btn {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  padding: 0.4em 0.85em;
  cursor: pointer;
  line-height: 1.2;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:active { background: var(--surface-2); }
.btn[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* A live state read-out (basis, vertex, objective) for aria-live regions */
.readout {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--ink-soft);
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 0.5em 0.75em;
  margin-top: var(--sp-3);
}
.readout b { color: var(--ink); }

/* 8b. S3 SYNCHRONIZED DUAL VIEW (polytope + tableau)  ==== fig:dualview ==== */
/* Owner: S3 implementer. This is the existing fig:dualview block. Add only a
   per-shape .is-lit refinement here if the shared brush needs tuning. The base
   .is-lit brush itself is defined once in the fig:sync block near the end of
   this file, so do not redefine it here. */
/* Two views, one scrubber. The polytope and the tableau share the persistent
   color language (slack s-k carries constraint-k's hue) and a stepped highlight
   so the lit vertex, the entering column, and the leaving row read as one act. */
.dualview-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: start;
}
@media (min-width: 820px) {
  .dualview-grid { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); }
}

/* Reserve the controls row so filling it with the scrubber shifts nothing. */
.dv-controls { min-height: 2.6rem; }
.dv-step-tag {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--faint);
  white-space: nowrap;
}
.dv-step-btn { flex: none; }
.dv-play { flex: none; min-width: 4.2em; }

/* What-if row: a second native range perturbs the binding constraint's rhs and
   the figure re-solves live. It reuses .scrubber, so only the caption, the value
   tag, and the disabled cue are new. Colour only via tokens, so both themes hold.
   The host is empty (collapsed) without JS and disabled without WebAssembly. */
.whatif-row { margin-top: var(--sp-2); }
.whatif-row:empty { display: none; }
.whatif-cap {
  font-family: var(--font-sans);
  font-size: 0.8rem;
  color: var(--ink-soft);
  white-space: nowrap;
}
.whatif-tag { color: var(--ink); }
input[type="range"].whatif-range:disabled { cursor: not-allowed; opacity: 0.5; }
.whatif-row.is-disabled .whatif-cap { color: var(--faint); }

/* Polytope panel. Front edges take the lifted depth token; back edges recede
   via the shared fig:iso3d vocabulary declared once near fig:sync. */
.dv-edge { stroke: var(--edge-front); stroke-width: 1.4; fill: none; }
.dv-trail {
  stroke: var(--trail);
  stroke-width: 3;
  stroke-linecap: round;
  fill: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.dv-trail.is-on { opacity: 1; }
.dv-vertex { fill: var(--fainter); }
.dv-vertex.is-visited { fill: var(--trail); }
.dv-current { fill: var(--objective); stroke: var(--surface); stroke-width: 2; }
.dv-current-ring { fill: none; stroke: var(--objective); stroke-width: 2; opacity: 0.5; }
.dv-current-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 650;
  fill: var(--objective);
}
/* Per-shape brush refinement (contract section 5): the shared svg .is-lit sets a
   2.5 stroke, which would make the 3px trail thinner. Override so a lit trail and
   vertex read as a clear link when the tableau side is hovered or focused. Only
   stroke changes, so no layout moves and CLS stays ~ 0. */
.dv-trail.is-lit { stroke: var(--objective); stroke-width: 5; opacity: 1; }
.dv-current.is-lit { stroke: var(--objective); stroke-width: 3.5; }

/* Tableau panel */
.tableau-panel { min-width: 0; }
.tableau-scroll { overflow-x: auto; }
table.tableau {
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  line-height: 1.3;
  color: var(--ink-soft);
  width: 100%;
}
table.tableau th,
table.tableau td {
  padding: 0.28em 0.5em;
  text-align: right;
  white-space: nowrap;
  border-bottom: 1px solid var(--hairline);
}
table.tableau thead th {
  font-weight: 700;
  border-bottom: 1px solid var(--hairline-strong);
  color: var(--faint);
}
table.tableau .dv-basis-head,
table.tableau .dv-row-head {
  text-align: left;
  font-weight: 700;
}
table.tableau tbody .dv-row-head { color: var(--ink); }
table.tableau tr.dv-obj-row th,
table.tableau tr.dv-obj-row td {
  border-top: 1px solid var(--hairline-strong);
  border-bottom: 0;
  color: var(--faint);
}
/* Linked highlight: entering column in objective wash, leaving row in trail wash,
   the pivot cell where they cross reads strongest. Colour is never the only cue,
   the aria-live readout names the entering and leaving variables in words. */
table.tableau td.is-hover,
table.tableau th.is-hover { background: var(--surface-2); }
table.tableau th.is-entering,
table.tableau td.is-entering { background: var(--objective-fill); }
table.tableau tr.is-leaving > th,
table.tableau tr.is-leaving > td { background: var(--wash); }
table.tableau tr.is-leaving > td.is-entering {
  box-shadow: inset 0 0 0 2px var(--objective);
}

/* Manifest card: the five limits named, so s1..s5 are not anonymous
   a section before S4 names them. A compact wrapping list; each row shows the
   slack chip in its own hue (--h, set per row), the name, and the live
   inequality dualview.js rewrites from the model. Colour only via tokens. */
.dv-manifest {
  list-style: none;
  margin: 0 0 var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
.dv-manifest-row {
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  font-size: 0.78rem;
  white-space: nowrap;
}
.dv-manifest-slot {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--h);
}
.dv-manifest-name { font-family: var(--font-sans); color: var(--ink); }
.dv-manifest-ineq { font-family: var(--font-mono); color: var(--ink-soft); }

/* Right column of the dual view: tableau on top, objective staircase beneath it
   in the two-column dead space. */
.dualview-right {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

/* Objective staircase: a supplementary step read of the climb. It
   exists only where the two-column dead space does, so it is hidden below 820px,
   exactly the width where .dualview-grid drops to one column. The authored SVG is
   the no-JS state; dualview.js rebuilds it for live re-solves. */
.dv-staircase { display: none; }
@media (min-width: 820px) {
  .dv-staircase { display: block; }
}
.dv-stair-cap {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  color: var(--faint);
  margin: 0 0 var(--sp-1);
}
.dv-stair-svg { width: 100%; height: auto; display: block; overflow: visible; }
.dv-stair-axis { stroke: var(--hairline); stroke-width: 1; }
.dv-stair-tread { stroke: var(--trail); stroke-width: 2; stroke-linecap: round; fill: none; }
.dv-stair-riser {
  stroke: var(--trail);
  stroke-width: 1.5;
  stroke-dasharray: 2 2;
  opacity: 0.7;
  fill: none;
}
.dv-stair-dot { fill: var(--trail); }
.dv-stair-val {
  font-family: var(--font-mono);
  font-size: 9px;
  fill: var(--ink-soft);
}
.dv-stair-step.is-current .dv-stair-tread { stroke: var(--objective); stroke-width: 3; }
.dv-stair-step.is-current .dv-stair-dot {
  fill: var(--objective);
  stroke: var(--surface);
  stroke-width: 2;
}
.dv-stair-step.is-current .dv-stair-val { fill: var(--objective); font-weight: 700; }

/* Identity line: the z row read in exact fractions is S4's price list. */
.dv-identity { margin-top: var(--sp-3); }

/* fig:hood  (S3 open-the-hood pivot zoom, data-role="dualview-hood")  ------ */
/* Owner: hood agent. A collapsible worked-pivot panel under the dual view.
   Colour comes only from tokens, so both themes recolour for free. The panel bg
   is pinned to var(--surface) so the --faint ghosts clear AA, and the 3 by 3
   excerpt reuses the main tableau's cues exactly: entering column in
   --objective-fill, leaving row in --wash, the pivot cell ringed with an inset
   --objective. The host reserves 2.4rem so filling it on hydration shifts
   nothing. Only backgrounds and borders carry state, so brushing and
   frame steps never reflow. The shared .is-lit brush is defined once in fig:sync
   and is not redefined here. */
.hood-host { min-height: 2.4rem; margin-top: var(--sp-3); }

.hood { font-family: var(--font-sans); }
.hood-bar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.hood-toggle { flex: none; }
/* Hood chip: chrome from .pill; a quiet surface fill and a neutral border. */
.hood-chip {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--faint);
  padding: 0.2em 0.55em;
  background: var(--surface);
  border-color: var(--hairline-strong);
}

/* Visually hidden live region: announced, never seen, never shifts layout. */
.hood-status {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
}

.hood-panel {
  margin-top: var(--sp-3);
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
}
.hood-panel[hidden], .hood-frames[hidden], .hood-lead[hidden] { display: none; }

.hood-lead {
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin-bottom: var(--sp-3);
}
.hood-frame-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.hood-step { flex: none; }
.hood-frame-tag {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--faint);
  white-space: nowrap;
}

.hood-grid-scroll { overflow-x: auto; }
table.hood-grid {
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.25;
  color: var(--ink-soft);
}
table.hood-grid th,
table.hood-grid td {
  padding: 0.34em 0.6em;
  text-align: right;
  white-space: nowrap;
  border-bottom: 1px solid var(--hairline);
}
table.hood-grid thead th {
  font-weight: 700;
  color: var(--faint);
  border-bottom: 1px solid var(--hairline-strong);
}
table.hood-grid .hood-rowhead { text-align: left; font-weight: 700; color: var(--ink); }
table.hood-grid tr.hood-obj .hood-rowhead { color: var(--faint); }
table.hood-grid tr.hood-obj th,
table.hood-grid tr.hood-obj td { border-top: 1px solid var(--hairline-strong); }

/* Entering column, leaving row, and the ringed pivot cell: the same cues the
   main tableau uses, so the excerpt reads as the very same pivot. */
td.hood-col-piv { background: var(--objective-fill); }
tr.hood-row-piv > th,
tr.hood-row-piv > td { background: var(--wash); }
td.hood-pivot { box-shadow: inset 0 0 0 2px var(--objective); }

.hood-cell .hood-now { color: var(--ink); font-weight: 650; }
/* Ghosts: the prior value struck through and labelled "was", never a sub-AA
   opacity fade. Coloured in --ink-soft, not --faint, because ghost cells sit on
   the pivot row (--wash) and pivot column (--objective-fill) backgrounds, not on
   plain --surface; --ink-soft clears AA on all three in both themes (>= 7:1),
   while size + strikethrough + weight + the "was" tag keep it subordinate to the
   --ink "now" value. */
.hood-ghost { display: block; font-size: 0.72rem; color: var(--ink-soft); font-weight: 400; }
.hood-was { text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.62rem; }
.hood-ghost s { text-decoration: line-through; }

.hood-op { text-align: left; font-size: 0.72rem; color: var(--faint); }
.hood-op-head { border-bottom-color: transparent; }

.hood-note, .hood-signnote {
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin-top: var(--sp-3);
}
.hood-signnote { color: var(--faint); }
.hood-signnote:empty { display: none; }
.hood-cite {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--faint);
  margin-top: var(--sp-3);
}

/* 9. CITE TOOLTIPS + TOC  ================================================= */
/* In text citation: a bracketed [n] link to its reference entry that floats the
   reference short form on hover or keyboard focus. The [n] is the visible
   academic marker, baseline not superscript, a touch smaller and in the sans
   face so it reads as a citation and not as body text. The tooltip chrome below
   (.cite-tip) is unchanged; it stays pure enhancement, since the href still
   anchor jumps with scripts off. .cite had no uses before this, so moving its
   visible form from a superscript to a bracket breaks nothing. The [n] prints
   (only .cite-tip is dropped in print); a { color } already inks it on paper. */
.cite {
  position: relative;
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: 0.82em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}
.cite-tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 0.55em);
  transform: translateX(-50%);
  width: min(19rem, 72vw);
  background: var(--surface);
  color: var(--ink-soft);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  padding: 0.65em 0.8em;
  font: 400 0.82rem/1.5 var(--font-sans);
  text-align: left;
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.13s ease, visibility 0.13s ease;
  z-index: 30;
}
.cite-tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--surface);
}
.cite:hover .cite-tip,
.cite:focus .cite-tip,
.cite:focus-within .cite-tip { opacity: 1; visibility: visible; }

/* Sticky table of contents. Quiet by default, accent when current. */
.toc {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  line-height: 1.5;
}
.toc ol { list-style: none; padding: 0; margin: 0; }
.toc a {
  display: block;
  padding: 0.2em 0;
  color: var(--faint);
  text-decoration: none;
  border-left: 2px solid transparent;
  padding-left: 0.7em;
}
.toc a:hover { color: var(--ink); }
.toc a[aria-current="true"],
.toc a.is-active {
  color: var(--accent);
  border-left-color: var(--accent);
  font-weight: 650;
}
/* The TOC only leaves the reading flow once there is gutter room for a left
   rail (>= 1460px, where .toc-nav below repositions it into the margin). Below
   that it stays a quiet inline nav, so it never pins over the centred column.
   The page width did not clear a rail at 1400px, so the trigger moved out. */
@media (min-width: 1460px) {
  .toc.is-sticky { position: sticky; top: 1.5rem; }
}

/* TOC wayfinding tags: seven neutral small-caps tokens in the
   eyebrow idiom, no pill and no data hue. They sit in the DOM on every tagged
   row; the inline nav shows them on all rows, and the wide margin rail keeps
   them only on the active entry so the fixed rail stays quiet. */
.toc-tags {
  display: flex; flex-wrap: wrap; gap: 0.2em 0.55em;
  margin: 0.1em 0 0.35em 0.7em;
}
.toc-tag {
  font-family: var(--font-sans);
  font-size: 0.58rem; font-weight: 650;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--faint);
}
@media (min-width: 1460px) {
  .toc-nav.is-sticky .toc-tags { display: none; }
  .toc-nav.is-sticky li:has(a.is-active) .toc-tags,
  .toc-nav.is-sticky li:has(a[aria-current]) .toc-tags { display: flex; }
}

/* Appendix B, the concept index. Each entry is an index line: the concept name
   at the left, a dotted leader running across, and the Sec. / Fig. locators
   right aligned, with the one sentence definition on the line below so the index
   still teaches and does not only point. The definitions are the single source
   the first use glosses quote, so they never fork. Rows link with plain anchors;
   there is no page level brush bus. */
#concept-map .concept-intro { color: var(--ink-soft); max-width: var(--measure); }
.concept-list { list-style: none; padding: 0; margin: var(--sp-5) 0 0; }
.concept-row {
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--hairline);
}
.concept-row:last-child { border-bottom: 1px solid var(--hairline); }

/* Narrow first: name, then locators, then definition, all stacked; the leader is
   a no op until there is width for a single line index row. */
.concept-head { display: block; }
.concept-name { font-family: var(--font-sans); font-weight: 650; color: var(--ink); }
.concept-leader { display: none; }
.concept-links {
  display: flex; flex-wrap: wrap; gap: 0.4em 1.1em;
  margin-top: 0.1em;
  font-family: var(--font-sans); font-size: 0.82rem;
}
.concept-links a { color: var(--faint); text-decoration: none; }
.concept-links a:hover { color: var(--accent); }
.concept-def { display: block; color: var(--ink-soft); margin-top: 0.25em; }

@media (min-width: 720px) {
  /* Wide: the head becomes one index line. The name holds its width, the leader
     stretches to fill, the locators sit at the right margin. Dots ride the
     baseline through a dotted border on the empty filler, visible in both themes
     because hairline-strong lifts in the dark override. */
  .concept-head { display: flex; align-items: baseline; gap: 0.6rem; }
  .concept-name { flex: none; }
  .concept-leader {
    display: block;
    flex: 1 1 auto;
    min-width: 1.5rem;
    border-bottom: 1px dotted var(--hairline-strong);
  }
  .concept-links {
    flex: none;
    margin-top: 0;
    justify-content: flex-end;
    white-space: nowrap;
  }
}

/* 10. FOCUS / REDUCED MOTION / PRINT  ===================================== */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
a:focus-visible { outline-offset: 3px; }
.btn:focus-visible { outline-offset: 2px; }

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

@media print {
  /* The dark selector must ride along here: it outranks the plain root reset,
     so without it a reader who prints from dark mode gets pale screen ink on
     white paper. With it, print output is the same whichever theme is set. */
  :root,
  html[data-theme="dark"] {
    color-scheme: light;
    --bg: #ffffff; --bg-glow: none;
    --surface: #ffffff; --surface-2: #f4f2ec;
    --ink: #000000; --ink-soft: #1a1a1a;
    --faint: #444444; --fainter: #767676;
    --hairline: #bbbbbb; --hairline-strong: #888888;
    --edge-front: #7d7d7d; --edge-back: #bdbdbd;
    --accent: #14314f; --shadow: none;
    /* Data hues pinned to the light set: the lifted dark palette is tuned for
       charcoal and prints pale, so paper always gets the paper-tuned inks. */
    --constraint-1: #c34818;
    --constraint-2: #0a6fb0;
    --constraint-3: #0a7f65;
    --constraint-4: #a83f80;
    --constraint-5: #9d6400;
    --objective: #7a2fb2;
    --trail: #6f6660;
    --constraint-1-fill: rgba(195, 72, 24, 0.16);
    --constraint-2-fill: rgba(10, 111, 176, 0.15);
    --constraint-3-fill: rgba(10, 127, 101, 0.15);
    --constraint-4-fill: rgba(168, 63, 128, 0.15);
    --constraint-5-fill: rgba(157, 100, 0, 0.16);
    --objective-fill: rgba(122, 47, 178, 0.14);
    --wash: rgba(181, 116, 0, 0.16);
    --region-fill: rgba(181, 116, 0, 0.16);
  }
  body { background: #fff; font-size: 11pt; }
  .toc, .scrubber-row, .btn, input[type="range"].scrubber { display: none !important; }
  .margin-note { float: none; width: auto; margin: var(--sp-3) 0; }
  aside.frame-note { float: none; width: auto; margin: var(--sp-3) 0; }
  figure, .figure-box, .offramp, .signal { break-inside: avoid; box-shadow: none; }
  a { color: var(--ink); text-decoration-color: var(--hairline-strong); }
  .cite-tip { display: none; }
  /* Back matter prints as paper back matter. Keep a reference entry and a term
     row whole on the page, and drop any transient :target wash from a click
     made before printing. */
  .ref-list > li, .glossary-row, .concept-row { break-inside: avoid; }
  .ref-list > li:target { background: none; box-shadow: none; }
}

/* 11. MILESTONE B FIGURE BLOCKS  ==========================================
   Each figure implementer edits ONLY inside their own fig:<key> marker block
   below (shared-file discipline). The cross-highlight brush .is-lit is defined
   ONCE, in fig:sync, and every figure reuses it; per-shape refinements live in
   each figure's own block. The S3 block (fig:dualview) is section 8b above.
   See integration contract section 5. */

/* fig:sync  (shared linked-brushing brush)  ------------------------------- */
/* The neutral cross-highlight the sync.js linkFigure delegation toggles on
   every element sharing the hovered or keyboard-focused element's data-key.
   HTML elements light with the warm --wash background; SVG shapes take no
   background, so they light via a stroke token instead (a face-fill shape needs
   a visible stroke to read as lit). Only background and stroke change here,
   never layout, so brushing costs no reflow and CLS stays ~ 0. The wash equals
   the tableau .is-leaving wash, so the brush never fights step state, and
   .is-lit is kept at specificity 0,1,0 so the tableau .is-entering cell wins.
   No animation lives here, so there is nothing for reduced motion to disable. */
.is-lit { background: var(--wash); }
:where(svg) .is-lit { stroke: var(--objective); stroke-width: 2.5; }

/* fig:iso3d  (shared 3D wireframe depth cues: S3, S5, S6)  ==== fig:iso3d = */
/* One vocabulary for the three isometric wireframes. The figure JS classifies
   every edge front or back against the fixed view direction (iso3d.js
   classifyEdges, recomputed on every what-if perturbation and pivot-rule
   re-solve). Back edges recede: lighter ink and the ONE shared dash pattern,
   2 4, declared only here. It is deliberately distinct from the race
   schematic dash (6 5) and from the 3 3 gridline and guide hairlines, so a
   receding edge never reads as a gridline or a schematic path. Front edges
   keep the solid lifted stroke declared in each figure's own block, still
   subordinate to the 3px walk trail. The axis triad and origin label share
   one look across all three stages. */
.dv-edge.is-back,
.km-edge.is-back,
.race-edge.is-back {
  stroke: var(--edge-back);
  stroke-width: 1;
  stroke-dasharray: 2 4;
}
.iso-triad-arm { stroke: var(--faint); stroke-width: 1.2; }
.iso-triad-label,
.iso-origin {
  font-family: var(--font-sans);
  font-size: 11px;
  fill: var(--faint);
}

/* fig:formulation  (S2, data-figure="formulation")  ----------------------- */
/* Owner: S2 implementer. .fm-storyline cards + checkbox, .fm-stdform rows
   (coloured per conHue), .fm-slack-tag, and @keyframes fm-pulse (kill the pulse
   under prefers-reduced-motion). Each element inherits its constraint hue from
   an inline --fm-hue (set to conHue(i) in JS, or a token in the still), so both
   themes recolour for free and no colour is ever hard-coded here. */
.fm-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}
@media (max-width: 820px) {
  .fm-grid { grid-template-columns: 1fr; }
}
.fm-side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

/* storyline: the whole card is the label; the checkbox carries the on state */
.fm-storyline { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0; }
.fm-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35em 0.6em;
  align-items: start;
  padding: 0.55em 0.7em;
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.fm-card:hover { border-color: var(--accent); }
.fm-card.is-active { border-left-color: var(--fm-hue); background: var(--surface-2); }
/* Brushing must read even on an already-active card, so this refinement (0,2,0)
   outranks .is-active and repaints the wash when the card is lit. */
.fm-card.is-lit { background: var(--wash); }
.fm-check {
  margin-top: 0.15em;
  width: 1.05em;
  height: 1.05em;
  accent-color: var(--fm-hue);
  flex: none;
}
.fm-card-body { display: flex; flex-direction: column; gap: 0.15em; min-width: 0; }
.fm-card-title {
  font-family: var(--font-sans);
  font-weight: 650;
  font-size: 0.9rem;
  color: var(--ink);
}
.fm-card-desc {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  color: var(--ink-soft);
  line-height: 1.35;
}
.fm-card-ineq {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--fm-hue);
}

/* static brief lines the no-JS still shows before hydration */
.fm-brief {
  font-family: var(--font-sans);
  font-size: 0.85rem;
  color: var(--ink-soft);
  line-height: 1.4;
  margin: 0 0 0.5em;
}
.fm-brief:last-child { margin-bottom: 0; }
.fm-brief b { color: var(--ink); }

/* standard form: objective, one row per active limit, then nonnegativity */
.fm-stdform {
  display: flex;
  flex-direction: column;
  gap: 0.3em;
  font-family: var(--font-mono);
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 0.7em 0.8em;
}
.fm-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4em;
  font-size: 0.86rem;
  color: var(--ink);
  padding: 0.12em 0.3em;
  border-left: 3px solid transparent;
  border-radius: 3px;
}
.fm-row.fm-con { border-left-color: var(--fm-hue); }
.fm-obj { font-weight: 650; }
.fm-nonneg { color: var(--faint); }
.fm-slack { color: var(--fm-hue); font-weight: 650; }
.fm-slack-tag {
  font-family: var(--font-sans);
  font-size: 0.7rem;
  color: var(--fm-hue);
  border: 1px solid var(--fm-hue);
  border-radius: 999px;
  padding: 0.05em 0.55em;
  white-space: nowrap;
}
.fm-slack-tag.fm-pulse { animation: fm-pulse 0.9s ease-out 1; }
@keyframes fm-pulse {
  0%   { box-shadow: 0 0 0 0.05em var(--wash); background: var(--wash); }
  100% { box-shadow: 0 0 0 0.7em transparent; background: transparent; }
}

.fm-controls { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: 0.1em; }

/* fig:duality  (S4, data-figure="duality")  ------------------------------- */
/* Owner: S4 implementer. .s4-bar (plus an .is-lit refinement) and bar layout. */

/* Two panels of one figure: the primal resource limits and the dual shadow
   prices, side by side above 760px and stacked below. Lighting a resource on
   either side lights it on both (shared con: data-key, sync.js). --h holds the
   constraint's hue, set inline per element, so nothing here hardcodes a colour. */
.s4-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: stretch;
  margin-bottom: var(--sp-4);
}
@media (min-width: 760px) {
  .s4-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.s4-head {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--faint);
  margin: 0 0 var(--sp-2);
}

/* Primal panel: the five resource limits, which bind and which have room. */
.s4-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.s4-cx {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.1rem 0.6rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--h);
  border-radius: var(--radius);
}
.s4-cx-name {
  grid-column: 1 / 2;
  font-family: var(--font-sans);
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--ink);
}
.s4-cx-ineq {
  grid-column: 1 / 2;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--faint);
}
.s4-tag {
  grid-column: 2 / 3;
  grid-row: 1 / 3;
  align-self: center;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  padding: 0.2em 0.55em;
  border-radius: 999px;
}
.s4-bind { color: var(--h); border: 1px solid var(--h); background: transparent; }
.s4-slack {
  color: var(--faint);
  border: 1px solid var(--hairline-strong);
  background: var(--surface-2);
}

/* Dual panel: the figure-box holds the SVG still, then the live bar buttons
   fill it absolutely so the box height (aspect-ratio, floored by min-height) is
   fixed across the swap and CLS stays ~ 0. Five rows always split the height so
   nothing ever overflows the box. */
.s4-dual { display: flex; flex-direction: column; }
.s4-dual > .figure-box[data-figure="duality"] { flex: 1 1 auto; }
.figure-box[data-figure="duality"] {
  min-height: 12.5rem;
  background: var(--surface);
}
.s4-bars {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: repeat(5, 1fr);
  gap: var(--sp-2);
  padding: var(--sp-3);
}
.s4-bar {
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name val" "bar bar";
  align-content: center;
  gap: 0.3rem 0.6rem;
  margin: 0;
  padding: 0.35rem 0.6rem;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  cursor: pointer;
}
.s4-bar-name {
  grid-area: name;
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-weight: 600;
}
.s4-bar-val {
  grid-area: val;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--h);
}
.s4-bar-track {
  grid-area: bar;
  height: 0.7rem;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  overflow: hidden;
}
.s4-bar-fill {
  display: block;
  height: 100%;
  width: calc(var(--frac, 0) * 100%);
  background: var(--h);
  border-radius: 999px;
}
.s4-bar:hover { border-color: var(--h); }
.s4-bar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Lit refinement over the shared fig:sync .is-lit background: the constraint's
   own hue as an inset ring so the two panels tie together in colour, never
   colour alone. Specificity 0,2,0 re-asserts the wash over the base surface.
   Only background and box-shadow change, so brushing costs no reflow (CLS ~ 0). */
.s4-bar.is-lit,
.s4-cx.is-lit {
  background: var(--wash);
  box-shadow: inset 0 0 0 2px var(--h);
}

/* The strong-duality certificate, drawn. A compact aside above the
   readout: weak-duality note, then each binding limit priced by its shadow
   price times its rhs, the three products stacked left to right against a twin
   best-profit bar in objective violet. --w carries each contribution on a fixed
   scale; flex-grow turns it into a proportional width, so the stack and the twin
   bar read as one length and the segments sum to the profit. duality.js resets
   --w from the trace on hydration. Colour only from tokens. */
.s4-weak {
  font-family: var(--font-sans);
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: var(--sp-4) 0 var(--sp-3);
}
.s4-stack {
  margin: 0 0 var(--sp-4);
  display: grid;
  gap: 0.3rem;
}
.s4-stack-head {
  font-family: var(--font-sans);
  font-size: 0.74rem;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--faint);
  margin: 0 0 0.15rem;
}
.s4-stack-bar {
  display: flex;
  height: 1.5rem;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.s4-stack-seg {
  flex: var(--w, 0) 0 0;
  min-width: 0;
  background: var(--h);
}
.s4-stack-seg + .s4-stack-seg { box-shadow: inset 1px 0 0 var(--surface); }
.s4-stack-legend {
  display: flex;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1;
}
.s4-stack-lab {
  flex: var(--w, 0) 0 0;
  min-width: 0;
  text-align: center;
  color: var(--h);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: clip;
  padding: 0 0.1rem;
}
.s4-stack-total { display: block; margin-top: 0.1rem; }
.s4-stack-total-lab {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--objective);
  margin-bottom: 0.15rem;
}
.s4-stack-total-bar {
  display: block;
  height: 1.5rem;
  border-radius: 999px;
  /* fill set inline to var(--objective) so still and hydrated paint agree */
}
.s4-stack-slacks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.9rem;
  margin: 0.15rem 0 0;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  color: var(--faint);
}
.s4-stack-slack { border-radius: 999px; padding: 0.05rem 0.25rem; }
/* Lit refinements: the shared fig:sync .is-lit washes text; the coloured
   segment keeps its hue (its own rule wins on source order) and gains an ink
   ring, so brushing a slack limit or a priced segment always shows a change. */
.s4-stack-seg.is-lit { box-shadow: inset 0 0 0 2px var(--ink); }
.s4-stack-lab.is-lit { color: var(--h); text-decoration: underline; text-underline-offset: 2px; }

/* Value function (upgrade C): the con:3 shadow price drawn as a slope. A full
   width panel below the two-panel grid. Colour comes only from tokens; --h
   holds con:3's hue, set inline on the panel, so both themes recolour for free
   and the marks tie to the same constraint as the bar and the limit row. */
.s4-vf { margin-top: var(--sp-5); }
.vf-box { margin-bottom: var(--sp-3); }
.vf-controls {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-height: 5rem; /* reserve for slider row + readout so hydration shifts nothing */
}
.vf-slider-row { display: flex; align-items: center; gap: var(--sp-3); }
.vf-cap {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--faint);
  white-space: nowrap;
}
.vf-readout {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0;
}
.vf-readout b { color: var(--ink); }

/* Chart marks. The value curve is faint; the active segment, tangent and moving
   point wear con:3 (--h) as graphic marks (>= 3.0 contrast). Readable text stays
   in --ink / --faint tokens (>= 4.5). No transition, so reduced motion needs
   nothing and the point snaps instantly on each keystroke or drag. */
.vf-axis { stroke: var(--hairline-strong); stroke-width: 1; }
.vf-grid { stroke: var(--hairline); stroke-width: 1; }
.vf-tick { font-family: var(--font-sans); font-size: 10px; fill: var(--faint); }
.vf-tick-rest { fill: var(--accent); font-weight: 700; }
.vf-axis-title { font-family: var(--font-sans); font-size: 10px; font-weight: 600; fill: var(--faint); }
.vf-curve { fill: none; stroke: var(--faint); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.vf-kink { fill: var(--faint); }
.vf-seg { fill: none; stroke: var(--h); stroke-width: 3.25; stroke-linecap: round; }
.vf-tangent { fill: none; stroke: var(--h); stroke-width: 1.5; stroke-dasharray: 4 3; opacity: 0.75; }
.vf-guide { stroke: var(--h); stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.5; }
.vf-dot { fill: var(--h); stroke: var(--surface); stroke-width: 2; }
/* Brush refinement: con:3 lit anywhere pops the point without a reflow. Scaled
   about its own box so cx/cy stay authoritative for the drag. */
.vf-dot.is-lit { transform-box: fill-box; transform-origin: center; transform: scale(1.3); }

/* fig:kleeminty  (S5, data-figure="kleeminty")  --------------------------- */
/* Owner: S5 implementer. .km-grid (stack below 820px), chart axes and curves,
   .km-edge / .km-trail / .km-vertex / .km-current. Two panels, one section: a
   log scale cost chart on the left and the n = 3 cube walk on the right. Colour
   only ever comes from the shared tokens, so both themes recolour for free. No
   pulse or autoplay lives here, so reduced motion has nothing extra to disable. */
.km-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: start;
}
@media (min-width: 820px) {
  .km-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.km-panel { min-width: 0; }
/* Reserve the control rows so filling them on hydrate shifts nothing (CLS ~ 0). */
.km-slider-row, .km-cube-row { min-height: 2.6rem; }
.km-big {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--ink-soft);
  white-space: nowrap;
}
.km-big b { color: var(--ink); }

/* Chart: axes, gridlines, the two curves, the marker */
.km-gridline { stroke: var(--hairline); stroke-width: 1; }
.km-axis-line, .km-tick-mark { stroke: var(--hairline-strong); stroke-width: 1; }
.km-tick-label {
  font-family: var(--font-sans);
  font-size: 11px;
  fill: var(--faint);
}
.km-tick-y { text-anchor: end; }
.km-tick-x { text-anchor: middle; }
.km-axis-title {
  font-family: var(--font-sans);
  font-size: 11px;
  fill: var(--faint);
  text-anchor: middle;
}
.km-ref {
  fill: none;
  stroke: var(--constraint-2);
  stroke-width: 2;
  stroke-dasharray: 5 4;
  stroke-linecap: round;
}
.km-worst {
  fill: none;
  stroke: var(--trail);
  stroke-width: 2.75;
  stroke-linecap: round;
}
.km-curve-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
}
/* Raise specificity to (0,2,1) so these beat the global `.fig-svg text`
   (0,1,1) fill: var(--faint); otherwise both labels flatten to faint grey in
   both themes and lose the colour tie to their curve. */
.km-chart-svg text.km-worst-label { fill: var(--trail); text-anchor: end; }
.km-chart-svg text.km-ref-label { fill: var(--constraint-2); text-anchor: start; }
.km-guide {
  stroke: var(--objective);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.55;
}
.km-marker {
  fill: var(--objective);
  stroke: var(--surface);
  stroke-width: 1.5;
}

/* Cube: wireframe edges, the trail, the visited corners, the current corner */
.km-edge { stroke: var(--edge-front); stroke-width: 1.4; fill: none; }
.km-trail {
  stroke: var(--trail);
  stroke-width: 3;
  stroke-linecap: round;
  fill: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.km-trail.is-on { opacity: 1; }
.km-vertex { fill: var(--fainter); }
.km-vertex.is-visited { fill: var(--trail); }
.km-current { fill: var(--objective); stroke: var(--surface); stroke-width: 2; }
.km-current-ring { fill: none; stroke: var(--objective); stroke-width: 2; opacity: 0.5; }
.km-current-label {
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 650;
  fill: var(--objective);
}

/* Place your bets: revealed by default, so it reads with scripts off too */
.km-bet { margin-top: var(--sp-3); }
.km-bet-q {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin: 0 0 var(--sp-2);
}
.km-bet-row {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-bottom: var(--sp-2);
}
.km-bet-btn { flex: none; min-width: 2.6em; }
.km-bet-msg {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-style: italic;
  color: var(--faint);
  margin: 0;
}

/* Pivot-rule selector: Dantzig vs Bland, a live re-solve of the cube walk */
.km-rules { margin-top: var(--sp-2); }
.km-rule-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.km-rules-label {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.km-rule-btn { flex: none; min-width: 4.4em; }
.km-rule-msg {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-style: italic;
  color: var(--faint);
  margin: var(--sp-2) 0 0;
  min-height: 1.2em;
}

/* Bland's walk as a divergence-aware ghost on the cube. Distinct from the
   Dantzig primary (--trail, solid) in both hue and stroke: teal and dashed, so on
   the four shared edges the offset pair reads as two lines, never one. */
.km-ghost {
  stroke: var(--constraint-3);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 7 4;
  fill: none;
  opacity: 0.95;
}

/* The rig card: the three rigged constraints copied from the trace, plus
   the bait line and who built the cube. Figure apparatus, not body prose, so it
   never counts against the S5 word cap. */
.km-rig {
  margin: var(--sp-4) 0 var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--constraint-1);
  border-radius: var(--radius);
  background: var(--surface-2);
  max-width: 42rem;
}
.km-rig-lead, .km-rig-bait {
  font-family: var(--font-sans);
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0;
}
.km-rig-lead b { font-family: var(--font-mono); color: var(--ink); }
.km-rig-stack {
  list-style: none;
  margin: var(--sp-2) 0;
  padding: 0;
  display: grid;
  gap: 0.2rem;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--ink);
}
.km-rig-bait { margin-top: var(--sp-2); color: var(--faint); }

/* fig:cycling  (S5 second exhibit, Chvatal 1983; mounted by kleeminty.js) --- */
/* The naive tie-break's basis loops with period six; Bland's rule escapes. The
   loop panel is the PRIMARY visual, the objective strip is the secondary read.
   Colour only from tokens: constraint-1 (vermillion) carries the stuck naive
   lane, constraint-3 (teal) the escaping Bland lane, matching the ghost on the
   cube above, so both themes recolour for free. */
.cyc-figure { margin-top: var(--sp-5); }
.cyc-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: start;
}
@media (min-width: 820px) {
  .cyc-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); }
}
.cyc-panel { min-width: 0; }
.cyc-loop-title, .cyc-strip-title {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  color: var(--ink-soft);
  margin: var(--sp-2) 0 0;
}
.cyc-readout {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  color: var(--faint);
  margin: var(--sp-2) 0 0;
}
.cyc-controls:empty { display: none; }
.cyc-scrub-row { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); }
.cyc-scrub { flex: 1 1 auto; min-width: 0; }
.cyc-caption {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin-top: var(--sp-3);
  max-width: 42rem;
}

/* Loop ring: six bases, arrows between, the sixth arrow closing back to pivot 0 */
.cyc-arc { stroke: var(--faint); stroke-width: 1.4; fill: none; }
.cyc-arc-close { stroke: var(--constraint-1); stroke-width: 2.4; }
.cyc-arrow-head { fill: var(--faint); }
.cyc-arc-label {
  font-family: var(--font-sans);
  font-size: 10px;
  fill: var(--faint);
}
.cyc-arc-label-close { fill: var(--constraint-1); font-weight: 650; }
.cyc-node-body {
  fill: var(--surface-2);
  stroke: var(--hairline-strong);
  stroke-width: 1.2;
}
.cyc-node-basis {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 650;
  fill: var(--ink);
}
.cyc-node-step {
  font-family: var(--font-sans);
  font-size: 10px;
  fill: var(--faint);
}
.cyc-node.is-current .cyc-node-body {
  fill: var(--constraint-1-fill);
  stroke: var(--constraint-1);
  stroke-width: 2.2;
}
.cyc-node.is-current .cyc-node-basis { fill: var(--constraint-1); }
.cyc-loop-hub {
  font-family: var(--font-sans);
  font-size: 10px;
  font-style: italic;
  fill: var(--faint);
}

/* Objective strip: naive flat at 0, Bland stepping out to 1 */
.cyc-strip-axis { stroke: var(--hairline-strong); stroke-width: 1; }
.cyc-strip-grid { stroke: var(--hairline); stroke-width: 1; }
.cyc-strip-tick {
  font-family: var(--font-sans);
  font-size: 11px;
  fill: var(--faint);
}
.cyc-strip-axis-title {
  font-family: var(--font-sans);
  font-size: 11px;
  fill: var(--ink-soft);
}
.cyc-strip-naive { fill: none; stroke: var(--constraint-1); stroke-width: 2.6; }
.cyc-strip-bland { fill: none; stroke: var(--constraint-3); stroke-width: 2.6; stroke-dasharray: 7 4; }
.cyc-strip-lab { font-family: var(--font-sans); font-size: 11px; font-weight: 600; }
.cyc-strip-lab-naive { fill: var(--constraint-1); }
.cyc-strip-lab-bland { fill: var(--constraint-3); }

/* fig:maxflow  (S6, data-figure="maxflow")  ------------------------------- */
/* Owner: S6 implementer. A source to sink network beside its linear program.
   Both panels share the data-key brush from fig:sync: HTML cells light with the
   --wash background, and each SVG arc lights via a stroke refinement below (a
   line has its own stroke, so the shared :where(svg) .is-lit stroke on the group
   does not reach it without this). All colour comes from tokens, so both themes
   recolour for free, and only stroke, fill, and background change on brushing,
   never layout, so CLS stays ~ 0. Saturated arcs carry a heavier stroke and a
   bold label as well as constraint-1 colour, so the state is never colour only. */
.mf-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: start;
}
@media (min-width: 820px) {
  .mf-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
}

/* network panel ----------------------------------------------------------- */
.mf-line {
  fill: none;
  stroke: var(--hairline-strong);
  stroke-width: 2.2;
  stroke-linecap: round;
  transition: stroke 0.12s ease, stroke-width 0.12s ease;
}
.mf-flabel {
  font-size: 12.4px;
  font-weight: 600;
  fill: var(--faint);
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}
/* saturated arcs run full: heavier, warm, bold, plus a mark, never colour only */
.mf-edge.is-saturated .mf-line { stroke: var(--constraint-1); stroke-width: 3.4; }
.mf-edge.is-saturated .mf-flabel { fill: var(--constraint-1); font-weight: 800; }
/* an idle arc carries nothing: thin and faint so its emptiness reads at a glance */
.mf-edge.is-empty .mf-line { stroke: var(--fainter); stroke-width: 1.3; }
.mf-edge.is-empty .mf-flabel { fill: var(--fainter); font-weight: 500; }
/* the brush wins over both states so a lit arc always reads as lit */
.mf-edge.is-lit .mf-line { stroke: var(--objective); stroke-width: 3.6; }
.mf-edge.is-lit .mf-flabel { fill: var(--objective); font-weight: 800; }

.mf-edge { cursor: default; }
.mf-edge:focus { outline: none; }
.mf-edge:focus-visible .mf-line { stroke: var(--objective); stroke-width: 3.6; }
.mf-edge:focus-visible .mf-flabel { fill: var(--objective); font-weight: 800; }

.mf-cut {
  fill: none;
  stroke: var(--objective);
  stroke-width: 1.6;
  stroke-dasharray: 5 4;
  opacity: 0.72;
}
.mf-cut-label {
  fill: var(--objective);
  font-size: 12.4px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.mf-node circle {
  fill: var(--surface);
  stroke: var(--hairline-strong);
  stroke-width: 1.6;
}
.mf-node text {
  fill: var(--ink);
  font-size: 13px;
  font-weight: 700;
}
.mf-node.is-terminal circle { fill: var(--surface-2); stroke: var(--accent); stroke-width: 2.4; }
.mf-node .mf-node-cap { fill: var(--faint); font-size: 12.4px; font-weight: 600; letter-spacing: 0.08em; }

/* matrix panel ------------------------------------------------------------ */
.mf-matrix-panel { min-width: 0; }
.mf-matrix-scroll { overflow-x: auto; }
.mf-matrix {
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  width: 100%;
}
.mf-matrix-cap {
  caption-side: top;
  text-align: left;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: var(--sp-1);
  padding-bottom: 0.4em;
}
.mf-matrix th, .mf-matrix td {
  padding: 0.3em 0.5em;
  text-align: center;
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}
.mf-rowhead {
  text-align: left;
  color: var(--faint);
  font-weight: 600;
  position: sticky;
  left: 0;
  background: var(--surface);
}
.mf-col-head {
  color: var(--ink);
  font-weight: 700;
  cursor: default;
  vertical-align: bottom;
}
.mf-col-head .mf-col-name { display: block; }
.mf-cut-tag {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--objective);
}
.mf-col-head.is-cut { box-shadow: inset 0 -2px 0 0 var(--objective); }
.mf-flow-row td { color: var(--ink); font-weight: 700; }
.mf-matrix td.is-saturated { color: var(--constraint-1); font-weight: 800; }
/* structural nonzeros promoted to full ink so the zeros fade, mirroring the
   sibling shortest-path matrix (.sp-matrix td.is-nz). */
.mf-matrix td.is-nz { color: var(--ink); font-weight: 650; }
/* the row-wise right-hand side column: a faint relation header aligned with the
   column names, mirroring the sibling .sp-bound-head. */
.mf-bound-head { color: var(--faint); vertical-align: bottom; }
/* the tenth (bound) column pushes the matrix past a narrow panel; tighten the
   cell gutters on small viewports so the whole table and its caption fit the
   scroll wrapper without a scrollbar. Desktop keeps the roomier gutters. */
@media (max-width: 460px) {
  .mf-matrix th, .mf-matrix td { padding-left: 0.34em; padding-right: 0.34em; }
}
.mf-col-head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .mf-line { transition: none; }
}

/* fig:shortestpath  (S6, data-figure="shortestpath")  --------------------- */
/* Owner: S6 shortest-path implementer. A four node graph beside its node
   potential bounds. con: ties each edge to its matrix row (they share the edge's
   constraint hue via --hue), var: ties each node to its potential column, both
   through the fig:sync brush. All colour comes from tokens, so both themes
   recolour for free; a route edge carries a heavier stroke AND a "route" tag as
   well as its hue, so the state is never colour only. Only stroke, fill, and
   background change on brushing, so CLS stays ~ 0. */
.sp-figure { margin: 0; }
.sp-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  align-items: start;
}
@media (min-width: 720px) {
  .sp-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr); }
}
.sp-graph-box { min-height: 9rem; }

/* graph panel */
.sp-line {
  fill: none;
  stroke: var(--hairline-strong);
  stroke-width: 1.8;
  stroke-linecap: round;
  transition: stroke 0.12s ease, stroke-width 0.12s ease;
}
.sp-wlabel {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  fill: var(--faint);
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}
/* an edge on the shortest route: its own hue, heavier, plus the route tag */
.sp-edge.is-route .sp-line { stroke: var(--hue); stroke-width: 3.4; }
.sp-edge.is-route .sp-wlabel { fill: var(--hue); font-weight: 800; }
/* the brush wins over both states so a lit edge always reads as lit */
.sp-edge.is-lit .sp-line { stroke: var(--objective); stroke-width: 3.6; }
.sp-edge.is-lit .sp-wlabel { fill: var(--objective); font-weight: 800; }
.sp-edge { cursor: default; }
.sp-edge:focus { outline: none; }
.sp-edge:focus-visible .sp-line { stroke: var(--objective); stroke-width: 3.6; }
.sp-edge:focus-visible .sp-wlabel { fill: var(--objective); font-weight: 800; }

.sp-node circle { fill: var(--surface); stroke: var(--hairline-strong); stroke-width: 1.6; }
.sp-node.is-terminal circle { fill: var(--surface-2); stroke: var(--accent); stroke-width: 2.4; }
.sp-node-id { fill: var(--ink); font-size: 12px; font-weight: 700; }
.sp-pot { fill: var(--faint); font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; }
.sp-node { cursor: default; }
.sp-node:focus { outline: none; }
.sp-node.is-lit circle,
.sp-node:focus-visible circle { stroke: var(--objective); stroke-width: 2.8; }
.sp-node.is-lit .sp-pot,
.sp-node:focus-visible .sp-pot { fill: var(--objective); font-weight: 800; }

/* matrix panel */
.sp-matrix-panel { min-width: 0; }
.sp-matrix-scroll { overflow-x: auto; }
.sp-matrix {
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  width: 100%;
}
.sp-matrix-cap {
  caption-side: top;
  text-align: left;
  font-family: var(--font-sans);
  font-size: 0.76rem;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: var(--sp-1);
  padding-bottom: 0.4em;
}
.sp-matrix th, .sp-matrix td {
  padding: 0.3em 0.5em;
  text-align: center;
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}
.sp-rowhead { text-align: left; color: var(--faint); font-weight: 700; }
.sp-edge-head { border-left: 3px solid transparent; padding-left: 0.55em; }
.sp-row.is-route .sp-edge-head { border-left-color: var(--hue); }
.sp-edge-name { font-weight: 700; color: var(--ink); }
.sp-route-tag {
  margin-left: 0.4em;
  font-family: var(--font-sans);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--hue);
}
.sp-col-head { color: var(--ink); font-weight: 700; cursor: default; }
.sp-col-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sp-bound-head { color: var(--faint); }
.sp-matrix td.is-nz { color: var(--ink); font-weight: 650; }
.sp-matrix td.sp-bound { color: var(--ink); font-weight: 700; }

@media (prefers-reduced-motion: reduce) {
  .sp-line { transition: none; }
}

/* fig:race  (S6, data-figure="race")  ------------------------------------- */
/* Owner: S6 race implementer. Two racers on one iso polytope, both recorded.
   Corner Case (the drone) replays the boundary walk: a solid trail in --trail and
   a solid-filled glyph. Strictly Feasible (the vessel) replays the recorded
   central path from the centralpath-topic21 sidecar: a muted --faint line and an
   outline hull, distinct from the boundary racer by colour and glyph. All colour
   comes from tokens; the glyphs ease along their paths and the global
   reduced-motion rule freezes that easing. */
.race-figure { margin: 0; }
.race-box { min-height: 11rem; }

/* polytope wireframe, shared by both racers; depth cues via fig:iso3d */
.race-edge { stroke: var(--edge-front); stroke-width: 1.4; fill: none; }

/* boundary racer: recorded trail + corners */
.race-trail {
  stroke: var(--trail);
  stroke-width: 3;
  stroke-linecap: round;
  fill: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.race-trail.is-on { opacity: 1; }
.race-trail.is-lit { stroke: var(--objective); stroke-width: 5; opacity: 1; }
.race-corner { fill: var(--fainter); transition: fill 0.15s ease; }
.race-corner.is-visited { fill: var(--trail); }
.race-corner.is-lit { fill: var(--objective); }
.race-corner:focus { outline: none; }
.race-corner:focus-visible { stroke: var(--objective); stroke-width: 2; }

/* the drone glyph is solid because it is real data */
.race-drone { transition: transform 0.28s ease; }
.race-drone-body { fill: var(--accent); }
.race-drone-rotor { fill: var(--accent); }
.race-drone-arm { stroke: var(--accent); stroke-width: 1.4; }

/* interior racer: the recorded central path, a solid muted line. The dashed
   is-schematic style is kept as a defensive fallback if the sidecar is missing. */
.race-interior { fill: none; }
.race-interior.is-schematic {
  stroke: var(--faint);
  stroke-width: 1.8;
  stroke-dasharray: 6 5;
  opacity: 0.85;
}
.race-interior.is-real { stroke: var(--faint); stroke-width: 2.4; }

/* the vessel glyph is an outline hull, distinct from the solid drone */
.race-vessel { transition: transform 0.28s ease; }
.race-vessel-hull { fill: none; stroke: var(--faint); stroke-width: 1.5; }
.race-vessel-fin { stroke: var(--faint); stroke-width: 1.2; }
.race-vessel-sail { fill: none; stroke: var(--faint); stroke-width: 1.2; stroke-linejoin: round; }

/* markers: the recorded interior start, the shared arrival */
.race-start { fill: none; stroke: var(--faint); stroke-width: 1.4; }
.race-arrival { fill: var(--objective); stroke: var(--surface); stroke-width: 2; }
.race-arrival-label {
  fill: var(--objective);
  font-family: var(--font-sans);
  font-size: 12.5px;
  font-weight: 700;
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.race-illus-label,
.race-schematic-tag {
  fill: var(--faint);
  font-family: var(--font-sans);
  font-size: 0.66rem;
  font-style: italic;
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.race-schematic-tag { font-style: normal; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }

/* controls + surrounding copy */
.race-controls { min-height: 2.6rem; }
.race-step-btn { flex: none; }
.race-play { flex: none; min-width: 4.2em; }
.race-step-tag { font-family: var(--font-mono); font-size: 0.78rem; color: var(--faint); white-space: nowrap; }
.race-legend { align-items: center; }
.race-glyph { width: 2.2em; height: 1.4em; flex: none; vertical-align: middle; }
.race-cue {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  color: var(--ink-soft);
  background: var(--surface-2);
  border-left: 3px solid var(--faint);
  border-radius: var(--radius);
  padding: 0.5em 0.75em;
  margin-top: var(--sp-3);
}
.race-cue b { color: var(--ink); }

/* ==========================================================================
 First-use glosses and the static glossary.
 A gloss is an inline term carrying a dotted underline; on hover or keyboard
 focus it reveals a one-sentence definition quoted verbatim from the concept
 map. The component satisfies WCAG 1.4.13: it is keyboard focusable, the
 definition is dismissible with Escape (main.js adds is-dismissed on Escape
 without moving focus), it stays open while the pointer rests on it, and the
 dotted affordance clears 3:1 non-text contrast in both themes via
 --gloss-underline. No title attribute is used anywhere; the definition is a
 nested role="tooltip" element also referenced by aria-describedby.
 ========================================================================== */
.gloss {
 position: relative;
 color: inherit;
 cursor: help;
 text-decoration-line: underline;
 text-decoration-style: dotted;
 text-decoration-color: var(--gloss-underline);
 text-decoration-thickness: 1px;
 text-underline-offset: 0.18em;
}
.gloss-def {
 display: none;
 position: absolute;
 left: 0;
 top: calc(100% + 0.4em);
 z-index: 40;
 width: max-content;
 max-width: min(22rem, 78vw);
 padding: 0.55rem 0.75rem;
 font-family: var(--font-sans);
 font-size: 0.82rem;
 font-weight: 400;
 font-style: normal;
 line-height: 1.45;
 text-align: left;
 text-decoration: none;
 color: var(--ink);
 background: var(--surface);
 border: 1px solid var(--hairline-strong);
 border-radius: var(--radius);
 box-shadow: var(--shadow);
}
.gloss:hover > .gloss-def,
.gloss:focus-within > .gloss-def {
 display: block;
}
/* Edge-aware flip (main.js adds .opens-left when the trigger is in the right
 half of the viewport): open the definition leftward so a wide card near the
 right of a line stays on screen instead of being clipped. Scoped above the
 mobile breakpoint so the fixed viewport-foot card below still wins there. */
@media (min-width: 48.0625rem) {
 .gloss.opens-left > .gloss-def {
  left: auto;
  right: 0;
 }
}
/* Escape dismissal (main.js) must win even while the trigger keeps focus. This
 rule follows the reveal rules so, at equal specificity, source order hides. */
.gloss.is-dismissed > .gloss-def {
 display: none;
}
/* Narrow viewports: the reading column fills the screen, so a definition
 tethered to a trigger on the right of a line would run off the edge and
 body{overflow-x:clip} would cut its text off mid word. Below the reading
 measure, dock the definition to the foot of the viewport as a centered card.
 position:fixed references the viewport and escapes the body clip, so the
 whole definition stays on screen and wraps within the width. This is pure CSS
 and works with scripts off, so a phone reader still gets the full sentence. */
@media (max-width: 48rem) {
 .gloss-def {
  position: fixed;
  left: 50%;
  right: auto;
  top: auto;
  bottom: 0.9rem;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(32rem, calc(100vw - 1.5rem));
 }
}

/* Appendix A, the glossary. The two column term and definition rhythm holds,
   tightened into the back matter register. The term reads in small caps sans, a
   dictionary lead in; the definitions are string identical to the concept map
   rows they source, so a definition never forks. */
#glossary .glossary-intro { color: var(--ink-soft); max-width: var(--measure); }
.glossary-list { list-style: none; padding: 0; margin: var(--sp-5) 0 0; }
.glossary-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.15em 1.2rem;
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--hairline);
}
.glossary-row:last-child { border-bottom: 1px solid var(--hairline); }
.glossary-name {
  font-family: var(--font-sans);
  font-weight: 600;
  font-variant: small-caps;
  letter-spacing: 0.03em;
  color: var(--ink);
}
.glossary-def { color: var(--ink-soft); }
@media (min-width: 720px) {
  .glossary-row {
    grid-template-columns: 11rem 1fr;
    align-items: baseline;
    gap: 0 1.4rem;
  }
}

/* fig:bench  (S7, Figures 11 and 12; static, no JS mount)  --------------- */
/* Same chart conventions as the Klee-Minty panel: hairline grid, faint sans
   ticks. One hue per runtime, held across both figures: ocean blue for the
   pure-Python reference, vermillion for the Rust core (dashed when the call
   crosses the PyO3 border, solid in process), teal for the browser build,
   trail grey for the border share of a call. */
.bench-gridline { stroke: var(--hairline); stroke-width: 1; }
.bench-axis-line, .bench-tick-mark { stroke: var(--hairline-strong); stroke-width: 1; }
.bench-tick-label {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--faint);
}
.bench-tick-y { text-anchor: end; }
.bench-tick-x { text-anchor: middle; }
.bench-axis-title {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--faint);
  text-anchor: middle;
}
.bench-band { fill: var(--wash); }
.bench-band-label {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--faint);
  text-anchor: middle;
}
.bench-line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.bench-line-py { stroke: var(--constraint-2); }
.bench-line-wasm { stroke: var(--constraint-3); }
.bench-line-native { stroke: var(--constraint-1); stroke-dasharray: 6 3.5; }
.bench-line-rust { stroke: var(--constraint-1); }
.bench-legend-swatch { fill: none; stroke-width: 2.5; stroke-linecap: round; }
/* Raised to (0,2,1) like the km labels: these beat the global `.fig-svg text`
   fill so the legend and the gap bracket keep their own ink. */
.bench-chart-svg text.bench-legend-label {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--ink-soft);
}
.bench-gap { stroke: var(--ink-soft); stroke-width: 1; }
.bench-chart-svg text.bench-gap-label {
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 600;
  fill: var(--ink-soft);
  text-anchor: end;
}
/* Figure 12 bars: the toll recedes to the trail grey so the thin solid
   core-solve sliver reads as the payload it is. */
.bench-chart-svg text.bench-row-label {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--ink-soft);
  text-anchor: end;
}
.bench-bar-py { fill: var(--constraint-2); opacity: 0.82; }
.bench-bar-solve { fill: var(--constraint-1); }
.bench-bar-toll { fill: var(--trail); opacity: 0.45; }
.bench-bar-note {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--faint);
}

/* ===================================================================== */
/* One instrument, two column tiers now. Hold every in-SVG label in       */
/* an 11 to 13px rendered band at the classic centered column, and in an  */
/* 11 to 14px band at the wider three rail width above 1460px, both above */
/* the 8.5px floor at 380px. Two levers, both double entry (they move the */
/* authored still and the hydrated paint together, and never touch a      */
/* coordinate): a max-width cap on each figure box caps the viewBox to     */
/* viewport scale so the render lands in band, and a font override on the  */
/* inline styled figures (hero, formulation, duality, and the value        */
/* function still) trims or lifts the render into the band. The wide block */
/* at the foot of this file retunes both levers inside the 1460px          */
/* breakpoint, where a grid cell, not always the cap, sets the rendered    */
/* scale. Caps do not bind at 380px, where every box sits under its cap.   */

/* Caps: the figure box shrinks with its reserved space, centered in the
   wide column. Each value is tuned from the measured scale so the figure's
   own font set renders inside 11 to 13px at 1280. */
.figure-box[data-figure="hero"] { max-width: 465px; margin-inline: auto; }
.figure-box[data-figure="formulation"] { max-width: 465px; margin-inline: auto; }
.figure-box[data-figure="duality"] { max-width: 375px; margin-inline: auto; }
.vf-box { max-width: 432px; margin-inline: auto; }
.km-box { max-width: 395px; margin-inline: auto; }
.cyc-loop-box { max-width: 432px; margin-inline: auto; }
.figure-box[data-figure="maxflow"] { max-width: 460px; margin-inline: auto; }
.sp-graph-box { max-width: 350px; margin-inline: auto; }
.race-box { max-width: 470px; margin-inline: auto; }
.bench-box { max-width: 520px; margin-inline: auto; }
.dv-stair-svg { max-width: 335px; margin-inline: auto; display: block; }

/* Font overrides for the inline-styled figures. Hero and formulation carry
   font-size on each text node; the value-function still does too. Overriding
   here lifts the rendered label without editing the inline style, so the
   still and the hydrated paint keep byte-identical style attributes and the
   lockstep parity holds. */
.figure-box[data-figure="hero"] svg.fig-svg text,
.figure-box[data-figure="formulation"] svg.fig-svg text { font-size: 16.5px !important; }
svg.vf-still text { font-size: 10px !important; }

/* The shared iso triad and origin labels are 11px, which lands in band on
   the S3 polytope and the S5 cube once those are capped. The race stage is
   the widest wireframe; give its triad and origin their own size so they
   clear the 8.5px floor at 380px. */
.race-stage-svg .iso-triad-label,
.race-stage-svg .iso-origin { font-size: 12.5px; }

/* ============================================================================
 Anomalies gallery. Three static small-multiples on the hero 2D chassis,
 at the post-S3 offramp seam. No JS, no in-SVG text: every word rides the HTML
 captions (a11y), and the marks reuse the shared color tokens so this strip
 speaks the same language as the hero. One shared viewBox (168 by 168) and one
 LP window (0 to 7) hold all three panels to the same scale.
 ============================================================================ */
.anomaly-strip {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--sp-5);
 scroll-margin-top: 5rem;
}
@media (min-width: 720px) {
 .anomaly-strip { grid-template-columns: repeat(3, 1fr); gap: var(--sp-4); }
}
.anomaly-panel {
 margin: 0;
 display: flex;
 flex-direction: column;
 align-items: flex-start;
 gap: var(--sp-2);
}
.anomaly-svg {
 width: 100%;
 aspect-ratio: 1 / 1;
 display: block;
 background: var(--surface);
 border: 1px solid var(--hairline);
 border-radius: var(--radius);
}
.anomaly-panel figcaption {
 margin-top: 0;
 max-width: none;
}
.anomaly-title {
 font-family: var(--font-sans);
 font-weight: 650;
 color: var(--ink-soft);
 margin-right: 0.3em;
}
/* Strip-level caption, styled to read like a figcaption with its figure number. */
.anomaly-cap {
 font-family: var(--font-sans);
 font-size: 0.86rem;
 line-height: 1.55;
 color: var(--faint);
 margin-top: var(--sp-4);
 max-width: 34rem;
}
.anomaly-cap b, .anomaly-cap strong { color: var(--ink-soft); font-weight: 650; }

/* Marks. The two nonnegativity walls read as quiet axes; the LP constraints
 carry the constraint hues; the feasible area and the objective reuse the
 region, trail, and objective tokens exactly as the hero does. */
.anomaly-axis { stroke: var(--hairline-strong); stroke-width: 1; }
.anomaly-constraint { fill: none; stroke-width: 2.4; stroke-linecap: round; }
.anomaly-constraint[data-c="1"] { stroke: var(--constraint-1); }
.anomaly-constraint[data-c="2"] { stroke: var(--constraint-2); }
.anomaly-constraint[data-c="3"] { stroke: var(--constraint-3); }
/* The redundant limit is dashed to say, at a glance, that it never binds. */
.anomaly-constraint.is-redundant { stroke-dasharray: 5 4; }
.anomaly-halfplane[data-c="1"] { fill: var(--constraint-1-fill); }
.anomaly-halfplane[data-c="2"] { fill: var(--constraint-2-fill); }
.anomaly-region {
 fill: var(--region-fill);
 stroke: var(--trail);
 stroke-width: 1.4;
 stroke-linejoin: round;
}
.anomaly-objective { stroke: var(--objective); stroke-width: 2.4; stroke-linecap: round; }
.anomaly-opt { fill: var(--trail); }

/* Region swatch for the shared legend (the other swatch hues already exist). */
.swatch[data-c="region"] { background: var(--region-fill); border-radius: 3px; }

/* ==========================================================================
   Three rail composition (screen, 1460px and up). Below 1460px the page
   is byte for byte the same as before: every rule here lives inside the
   breakpoint and print never matches it, so the classic centered column and
   the 42rem print measure are untouched. The reading column stays centered;
   a fixed Contents rail is anchored to the left of it, the builder notes
   widen into a right rail, and the figure blocks left align to the reading
   column and reach across the center plus the right rail. See the note
   above for the label band bookkeeping. Colours stay on the existing tokens,
   no new hue, no JS, plain grid and calc only.
   ========================================================================== */
@media screen and (min-width: 1460px) {
  :root {
    --measure: 46rem;    /* center reading column, up from 42rem */
    --page-w: 63rem;     /* figure column spans center plus right rail */
    --note-w: 17rem;     /* right rail notes, up from 13.5rem */
    --note-gap: 2rem;    /* clear gap between column and notes */
    --toc-rail: 13rem;   /* left Contents rail width */
    --toc-gap: 1.5rem;   /* clear gap between rail and column */
  }

  /* LEFT RAIL. The Contents nav is already a fixed rail here via the inline
     head style, anchored to the figure column and 10.5rem wide. This rule
     widens it to a real 13rem column and glues it beside the reading column
     instead: left:50% plus a negative margin equal to half the column box,
     the gap, and the rail width tracks the centered column at every width
     past 1460px, never over the figures. The nav.toc-nav.is-sticky form is
     (0,2,1), one step over the inline (0,2,0), so it wins wherever the two
     overlap regardless of source order. The inner .l-body wrapper drops its
     measure so the list fills the rail. Titles wrap and never clip; the
     category tags keep the is-sticky rule above, shown on the active row. */
  nav.toc-nav.is-sticky {
    position: fixed;
    top: 1.5rem;
    left: 50%;
    margin-left: calc(-1 * (var(--measure) / 2 + var(--gutter) + var(--toc-gap) + var(--toc-rail)));
    width: var(--toc-rail);
    margin-right: 0;
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
  }
  .toc-nav.is-sticky > .l-body {
    max-width: none;
    margin-inline: 0;
    padding-inline: 0;
  }

  /* CENTER plus RIGHT RAIL. The reading column keeps its own margin auto and
     centres at the new 46rem measure. The figure blocks left align to that
     column box and grow rightward to 63rem, so a figure reaches across the
     center and the right rail and ends level with the widened notes. */
  .l-page {
    margin-left: calc(50% - var(--measure) / 2 - var(--gutter));
    margin-right: auto;
  }

  /* Per figure caps, raised about 1.4x so each figure box fills more of the
     wider column. A two column grid cell can bind before the raised cap does,
     so the font compensation below is tuned to the real rendered width, cell
     or cap, not to a flat 1.4x. */
  .figure-box[data-figure="hero"] { max-width: 650px; }
  .figure-box[data-figure="formulation"] { max-width: 650px; }
  .figure-box[data-figure="duality"] { max-width: 525px; }
  .vf-box { max-width: 600px; }
  .km-box { max-width: 550px; }
  .cyc-loop-box { max-width: 600px; }
  .figure-box[data-figure="maxflow"] { max-width: 645px; }
  .sp-graph-box { max-width: 490px; }
  .race-box { max-width: 660px; }
  .bench-box { max-width: 545px; }
  .dv-stair-svg { max-width: 470px; }

  /* Font compensation. Each rule trims the authored label so the render lands
     near 12.7px at the figure's real width, inside the 11 to 14px rail band.
     Two tier figures keep their ratio: both tiers scale by one factor, and the
     km exponent tspans ride their parent em so they stay proportionally small.
     Hero, formulation and duality carry inline sizes, so their rules keep the
     important flag to win, matching the classic override mechanism. */
  .figure-box[data-figure="hero"] svg.fig-svg text { font-size: 12.5px !important; }
  .figure-box[data-figure="formulation"] svg.fig-svg text { font-size: 15.5px !important; }
  .figure-box[data-figure="duality"] svg.fig-svg text { font-size: 9.2px !important; }
  .vf-box svg.vf-still text { font-size: 7.6px !important; }
  .vf-box .vf-tick,
  .vf-box .vf-axis-title { font-size: 7.6px; }
  .km-box .km-tick-label,
  .km-box .km-axis-title,
  .km-box .km-curve-label { font-size: 9.7px; }
  .km-cube-svg .iso-triad-label,
  .km-cube-svg .iso-origin { font-size: 9.35px; }
  .km-cube-svg .km-current-label { font-size: 10.2px; }
  .cyc-loop-box .cyc-node-basis { font-size: 8.4px; }
  .cyc-loop-box .cyc-arc-label,
  .cyc-loop-box .cyc-node-step,
  .cyc-loop-box .cyc-loop-hub { font-size: 7.6px; }
  .figure-box[data-figure="maxflow"] .mf-node text { font-size: 11.5px; }
  .figure-box[data-figure="maxflow"] .mf-node .mf-node-cap,
  .figure-box[data-figure="maxflow"] .mf-flabel,
  .figure-box[data-figure="maxflow"] .mf-cut-label { font-size: 11px; }
  .sp-graph-box .sp-wlabel,
  .sp-graph-box .sp-node-id { font-size: 8.8px; }
  .sp-graph-box .sp-pot { font-size: 8.4px; }
  .race-box .race-arrival-label,
  .race-box .iso-triad-label,
  .race-box .iso-origin { font-size: 9.2px; }
  .race-box .race-illus-label,
  .race-box .race-schematic-tag { font-size: 7.8px; }
  .dv-stair-svg .dv-stair-val { font-size: 6.8px; }
}

/* 12. ACADEMIC BACK MATTER  (references, appendix a, appendix b)  ========= */
/* The three closing sections read as paper back matter, one register below the
   article: reduced body type in the existing small band, tighter paragraph air,
   and the numbered heads carried by the eyebrow (Back matter, Appendix A,
   Appendix B) over the plain h2 name. Set once here as a scope so the shift
   reads as one deliberate step down. h2 and eyebrow keep the page scale, since
   their sizes are absolute rem and do not inherit the scope; only running prose
   drops to the small band. */
.backmatter { font-size: var(--fs-small); line-height: 1.62; }
.backmatter p + p { margin-top: var(--sp-3); }

/* A hairline opens the back matter, the seam where the article ends and the
   references begin. Put it on the first back matter section only. */
.backmatter-open {
  border-top: 1px solid var(--hairline-strong);
  padding-top: var(--sp-7);
}

/* The references lead-in line matches its two appendix siblings: soft ink,
   held to the reading measure. */
#references .ref-intro { color: var(--ink-soft); max-width: var(--measure); }

/* References: a numbered [N] hanging indent. The bracket is drawn from the
   source order counter and sits flush left, the entry text is inset past it, so
   a wrapped line clears the marker the way a paper reference does. Because [N]
   is counted, each li id="ref-N" must match its position in the list. Top rule
   per entry and a bottom rule on the last match the two index lists above. */
.ref-list {
  list-style: none;
  counter-reset: ref;
  padding: 0;
  margin: var(--sp-5) 0 0;
}
.ref-list > li {
  counter-increment: ref;
  position: relative;
  padding: var(--sp-3) 0 var(--sp-3) 2.4rem;
  border-top: 1px solid var(--hairline);
  line-height: 1.6;
  scroll-margin-top: 5rem;   /* land clear of the sticky contents rail */
}
.ref-list > li + li { margin-top: 0; }
.ref-list > li:last-child { border-bottom: 1px solid var(--hairline); }
.ref-list > li::before {
  content: "[" counter(ref) "]";
  position: absolute;
  left: 0;
  top: var(--sp-3);
  font-family: var(--font-sans);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}
/* The lead in label of a reference (author or short title), set in the sans face
   so the eye finds the source before reading the note. Replaces the old .src-tag. */
.ref-tag {
  font-family: var(--font-sans);
  font-weight: 600;
  color: var(--ink);
}
/* Clicking an in text [n] lands here: a warm wash and an accent left rule mark
   the row without shifting it (inset shadow, not a border), and fade in so the
   jump is felt. Same wash as the page brush and the text selection, no new hue. */
.ref-list > li:target {
  background: var(--wash);
  box-shadow: inset 3px 0 0 var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  transition: background 0.3s ease;
}
