/* ============================================================
   Canadian Institute for Exotic Science
   "Instrument panel" — v0.7

   Built from `Exotic Science Design Package.dc.html`, artboards 1a–1m.
   1j is the token spec, 1k the component inventory, 1m the build notes.
   Where a value here differs from those artboards, the reason is written
   beside it and nowhere else.

   DIRECTION (1m, verbatim): "An instrument panel for a research institute:
   dark navy grounds for orientation and argument, white grounds for reading,
   orange as the single action and emphasis color, and a blue-cyan node mesh
   used only in heroes."

   WHY THIS RESTYLES EXISTING CLASS NAMES RATHER THAN INTRODUCING NEW ONES
   -----------------------------------------------------------------------
   The shipped site uses 305 distinct class names across 7,539 pages, and 70 of
   those pages are authored prose carried through verbatim — the Astro shell
   wraps them, nothing regenerates them. Their markup IS the contract.

   So `.article`, `.flag`, `.branch-item`, `.reading-list-*` keep their names
   and get the instrument-panel implementation. The vocabulary is the API; this
   file is a new implementation behind it. A page nobody touches still comes out
   looking right, and the four flags — already `.flag-established` and friends
   on 7,484 pages — port without a single edit to a brief.

   New names appear only for structures the design genuinely adds and nothing
   exists for: the mesh hero, the stat band, the topic-map grid, filter chips.
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap');

/* ---------- Design tokens (artboard 1j) ---------- */
:root {
  --ink-900: #060D1F;
  --ink-800: #0B1733;
  --ink-700: #132349;
  --ink-600: #2A3448;
  --mist:    #F4F6FB;
  --paper:   #FFFFFF;
  --muted:   #5B6880;
  --muted-dk:#B9C7E0;

  /* Accents — orange leads, blues carry the mesh. */
  --accent:      #F97316;
  --accent-text: #C2410C;   /* 1j: "#F97316 never carries body text on white" */
  --focus-light: #C2410C;   /* the focus ring on a light ground: 5.18:1 */
  --focus-dark:  #F97316;   /* and on a dark one: 6.32:1 against ink-800 */
  --cyan:        #38BDF8;
  --blue:        #1D4ED8;

  /* ---- The small label, and the one token 1j gets wrong ----
     1j specifies labels as Mono 500/11 with .2em tracking; 1k renders them at
     9–9.5px in #8b93a7. At 9px that is SMALL TEXT, needing 4.5:1, and #8b93a7
     measures 3.07 on white and 2.84 on mist. The 3:1 allowance it would rely on
     is for 18.66px+ text, or 14px+ bold — it does not reach 9px uppercase mono,
     which is the hardest thing on the page to read. Same hue, darkened until it
     clears: 4.53 on white. Labels ship at 11px, per 1j, not the artboard's 9. */
  --label:     #6D768F;
  --label-dk:  #9FB0CD;    /* the same role on a dark ground: 8.08 on ink-800 */

  --hairline:       rgba(11, 23, 51, .12);
  --hairline-soft:  rgba(11, 23, 51, .07);
  --hairline-dark:  rgba(255, 255, 255, .09);
  --hairline-mid:   rgba(255, 255, 255, .28);

  /* ---- The four flags ----
     "Flags are data, not decoration" (1m). They drive five surfaces, and the
     value a flag needs differs by surface, because a chip's colour is a
     BACKGROUND carrying dark text and an inline mark's colour is a 2px
     underline. Both are specified in 1k and both are kept.

     THREE OF THE FOUR MARKS FAIL AS SPECIFIED on the mist ground the build
     notes put body copy on: cyan 1.98, orange 2.59, grey 2.84, against the 3:1
     a graphical object needs. Each below is the artboard's own hue one step
     down — not a visible design change, and the same repair this site made in
     v0.2.1 when two of its four flags measured below threshold. Frontier is
     unchanged because it passes at 6.20. */
  --flag-established-mark:    #0896D6;   /* 3.31 white / 3.06 mist */
  --flag-frontier-mark:       #1D4ED8;   /* 6.70 / 6.20 — as specified */
  --flag-speculative-mark:    #EB6406;   /* 3.30 / 3.05 */
  --flag-handwave-mark:       #858DA2;   /* 3.32 / 3.07 */

  --flag-established-chip-bg: #E0F2FE;  --flag-established-chip-fg: #075985;
  --flag-frontier-chip-bg:    #DBE7FF;  --flag-frontier-chip-fg:    #1E3A8A;
  --flag-speculative-chip-bg: #FFEDD5;  --flag-speculative-chip-fg: #9A3412;
  --flag-handwave-chip-bg:    #F1F3F8;  --flag-handwave-chip-fg:    #3F4A60;

  /* ---- Type (1j) ---- */
  --font-display: "Space Grotesk", system-ui, sans-serif;
  --font-body:    "IBM Plex Sans", system-ui, sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, "Courier New", monospace;

  /* ---- Grid and scale (1j) ----
     "Grid: 12 columns, 1280px max, 24px gutters. Spacing scale
      4·8·12·16·24·32·48·64·96. Radius 2–3px only (this is an instrument panel,
      not a consumer app)." */
  --measure: 1280px;
  --measure-read: 760px;
  --gutter: 24px;
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;
  --radius: 2px;
  --radius-lg: 3px;

  /* "No shadows on dark sections; 0 1px 3px rgba(11,23,51,.08) on light cards." */
  --shadow-card: 0 1px 3px rgba(11, 23, 51, .08);

  /* The hero mesh, generated once from a fixed seed. A token so the
     geometry has one home and `.hero::before` says only where it goes. */
  --hero-mesh: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1280 380' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3CradialGradient id='g' cx='50%25' cy='60%25' r='60%25'%3E%3Cstop offset='0%25' stop-color='%231D4ED8' stop-opacity='.55'/%3E%3Cstop offset='100%25' stop-color='%23060D1F' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='1280' height='380' fill='url(%2523g)'/%3E%3Cg stroke='%2338BDF8' stroke-width='.6' opacity='.5' fill='none'%3E%3Cpath d='M-20.0 239.9 L168.6 281.5 L357.1 217.4 L545.7 233.2 L734.3 213.8 L922.9 262.9 L1111.4 260.4 L1300.0 274.5'/%3E%3Cpath d='M-20.0 189.2 L168.6 129.9 L357.1 200.4 L545.7 203.6 L734.3 195.4 L922.9 111.4 L1111.4 191.7 L1300.0 165.7'/%3E%3Cpath d='M-20.0 332.1 L168.6 286.2 L357.1 277.0 L545.7 296.1 L734.3 328.5 L922.9 316.4 L1111.4 322.2 L1300.0 270.2'/%3E%3Cpath d='M357.1 217.4 L357.1 200.4 M734.3 195.4 L922.9 316.4 M1111.4 260.4 L545.7 203.6 M545.7 203.6 L1111.4 322.2'/%3E%3C/g%3E%3Cg fill='%237DD3FC'%3E%3Ccircle cx='168.6' cy='281.5' r='2.0'/%3E%3Ccircle cx='734.3' cy='213.8' r='2.6'/%3E%3Ccircle cx='1300.0' cy='274.5' r='3.2'/%3E%3Ccircle cx='357.1' cy='200.4' r='2.0'/%3E%3Ccircle cx='922.9' cy='111.4' r='2.6'/%3E%3Ccircle cx='-20.0' cy='332.1' r='3.2'/%3E%3C/g%3E%3Cg fill='%23F97316'%3E%3Ccircle cx='545.7' cy='296.1' r='2.4' opacity='0.80'/%3E%3Ccircle cx='1111.4' cy='322.2' r='2.9' opacity='0.85'/%3E%3C/g%3E%3C/svg%3E");
}

/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink-600);
  /* 1j: Body Plex 400/16·1.75. 1m: "Body copy never below 16px on any
     breakpoint." The artboards are drawn 620px wide with type scaled to fit
     that frame; the shipped sizes are 1j's, not the artboard's rendered px. */
  font: 400 16px/1.75 var(--font-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5 { font-family: var(--font-display); font-weight: 600; margin: 0; }
h1 { font-size: clamp(2rem, 4.2vw, 3.25rem); line-height: 1.12; letter-spacing: -.015em; }
h2 { font-size: clamp(1.3rem, 2.4vw, 1.6rem); line-height: 1.22; letter-spacing: -.01em; }
h3 { font-size: 1.12rem; line-height: 1.3; }
h4 { font-size: 1rem; }

p { margin: 0 0 var(--s4); }
ul, ol { margin: 0 0 var(--s4); padding-left: 1.3em; }
li { margin-bottom: var(--s2); }
strong { font-weight: 600; color: var(--ink-800); }
em { font-style: italic; }
sup { font-size: .72em; }
hr { border: 0; border-top: 1px solid var(--hairline); margin: var(--s7) 0; }
code, kbd, samp { font-family: var(--font-mono); font-size: .92em; }

a { color: var(--accent-text); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* 1m: "Focus ring 2px #F97316 at 2px offset on both grounds."
   Kept on dark, where #F97316 is 6.32:1 against ink-800. NOT kept on the light
   ground, where it is 2.80:1 — below the 3:1 a focus indicator needs, and the
   focus ring is the only thing a keyboard user has to locate themselves with.
   Found by `_contrastcheck.py` on its first run, not by reading the spec.
   The substitution is the artboard's own: 1j already rules that "#F97316 never
   carries body text on white — use #C2410C". #C2410C measures 5.18:1. */
:focus-visible { outline: 2px solid var(--focus-light); outline-offset: 2px; }
::selection { background: rgba(249, 115, 22, .22); }
img { max-width: 100%; height: auto; }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--ink-900);
  padding: var(--s3) var(--s5); font: 600 14px var(--font-display);
}
.skip-link:focus { left: 0; }

/* ---------- Ground utilities ----------
   The section rhythm from 1m: dark hero (+ optional dark stat band) → white
   reading → mist lists and cards → dark closing → dark footer. "Never two dark
   sections adjacent except hero + stat band." That is a composition rule and it
   lives in the templates; these are only the grounds it composes from. */
.on-dark  { background: var(--ink-800); color: #fff; }
.on-deep  { background: var(--ink-900); color: #fff; }
.on-mist  { background: var(--mist); }
.on-paper { background: var(--paper); }

.on-dark a, .on-deep a, .topbar a, .site-footer a { color: var(--accent); }
.on-dark :focus-visible, .on-deep :focus-visible,
.topbar :focus-visible, .site-footer :focus-visible { outline-color: var(--focus-dark); }
.on-dark h1, .on-dark h2, .on-dark h3, .on-deep h1, .on-deep h2, .on-deep h3 { color: #fff; }
.on-dark strong, .on-deep strong { color: #fff; }

/* ---------- Layout containers ---------- */
.container { max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter); }
.container-narrow { max-width: var(--measure-read); margin-inline: auto; padding-inline: var(--gutter); }

.section { padding-block: var(--s9); }
.section-header { margin-bottom: var(--s6); }
.section-number {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.on-dark .section-number, .on-deep .section-number { color: var(--accent); }
.section-title { margin-top: var(--s2); }
.section-intro { max-width: 62ch; color: var(--muted); }
.on-dark .section-intro, .on-deep .section-intro { color: var(--muted-dk); }

/* ---------- Top bar (artboard 1a) ----------
   "display:flex;align-items:center;justify-content:space-between;
    padding:13px 22px;background:#0b1733" */
.topbar { background: var(--ink-800); color: #fff; }
.topbar-inner {
  max-width: var(--measure); margin-inline: auto;
  padding: var(--s4) var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s5);
}
.brand { display: flex; align-items: center; gap: var(--s3); text-decoration: none; }
.brand:hover { text-decoration: none; }
.brand-mark { flex: none; width: 24px; height: 24px; display: block; }
.brand-text { font: 600 13px/1.18 var(--font-display); color: #fff; letter-spacing: .01em; }
.brand-name { display: block; }
/* 1a sets the second line of the wordmark in the accent. On ink-800 that is
   6.32:1, which is the one place 1j permits #F97316 to carry text. */
.brand-sub { display: block; color: var(--accent); }

.nav { display: flex; align-items: center; gap: var(--s5); }
.nav a {
  font: 500 14px var(--font-body); color: #C8D4EA; text-decoration: none;
  padding-block: var(--s1);
}
.nav a:hover { color: #fff; text-decoration: none; border-bottom: 2px solid var(--accent); }
.nav a[aria-current="page"] { color: #fff; border-bottom: 2px solid var(--accent); }
/* The one nav item that is a button. 1a draws it as the solid accent fill at
   the right of the bar; it is a link, so it keeps the nav's hover rule off. */
.nav-cta { padding: 8px 14px; font-size: 13px; }
.nav a.nav-cta:hover { border-bottom: 0; background: #E2650B; }
/* `.footer-col h4` and the brand heading were the old markup's shape; the
   Shell now emits the brand block as a container with its own text node, so
   the heading style has to reach both. */
.footer-col h4 {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label-dk); margin: 0 0 var(--s3);
}

/* ---------- Hero (artboard 1a) ---------- */
.hero { position: relative; overflow: hidden; background: var(--ink-900); color: #fff; }
.hero > .container, .hero > .container-narrow { position: relative; padding-block: var(--s9); }
.hero-eyebrow {
  font: 500 11px var(--font-mono); letter-spacing: .22em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s4);
}
.hero h1 { max-width: 20ch; }
.hero-lede { max-width: 58ch; margin-top: var(--s4); color: #B9C7E0; font-size: 1.05rem; }

/* The mesh.
   1m: "Pure SVG, no imagery. Three depth layers: faint radial glow (#1D4ED8 at
   55% fading to transparent), polyline network at 0.6px #38BDF8 / 50% opacity,
   nodes as 2-3.4px circles with two or three in #F97316. Confine it to hero
   blocks at 40-85% opacity; never behind body copy. Static by default."

   It is a BACKGROUND rather than an element, and that is the whole reason it
   reaches every hero: `.hero` lives in 70 pages of authored prose that nothing
   regenerates, so an element would mean editing each of them. The mesh is
   decorative and `aria-hidden` by design, which is exactly the content a
   background is for. Geometry generated once from a fixed seed rather than
   drawn by hand at the artboard's 620px, because `preserveAspectRatio` would
   otherwise stretch 0.6px strokes into bars at 1280.

   "Static by default" is taken at its word: no animation ships. The
   `components/Mesh.astro` element form exists for a hero that wants the
   breathing nodes and can afford the markup. */

.hero > .container, .hero > .container-narrow { position: relative; z-index: 1; }

/* "NEVER BEHIND BODY COPY" (1m) is a rule, not a preference, and two versions
   of this broke it in different ways.

   The first put the mesh straight on `.hero` as a background: the polylines ran
   through the lede.

   The second masked `.hero` -- which masks the ELEMENT, text and all, so the
   heading faded out with the mesh. `mask-image` is not a background property.

   So the mesh lives on a pseudo-element that holds nothing but itself: masked
   away across the left column where the heading and lede sit, full strength on
   the right where the artboard puts it. The percentages track the text column
   (`.hero h1` is 20ch, `.hero-lede` 58ch), so the fade begins past the longest
   line rather than at an arbitrary fraction.

   Below 900px the text column is the full width, so there is nowhere for the
   mesh to be that is not behind copy, and it goes entirely. */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: var(--hero-mesh);
  background-size: cover; background-position: center right;
  -webkit-mask-image: linear-gradient(to right, transparent 0 46%, #000 72%);
          mask-image: linear-gradient(to right, transparent 0 46%, #000 72%);
}
@media (max-width: 900px) { .hero::before { display: none; } }

/* The element form, for heroes that carry <Mesh />. */
.ip-mesh { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  .ip-mesh [data-mesh-node] { animation: ip-node-breathe 8s ease-in-out infinite; }
}
@keyframes ip-node-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }

/* The hero rail beneath the lede: "Coordinates / Current focus / Language". */
.hero-rail {
  display: flex; flex-wrap: wrap; gap: var(--s7);
  margin-top: var(--s6); padding-top: var(--s5);
  border-top: 1px solid rgba(255, 255, 255, .12);
}
.hero-rail-k {
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--label-dk);
}
.hero-rail-v { margin-top: var(--s1); font: 500 15px var(--font-body); color: #E8EDF7; }

/* ---------- Stat band (1k) ----------
   1m: "Stat band numbers come from the live catalogue count, not hardcoded."
   That is the template's rule to keep; this is the shape it fills. */
.stat-band { display: grid; grid-template-columns: repeat(4, 1fr); background: var(--ink-800); }
.stat { padding: var(--s5) var(--s4); border-right: 1px solid var(--hairline-dark); }
.stat:last-child { border-right: 0; }
.stat-n { font: 600 30px var(--font-display); color: var(--accent); line-height: 1.1; }
.stat-l { margin-top: var(--s1); font: 400 13px var(--font-body); color: var(--label-dk); }

/* ---------- Buttons (1k) ---------- */
.btn {
  display: inline-block; padding: 12px 20px;
  border: 1px solid transparent; border-radius: var(--radius);
  font: 600 14px var(--font-display); cursor: pointer; text-decoration: none;
}
.btn:hover { text-decoration: none; }
.btn-primary { background: var(--accent); color: var(--ink-800); }
.btn-primary:hover { background: #E2650B; }     /* 1k: "Hover: darken 8%." */
.btn-primary:active { background: #C2540A; }
.btn-inverse { background: var(--ink-800); color: #fff; }
.btn-inverse:hover { background: var(--ink-700); }
.btn-outline { border-color: rgba(11, 23, 51, .3); color: var(--ink-800); background: transparent; }
.btn-outline:hover { background: rgba(11, 23, 51, .05); }
.on-dark .btn-outline, .on-deep .btn-outline, .hero .btn-outline {
  border-color: var(--hairline-mid); color: #E8EDF7;
}
.on-dark .btn-outline:hover, .on-deep .btn-outline:hover, .hero .btn-outline:hover {
  background: rgba(255, 255, 255, .08);
}

/* ---------- The four flags ----------
   `.flag` is on 7,484 pages and `.flag-established` alone appears 25,604 times.
   The markup is `<span class="flag flag-established">Established</span>` and it
   does not change; only this does. 1m: "Flags must never be color-only — always
   the word" — the word is in the markup, which is why this port is free. */
.flag {
  display: inline-block;
  font: 500 11px var(--font-mono); letter-spacing: .04em;
  padding: 3px 7px; border-radius: var(--radius);
  white-space: nowrap; vertical-align: baseline;
}
.flag-established { background: var(--flag-established-chip-bg); color: var(--flag-established-chip-fg); }
.flag-frontier    { background: var(--flag-frontier-chip-bg);    color: var(--flag-frontier-chip-fg); }
.flag-speculative { background: var(--flag-speculative-chip-bg); color: var(--flag-speculative-chip-fg); }
.flag-handwave    { background: var(--flag-handwave-chip-bg);    color: var(--flag-handwave-chip-fg); }

/* On a dark ground the tinted chips glow. 1k draws the dark-ground chip as
   ink-900 with a light hue per flag; same construction, four hues. */
.on-dark .flag, .on-deep .flag, .hero .flag, .stat-band .flag { background: var(--ink-900); }
.on-dark .flag-established, .on-deep .flag-established, .hero .flag-established { color: #7DD3FC; }
.on-dark .flag-frontier,    .on-deep .flag-frontier,    .hero .flag-frontier    { color: #93B4FF; }
.on-dark .flag-speculative, .on-deep .flag-speculative, .hero .flag-speculative { color: #FDBA74; }
.on-dark .flag-handwave,    .on-deep .flag-handwave,    .hero .flag-handwave    { color: #C8D4EA; }

/* ---------- Article content ---------- */
.article { padding-block: var(--s8) var(--s9); }
.article-header { margin-bottom: var(--s7); }
.article-eyebrow {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.article-title { margin-top: var(--s3); }
.article-subtitle {
  margin-top: var(--s4); max-width: 60ch;
  font-size: 1.12rem; line-height: 1.6; color: var(--muted);
}
.article-meta {
  margin-top: var(--s5); padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; gap: var(--s5);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--label);
}
.article-body { max-width: 72ch; }
.article-body h2 {
  margin-top: var(--s8); margin-bottom: var(--s4);
  padding-top: var(--s4); border-top: 2px solid var(--ink-800);
}
.article-body h3 { margin-top: var(--s6); margin-bottom: var(--s3); color: var(--ink-800); }
.article-body p { margin-bottom: var(--s5); }
.article-body > ul, .article-body > ol { margin-bottom: var(--s5); }

/* ---------- Inline claim marks (1k) ----------
   "Underline weight 2px, color by flag. Click opens the source in a side
   panel." Applied to the flag span when it is inline in prose rather than a
   standalone chip — the corpus's own house pattern opens a paragraph with the
   chip, so the chip form above is the common case and this is the annotation. */
.claim { border-bottom: 2px solid currentColor; padding-bottom: 1px; }
.claim-established { border-bottom-color: var(--flag-established-mark); }
.claim-frontier    { border-bottom-color: var(--flag-frontier-mark); }
.claim-speculative { border-bottom-color: var(--flag-speculative-mark); }
.claim-handwave    { border-bottom-color: var(--flag-handwave-mark); border-bottom-style: dotted; }

/* ---------- Cards grid (1a, 1k) ---------- */
.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s5); }
.card {
  display: block; background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); padding: var(--s5);
  box-shadow: var(--shadow-card); text-decoration: none; color: inherit;
}
.card:hover { text-decoration: none; border-color: rgba(11, 23, 51, .25); }
.card-number { font: 500 11px var(--font-mono); letter-spacing: .1em; color: var(--accent-text); }
.card-title { margin-top: var(--s2); font: 600 18px var(--font-display); color: var(--ink-800); }
.card-desc { margin: var(--s2) 0 var(--s4); font-size: 14px; line-height: 1.6; color: var(--muted); }
.card-meta { display: flex; flex-wrap: wrap; gap: var(--s2); font: 500 11px var(--font-mono); color: var(--label); }
/* 1k: "Top border color = dominant flag of the programme." */
.card[data-flag="established"] { border-top-color: var(--flag-established-mark); }
.card[data-flag="frontier"]    { border-top-color: var(--flag-frontier-mark); }
.card[data-flag="speculative"] { border-top-color: var(--flag-speculative-mark); }
.card[data-flag="handwave"]    { border-top-color: var(--flag-handwave-mark); }

.card-status { padding: 2px 7px; border-radius: var(--radius); font: 500 11px var(--font-mono); }
.card-status.live    { background: var(--flag-established-chip-bg); color: var(--flag-established-chip-fg); }
.card-status.draft   { background: var(--flag-speculative-chip-bg); color: var(--flag-speculative-chip-fg); }
.card-status.planned { background: var(--flag-handwave-chip-bg);    color: var(--flag-handwave-chip-fg); }

/* ---------- Branch navigation (prev / up / next) ---------- */
.branch-block { margin-top: var(--s9); padding-top: var(--s6); border-top: 2px solid var(--ink-800); }
.branch-title {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label); margin-bottom: var(--s4);
}
.branch-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s4); }
.branch-item {
  display: block; padding: var(--s4);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper); text-decoration: none; color: inherit;
}
.branch-item:hover { text-decoration: none; border-color: var(--accent); background: var(--mist); }
.branch-item-direction { font: 500 11px var(--font-mono); letter-spacing: .1em; color: var(--accent-text); }
.branch-item-title { margin-top: var(--s2); font: 600 15px var(--font-display); color: var(--ink-800); }
.branch-item-desc { margin-top: var(--s1); font-size: 13px; line-height: 1.5; color: var(--muted); }

/* ---------- Index page TOC ---------- */
.index-list { list-style: none; padding: 0; margin: 0; }
.index-list li { margin-bottom: 0; border-bottom: 1px solid var(--hairline-soft); }
.index-list a { display: block; padding: var(--s3) 0; color: var(--ink-800); text-decoration: none; }
.index-list a:hover { color: var(--accent-text); text-decoration: none; }

.toc { position: sticky; top: var(--s5); }
.toc-title {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label); margin-bottom: var(--s3);
}
.toc a { display: block; padding: 4px 0 4px var(--s3); border-left: 2px solid var(--hairline);
         font-size: 13px; color: var(--muted); text-decoration: none; }
.toc a:hover, .toc a.active { border-left-color: var(--accent); color: var(--ink-800); text-decoration: none; }

/* ---------- The brief reader (artboard 1e) ----------
   1m: "Brief — sticky ladder sidebar, reading-progress bar under the nav, flags
   toggle in the header that turns inline marks off for clean reading."

   Everything here is ADDED to the DOM by `brief-reader.v0.9.js`, never hidden
   by it, so a page where the script does not run is the page it was before
   rather than a page with empty furniture in it. That is the v0.3 lesson: four
   interactive diagrams became empty boxes and every check passed. */

/* The ladder. A rail in the margin on a wide viewport, where the reading column
   is 760px inside a 1280px page and the space is already there; a collapsed
   accordion above the article below 1101px, which is 1m's "brief sidebar
   collapses to a top accordion under 900px" one breakpoint earlier, because
   this column runs out of room before the accordion rule does.

   The two modes are told apart by `is-accordion`, which the script sets from
   the same media query this sheet uses. Two places hold that number and they
   have to agree; the alternative was for the script to read a computed style
   to find out, which is worse to read and no more certain. */
.brief-ladder { display: none; }

.brief-ladder.is-accordion {
  display: block; margin-bottom: var(--s5);
  border: 1px solid var(--hairline); border-radius: 2px;
  background: var(--mist);
}
.brief-ladder.is-accordion .brief-ladder-title {
  display: flex; width: 100%; align-items: center; justify-content: space-between;
  margin: 0; padding: var(--s3) var(--s4);
  background: none; border: 0; cursor: pointer; text-align: left;
}
/* A disclosure caret drawn in text, so it inherits the label's colour and needs
   no asset. `aria-expanded` is what actually carries the state; this is the
   sighted half of the same fact. */
.brief-ladder.is-accordion .brief-ladder-title::after {
  content: "\25BE"; font-size: 13px; letter-spacing: 0;
  transition: transform .12s ease;
}
.brief-ladder.is-accordion.is-open .brief-ladder-title::after { transform: rotate(180deg); }
.brief-ladder.is-accordion ul { display: none; padding: 0 var(--s4) var(--s4); }
.brief-ladder.is-accordion.is-open ul { display: block; }
/* Two columns of section names on a tablet-width accordion; one on a phone.
   Fifteen names in a single column is most of a small viewport. */
@media (min-width: 620px) {
  .brief-ladder.is-accordion.is-open ul { columns: 2; column-gap: var(--s5); }
  .brief-ladder.is-accordion li { break-inside: avoid; }
}

/* A real two-column grid on a wide viewport rather than an absolutely
   positioned aside. The first version declared `position: absolute` in the
   media query and `position: sticky` after it; the later declaration won, so
   the ladder stayed IN the flow with the absolute rule's `left` applied, and
   the whole article was pushed down below a fifteen-item list. Sticky and
   absolute do not compose -- one of them is the position, and for a sidebar
   that must scroll with its reader the answer is sticky inside a column that
   actually exists. */
@media (min-width: 1101px) {
  .article[data-brief-reader] > .container-narrow {
    max-width: calc(190px + var(--s8) + 760px);
    display: grid; grid-template-columns: 190px minmax(0, 760px);
    /* Column gap only. The reading column's own children carry their vertical
       rhythm in margins, as they did before this grid existed, and a row gap
       here would both double that and charge for the empty rows the ladder
       spans past. */
    column-gap: var(--s8); row-gap: 0; align-items: start;
  }
  /* Everything the renderer emits sits in the reading column; only the ladder,
     which the script inserts, takes the first. Both rules are written at the
     SAME depth on purpose: the first version put the ladder's `grid-column: 1`
     on a bare `.brief-ladder`, which lost to the three-class `> *` rule above
     it and dropped the ladder into the reading column, stacked above the title
     — the same layout bug in a new place. Specificity is not a detail here;
     it is the whole mechanism. */
  .article[data-brief-reader] > .container-narrow > :not(.brief-ladder) {
    grid-column: 2;
  }
  .article[data-brief-reader] > .container-narrow > .brief-ladder {
    display: block; grid-column: 1;
    /* The row span is set by the script, not here. A sticky element is confined
       to its grid area, so a ladder placed in row 1 alone would scroll away at
       the end of the article header — and a ladder whose area IS row 1 makes
       that row as tall as fifteen section names, which left a 309px hole above
       the prose. It has to span every row, and the number of rows is the number
       of things the renderer emitted, which is a DOM fact and so is read from
       the DOM. A number written down here would be right until a brief gained
       a sixth block. */
    position: sticky; top: var(--s5);
    max-height: calc(100vh - var(--s7)); overflow-y: auto;
  }
}
.brief-ladder-title {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label); margin-bottom: var(--s3);
}
.brief-ladder ul { list-style: none; margin: 0; padding: 0; }
.brief-ladder li { margin: 0; }
.brief-ladder a {
  display: flex; gap: var(--s2); align-items: baseline;
  padding: 5px 0 5px var(--s3);
  border-left: 2px solid var(--hairline);
  font-size: 13px; line-height: 1.35; color: var(--muted); text-decoration: none;
}
.brief-ladder a:hover { color: var(--ink-800); text-decoration: none; border-left-color: var(--muted); }
/* The current section is marked by weight AND colour AND the rule, not by
   colour alone: a reader who cannot separate the two greys still has two other
   signals, and `aria-current` carries it to anyone listening. */
.brief-ladder a.is-current {
  color: var(--ink-800); font-weight: 600;
  border-left-color: var(--accent);
}
.brief-ladder-n {
  flex: none; min-width: 1.4em;
  font: 500 11px var(--font-mono); color: var(--label);
}
.brief-ladder a.is-current .brief-ladder-n { color: var(--accent-text); }

/* The progress bar, "under the nav". Fixed rather than sticky so it survives
   the article scrolling past it, and 3px so it reads as an instrument's rule
   rather than as a loading bar. */
.brief-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 50;
  background: transparent; pointer-events: none;
}
.brief-progress i { display: block; height: 100%; width: 0; background: var(--accent); }
@media print { .brief-progress { display: none; } }

/* The flags toggle. It sits in `.article-meta`, which is the header rail the
   artboard puts it in, and it is a real button: pressed state, keyboard
   focusable, and its label says what it will do rather than what it is. */
.brief-flagtoggle {
  appearance: none; cursor: pointer;
  padding: 4px 10px; border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper);
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.brief-flagtoggle:hover { border-color: var(--muted); color: var(--ink-800); }
.brief-flagtoggle[aria-pressed="true"] {
  background: var(--flag-handwave-chip-bg); border-color: transparent;
  color: var(--flag-handwave-chip-fg);
}

/* Flags muted. The CHIP KEEPS ITS WORD -- 1m: "Flags must never be color-only
   -- always the word, in chips and in the claim ledger." What comes off is the
   colour that makes dense prose busy; what a reader is never left with is an
   unflagged claim. So this desaturates the chip and leaves the text. */
.flags-muted .article-body .flag {
  background: var(--flag-handwave-chip-bg) !important;
  color: var(--flag-handwave-chip-fg) !important;
}
.flags-muted .article-body .claim { border-bottom-color: transparent !important; }
@media print { .flags-muted .article-body .flag { background: none !important; } }

/* ---------- Reading list & sources ---------- */
.reading-list { margin-top: var(--s8); padding: var(--s6); background: var(--mist); border-radius: var(--radius); }
.reading-list-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.reading-list-title { margin-top: var(--s2); }
.reading-list-intro { margin-top: var(--s3); color: var(--muted); font-size: 15px; }
.reading-list ul { list-style: none; padding: 0; margin: var(--s5) 0 0; }
.reading-list li {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s2) var(--s4);
  padding: var(--s4) 0; border-top: 1px solid var(--hairline);
}
.reading-list-name { font-weight: 500; color: var(--ink-800); }
.reading-list-kind {
  justify-self: end; align-self: start;
  font: 500 11px var(--font-mono); letter-spacing: .06em;
  padding: 2px 7px; border-radius: var(--radius);
  background: var(--flag-handwave-chip-bg); color: var(--flag-handwave-chip-fg);
}
.reading-list-desc { grid-column: 1 / -1; font-size: 14px; line-height: 1.65; color: var(--muted); }
.kind-paper    { background: var(--flag-frontier-chip-bg);    color: var(--flag-frontier-chip-fg); }
.kind-resource { background: var(--flag-handwave-chip-bg);    color: var(--flag-handwave-chip-fg); }
.kind-program  { background: var(--flag-speculative-chip-bg); color: var(--flag-speculative-chip-fg); }
.kind-lab      { background: var(--flag-established-chip-bg); color: var(--flag-established-chip-fg); }

/* ---------- The brief's standing explainer ----------
   `.curriculum-note` is on 328 pages and was styled by NEITHER stylesheet: it
   carried an inline `style="font-family:var(--font-mono);font-size:0.75rem"`
   in the template, which is both a second copy of a rule and 12px body copy,
   under the 16px floor 1m sets for any breakpoint. The inline style is gone and
   this is what it means.

   Worth recording that `_cssclasscheck.py` reported 285/285 with this class
   unstyled, and was right to: it measures the redesign against what the OLD
   sheet covered, and the old sheet did not cover this either. A class styled
   only from a template's inline attribute is invisible to a stylesheet audit
   from both directions. */
.curriculum-note {
  max-width: 68ch;
  font-size: 15px; line-height: 1.7; color: var(--muted);
}
.curriculum-note--aside {
  border-left: 3px solid var(--accent);
  padding-left: var(--s4);
  margin-bottom: var(--s5);
}
.on-dark .curriculum-note, .on-deep .curriculum-note { color: var(--muted-dk); }

/* ---------- Sections 13 and 14: horizons and the technology tree ----------
   `.framework-timeline` and `.framework-tree` are on all 319 briefs and were
   styled by neither sheet — each carries `style="list-style:none;padding-left:0"`
   in the brief records themselves, which is as far as an authored inline style
   can reasonably go. The list markup is authored and cannot change (it is in
   319 records and `_briefvalidate.py` requires it), so everything here hangs
   off the two class names and the `<strong>` lead each item already has.

   1k names "horizon timeline" in its component inventory and 1f shows it as
   "horizons as one timeline"; this is that component, built from the markup
   that exists rather than the markup the artboard drew. */
.framework-timeline, .framework-tree {
  list-style: none; padding-left: 0; margin: var(--s5) 0;
  border-top: 1px solid var(--hairline);
}
.framework-timeline > li, .framework-tree > li {
  margin: 0; padding: var(--s4) 0 var(--s4) var(--s6);
  border-bottom: 1px solid var(--hairline);
  position: relative;
}
/* The horizon marker: a mono rule in the gutter, weightier the further out the
   horizon goes. Nothing in the markup says which horizon a row is, so the
   marker is positional rather than semantic — it reads as a ladder, which is
   what a set of horizons is. */
.framework-timeline > li::before,
.framework-tree > li::before {
  content: ""; position: absolute; left: 0; top: calc(var(--s4) + .55em);
  width: var(--s5); height: 2px; background: var(--hairline);
}
.framework-timeline > li:nth-child(1)::before { background: var(--flag-established-mark); }
.framework-timeline > li:nth-child(2)::before { background: var(--flag-frontier-mark); }
.framework-timeline > li:nth-child(3)::before { background: var(--flag-speculative-mark); }
.framework-timeline > li:nth-child(4)::before { background: var(--flag-handwave-mark); }
.framework-tree > li::before { background: var(--accent); }

.framework-timeline > li > strong:first-child,
.framework-tree > li > strong:first-child {
  display: inline-block; min-width: 9em; margin-right: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-text);
}
.on-dark .framework-timeline > li > strong:first-child,
.on-deep .framework-tree > li > strong:first-child { color: var(--accent); }

/* The layered-brief panels. `LAYERED_BRIEFS` is off, so every panel renders in
   sequence — the no-JS path the template was designed around. Unstyled that is
   three headings with no separation between them. */
.lb-layer + .lb-layer { margin-top: var(--s8); }

@media (max-width: 620px) {
  .framework-timeline > li > strong:first-child,
  .framework-tree > li > strong:first-child { display: block; margin-bottom: var(--s1); }
}

/* ---------- Provenance block ---------- */
.prov-block { margin-top: var(--s6); }
.prov-label { font: 500 11px var(--font-mono); letter-spacing: .2em; text-transform: uppercase; color: var(--label); }
.prov-list { list-style: none; padding: 0; margin: var(--s3) 0 0; }
.prov-list li { display: flex; justify-content: space-between; gap: var(--s4);
                padding: 6px 0; border-bottom: 1px solid var(--hairline-soft); font-size: 14px; }
.prov-venue { color: var(--ink-600); }
.prov-n { font-family: var(--font-mono); font-size: 12px; color: var(--label); }
.prov-foot { margin-top: var(--s3); font-size: 13px; color: var(--muted); }

/* ---------- Footer ---------- */
.site-footer { background: var(--ink-900); color: #fff; padding-block: var(--s8) var(--s6); }
.footer-grid {
  max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter);
  display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--s6);
}
.footer-brand { font: 600 16px var(--font-display); color: #fff; }
.footer-brand p { margin-top: var(--s3); font-size: 14px; color: var(--muted-dk); font-weight: 400; }
.footer-col h3, .footer-col .footer-col-title {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label-dk); margin-bottom: var(--s3);
}
.footer-col ul { list-style: none; padding: 0; margin: 0; }
.footer-col li { margin-bottom: var(--s2); }
.footer-col a { color: #C8D4EA; font-size: 14px; text-decoration: none; }
.footer-col a:hover { color: var(--accent); text-decoration: underline; }
.footer-base {
  max-width: var(--measure); margin: var(--s7) auto 0; padding: var(--s5) var(--gutter) 0;
  border-top: 1px solid var(--hairline-dark);
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s4);
  font-size: 13px; color: var(--muted-dk);
}
.footer-version { font-family: var(--font-mono); font-size: 12px; color: var(--label-dk); }

/* ---------- Tables ---------- */
table { border-collapse: collapse; width: 100%; }
.atlas-table-wrap, .dir-scroll { overflow-x: auto; margin: var(--s5) 0; }
.atlas, .dir-table { width: 100%; border-collapse: collapse; border: 1px solid var(--hairline); }
.atlas thead th, .dir-table thead th {
  background: var(--ink-800); color: #C8D4EA; text-align: left;
  font: 500 11px var(--font-mono); letter-spacing: .06em; padding: 10px 12px;
}
.atlas td, .dir-table td { padding: 10px 12px; border-top: 1px solid var(--hairline); font-size: 14px; }
.atlas tbody tr:nth-child(even), .dir-table tbody tr:nth-child(even) { background: var(--mist); }
.atlas-code, .dir-n { font-family: var(--font-mono); font-size: 12px; white-space: nowrap; }
.dir-n { text-align: right; }
.atlas-name { font-weight: 500; color: var(--ink-800); }
.atlas-name-sub { display: block; font-size: 12px; color: var(--muted); font-weight: 400; }
.dir-note { font-size: 14px; color: var(--muted); }

/* ---------- One token these families need ----------
   Declared here rather than in the 1j block above because this file is being
   appended to by several hands at once and that block is not mine to rewrite.
   It merges with :root either way, and _contrastcheck.py reads declarations,
   not their position.

   1j's --cyan is #38BDF8: 2.14:1 against white, so it cannot carry white text
   and cannot be a fill behind any. This is that hue at a value that can —
   exactly the substitution 1j already makes for orange with --accent-text.
   It fills the bucket-A segment of the allocation bar (white label at 7.31)
   and colours the "raises / would confirm" direction in the two signpost
   families (7.31 on white, 6.76 on mist). Both uses are the same claim: cyan
   is 1j's established hue, and confirmation is the direction of established. */
:root { --cyan-deep: #0E5C82; }

/* ---------- Reader feedback (369 pages) ----------
   The widget's script is deliberately not shipped — `src/lib/pagescripts.ts`
   gives the reason: its consent line builds a link to the privacy page the site
   delisted on purpose, and with a blank endpoint the script leaves the mailto
   in place regardless. So `.is-live` is never set, the built form never
   renders, and `.feedback-fallback` is not a degraded state: it is the only
   state these 369 pages have ever had. It is styled as the live route — the
   address gets its own paper panel rather than small print under a form that
   is not there — and the prose in the markup already says so. The old sheet's
   `.is-live` rule is carried anyway, so re-enabling the script still hides it.

   The old sheet calls this "a call-to-action sibling of .checkpoint (crimson
   left accent)". That kinship is the one thing worth preserving from it: both
   ask the reader to do something, and both now carry the 3px orange rule,
   orange being this palette's single action colour. The analytic boxes further
   down (.researcher-qa, .assessment-box) take ink instead, which keeps the
   old sheet's own split between "act on this" and "weigh this". */
.feedback {
  /* .container-narrow, which every instance also carries, supplies the measure
     and the auto margins; this shorthand would drop its gutter, so the padding
     restates a horizontal value of its own that is wider than one. */
  margin: var(--s9) auto var(--s7);
  padding: var(--s6) var(--s6) var(--s5);
  background: var(--mist);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}
.feedback-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
/* An <h2>. Left at the reading size rather than the global h2 clamp: this sits
   after the article's own h2s and must not outrank them. */
.feedback-title {
  margin: var(--s2) 0 0;
  font: 600 20px/1.3 var(--font-display); color: var(--ink-800);
}
.feedback-intro {
  margin: var(--s3) 0 var(--s5);
  max-width: 62ch; font-size: 15px; line-height: 1.7; color: var(--muted);
}

.feedback.is-live .feedback-fallback { display: none; }
.feedback-fallback {
  padding: var(--s4) var(--s5);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  font-size: 15px; line-height: 1.7; color: var(--ink-600);
}
.feedback-fallback p:last-child { margin-bottom: 0; }
/* The status line, on the paper panel where --label measures 4.53. On the mist
   ground outside it the same token is 4.19 and would not clear. */
.feedback-fallback-note {
  display: block; margin-top: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--label);
}

@media (prefers-reduced-motion: reduce) {
  /* Carried from the old sheet. Nothing here animates, but the block this
     guards is the form, and the form is one config flag away from returning. */
  .feedback * { transition: none !important; animation: none !important; }
}

/* ---------- Researcher Q&A (30 pages) ----------
   A <details> accordion on the white reading ground. The old sheet set the
   intro in display italic; that was the serif design's editorial aside and has
   no counterpart here — Space Grotesk has no italic in the loaded set, and
   faking one is worse than dropping it. The aside is carried by the muted
   value instead. */
.researcher-qa {
  margin: var(--s9) 0 0;
  padding: var(--s6);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-top: 2px solid var(--ink-800);
  border-radius: var(--radius);
}
.researcher-qa-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label);
}
.researcher-qa-title {
  margin: var(--s2) 0 0;
  font: 600 22px/1.25 var(--font-display); letter-spacing: -.01em; color: var(--ink-800);
}
.researcher-qa-intro {
  margin: var(--s3) 0 var(--s5);
  max-width: 62ch; font-size: 15px; line-height: 1.7; color: var(--muted);
}

.qa-item { margin: 0; padding: 0; border-top: 1px solid var(--hairline-soft); }
.qa-item:first-of-type { border-top: 1px solid var(--hairline); }
.qa-item summary {
  position: relative; cursor: pointer; list-style: none;
  padding: var(--s4) var(--s7) var(--s4) 0;
  font: 600 16px/1.45 var(--font-display); color: var(--ink-800);
}
.qa-item summary::-webkit-details-marker { display: none; }
.qa-item summary::after {
  content: "+"; position: absolute; right: var(--s1); top: 50%;
  transform: translateY(-50%);
  font: 400 18px var(--font-mono); color: var(--accent-text);
}
.qa-item[open] summary::after { content: "\2212"; }
.qa-item summary:hover { color: var(--accent-text); }

.qa-answer { padding: var(--s1) 0 var(--s5); font-size: 15px; line-height: 1.75; color: var(--muted); }
.qa-answer p { margin: 0 0 var(--s3); }
.qa-answer p:last-child { margin-bottom: 0; }
.qa-answer strong { color: var(--ink-800); }
.qa-answer a { color: var(--accent-text); border-bottom: 1px solid var(--hairline); text-decoration: none; }
.qa-answer a:hover { border-bottom-color: var(--accent-text); text-decoration: none; }
.qa-answer code { padding: .1em .4em; background: var(--mist); border-radius: var(--radius); }
.qa-answer blockquote {
  margin: var(--s3) 0; padding: var(--s1) 0 var(--s1) var(--s4);
  border-left: 2px solid var(--hairline); color: var(--muted);
}

/* ---------- Checkpoint (26 pages) ----------
   The prompt-and-answer the learn modules interrupt themselves with. Sibling of
   .feedback above, per the old sheet, so it takes the same orange rule. The
   answer drops the old italic for the same reason .qa-answer did: it is the
   same thing — prose revealed by a <summary> — and the two should not be set
   differently on pages that carry both. */
.checkpoint {
  margin: var(--s6) 0;
  padding: var(--s5);
  background: var(--mist);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}
.checkpoint-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s2);
}
.checkpoint-q { margin: 0 0 var(--s3); font-weight: 500; color: var(--ink-800); }
.checkpoint-a { margin: 0; font-size: 15px; line-height: 1.75; color: var(--muted); }
.checkpoint details { margin-top: var(--s2); }
.checkpoint summary {
  cursor: pointer; user-select: none; padding: var(--s1) 0;
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-600);
}
.checkpoint summary:hover { color: var(--accent-text); }

/* ---------- Assessment box (17 pages) ----------
   "What would change our assessment" — the same instrument as .signpost below,
   at brief scale rather than claim scale, and its own label says so
   ("Falsifiable Signposts"). The two therefore share one direction colour pair:
   --cyan-deep for the signal that raises or confirms, --accent-text for the one
   that lowers or stalls. The old sheet used a green/crimson pair that this
   palette has no equivalent for; cyan and orange are 1j's own established and
   speculative hues, which is the axis these rows actually move along. Neither
   is colour-only — every row is labelled "Raises" or "Lowers" in the markup. */
.assessment-box {
  margin: var(--s7) 0 0;
  padding: var(--s6);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--ink-800);
  border-radius: var(--radius);
}
.assessment-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--label);
}
.assessment-title {
  margin: var(--s2) 0 var(--s5);
  font: 600 20px/1.25 var(--font-display); letter-spacing: -.01em; color: var(--ink-800);
}
.assessment-row {
  display: grid; grid-template-columns: 88px 1fr; gap: var(--s4);
  padding: var(--s3) 0; border-bottom: 1px solid var(--hairline);
}
.assessment-row:last-child { border-bottom: none; }
.assessment-dir {
  padding-top: 2px;
  font: 500 11px var(--font-mono); letter-spacing: .1em; text-transform: uppercase;
}
.assessment-dir.dir-up   { color: var(--cyan-deep); }
.assessment-dir.dir-down { color: var(--accent-text); }
.assessment-signal { font-size: 15px; line-height: 1.7; color: var(--muted); }
.assessment-signal strong { color: var(--ink-800); }

/* ---------- Funding allocation bar (4 pages) ----------
   1f: "buckets as bars". The five fills are the only colours in this batch that
   are load-bearing rather than decorative — each carries its own percentage as
   white mono text inside the segment, so each is a BACKGROUND needing 4.5:1
   against white, not a 3:1 graphical object. Measured: 7.31 / 6.70 / 15.39 /
   5.18 / 5.62. The ramp is deliberate: A·B·C step back through the mesh's own
   cyan → blue → navy depths so the three research buckets read as one family,
   safety takes the orange because it is the number a reader should catch, and
   integration takes the neutral grey.

   This block keeps its light ground inside a dark section (see the dark-ground
   rules further down, which leave it alone). Re-grounding it would mean
   recomputing all five fills against a new surface, and a data graphic whose
   colours are its data is the wrong place to do that twice. */
.allocation-block {
  margin: var(--s6) 0;
  padding: var(--s5) var(--s6);
  background: var(--mist);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
/* --label is 4.19 on mist. --muted is the same role one step down, at 5.20. */
.allocation-label {
  margin-bottom: var(--s4);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted);
}
.allocation-bar {
  display: flex; height: var(--s6); margin-bottom: var(--s4);
  border: 1px solid var(--ink-800); border-radius: var(--radius); overflow: hidden;
}
/* overflow/nowrap are new. A 14% segment is ~47px wide on a phone and its
   label is not; without them the text escapes its own fill and reads across the
   neighbouring one, which on a bar whose colours ARE its data is a wrong
   reading rather than an ugly one. Each segment now clips its own label and the
   legend below carries the full names. */
.allocation-segment {
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; white-space: nowrap; padding-inline: 2px;
  font: 500 11px var(--font-mono); letter-spacing: .04em; color: var(--paper);
  position: relative; transition: filter .15s;
}
.allocation-segment:hover { filter: brightness(1.12); }

.allocation-a           { background: var(--cyan-deep); }
.allocation-b           { background: var(--blue); }
.allocation-c           { background: var(--ink-700); }
.allocation-safety      { background: var(--accent-text); }
.allocation-integration { background: var(--muted); }

.allocation-legend {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  font: 400 13px var(--font-body); color: var(--ink-600);
}
.allocation-legend-item { display: flex; align-items: center; gap: var(--s2); }
/* The swatch repeats a segment's fill class, so its colour comes from the block
   above. Against mist the five measure 6.76 / 6.20 / 14.24 / 4.79 / 5.20 —
   every one clear of the 3:1 a 12px graphical object needs. */
.allocation-swatch { flex: none; width: 12px; height: 12px; display: inline-block; border-radius: 1px; }

/* ---------- Falsification signpost (2 pages) ----------
   The near-term test that would move a claim's flag. Dashed, per the old sheet,
   because the box describes something that has not happened yet — the one place
   in this file where a border style carries meaning. Direction colours are
   shared with .assessment-box above; see the note there. */
.signpost {
  margin: var(--s6) 0;
  padding: var(--s4) var(--s5);
  background: var(--paper);
  border: 1px dashed var(--hairline);
  border-radius: var(--radius);
}
.signpost-label {
  margin-bottom: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--label);
}
.signpost-row {
  display: flex; gap: var(--s3); margin: var(--s2) 0;
  font-size: 15px; line-height: 1.65; color: var(--muted);
}
.signpost-row .signpost-k {
  flex: none; min-width: 8.5em; padding-top: 2px; white-space: nowrap;
  font: 500 11px var(--font-mono); letter-spacing: .1em; text-transform: uppercase;
}
.signpost-confirm .signpost-k { color: var(--cyan-deep); }
.signpost-stall   .signpost-k { color: var(--accent-text); }

/* ---------- Featured panel (home) ----------
   The dark block that carries the topic map into the page. The old sheet laid a
   4px dot-grid over it with ::before — a paper-texture idiom belonging to the
   serif design. 1m gives the dark grounds one texture and confines it: "the
   mesh... never behind body copy", heroes only. So the panel is flat, and
   position:relative is kept only because the grid inside it relied on stacking
   above that overlay and cheap insurance costs one declaration. */
.feature-panel {
  position: relative; overflow: hidden;
  margin: var(--s9) 0 0; padding-block: var(--s9);
  background: var(--ink-900); color: #fff;
}
/* The top margin separates this dark panel from a LIGHT section above it. Where
   it follows the hero, the two dark grounds are separated by 96px of bare white
   with nothing in it, which reads as a mistake rather than as rhythm.
   1m's rule -- "never two dark sections adjacent except hero + stat band" -- is
   about composition, and the answer to a dark panel sitting against the hero is
   to put a light section between them on the page, not to draw an empty strip
   in CSS. The strip goes; the composition question stays with the page. */
.hero + .feature-panel, .stat-band + .feature-panel { margin-top: 0; }
/* .on-dark carries these for the named dark grounds; this panel is its own
   ground and has to state them, focus ring included — on ink-900 the light
   ring is 1.9:1 and a keyboard user would lose the link entirely. */
.feature-panel :focus-visible { outline-color: var(--focus-dark); }
.feature-grid { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: var(--s8); }
.feature-eyebrow {
  margin-bottom: var(--s4);
  font: 500 11px var(--font-mono); letter-spacing: .22em;
  text-transform: uppercase; color: var(--accent);
}
.feature-title {
  margin: 0 0 var(--s4);
  font-size: clamp(1.6rem, 3.2vw, 2.15rem); line-height: 1.15;
  letter-spacing: -.015em; color: #fff;
}
.feature-desc { margin: 0 0 var(--s6); max-width: 54ch; font-size: 1.05rem; line-height: 1.7; color: var(--muted-dk); }
.feature-link {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding-bottom: var(--s1);
  font: 500 11px var(--font-mono); letter-spacing: .18em; text-transform: uppercase;
  color: var(--accent); text-decoration: none;
  border-bottom: 1px solid var(--hairline-mid);
}
.feature-link::after { content: "\2192"; }
.feature-link:hover { text-decoration: none; border-bottom-color: var(--accent); }

.feature-stats { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s6); align-content: center; }
/* One orange per stat: the number takes it, as .stat-n does in the stat band,
   and the rule beside it steps back to the mid hairline. */
.feature-stat { padding-left: var(--s4); border-left: 2px solid var(--hairline-mid); }
.feature-stat-value { font: 600 34px/1 var(--font-display); color: var(--accent); }
.feature-stat-label {
  margin-top: var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; line-height: 1.45; color: var(--label-dk);
}

/* ---------- Dependency graph (atlas) ----------
   An SVG in a horizontal scroller. The min-width on the svg is what makes the
   scroller a scroller — drop it and the graph squeezes to illegibility instead
   of overflowing — so it is carried verbatim. */
.depgraph-wrap {
  margin: var(--s5) 0 0; overflow-x: auto;
  background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius);
}
.depgraph-wrap svg { display: block; min-width: 640px; width: 100%; height: auto; }

.depnode rect { fill: var(--mist); stroke: var(--muted); stroke-width: 1; transition: fill .15s; }
.depnode text { font: 500 11px var(--font-mono); fill: var(--ink-800); }
/* A bottleneck node is marked in the graph by nothing but this stroke — the ◆
   in the legend is not repeated on the node — so the doubled stroke-width does
   the work colour alone must not, and the hue is --accent-text rather than
   --accent because #F97316 is 2.80:1 on this ground and would fail the 3:1 a
   graphical object that carries meaning needs. */
.depnode.bottleneck rect { stroke: var(--accent-text); stroke-width: 2; }
.depnode:hover rect { fill: var(--ink-800); }
.depnode:hover text { fill: var(--paper); }
/* Every node is a link. The old sheet gave the inversion to :hover only, which
   left the keyboard path without the affordance the mouse path has. */
.depnode:focus-within rect { fill: var(--ink-800); }
.depnode:focus-within text { fill: var(--paper); }
/* 10 here is viewBox units, not CSS pixels: the svg is drawn 640 wide and
   rendered up to the 760px measure, so this lands at ~12px on screen. */
.depgraph-col-label {
  font: 500 10px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; fill: var(--label);
}

/* ---------- These families on the dark grounds ----------
   None of them sits inside .on-dark or .on-deep in the shipped corpus, and the
   old sheet had no dark variants. They are written because the new rhythm ends
   pages on a dark closing section and .feedback is the last block on a brief —
   the first template that moves it there should not have to discover that the
   box goes white-on-white. The panel lifts to ink-700 rather than dropping to
   ink-900, so it separates on .on-dark and .on-deep alike.

   .allocation-block and .depgraph-wrap are deliberately absent above: both are
   data graphics whose fills are computed against a light surface, and they keep
   that surface as a light inset. .depgraph-wrap gets a true dark variant below
   instead, because 1f asks for bottlenecks "in a dark block" by name. */
.on-dark .feedback, .on-deep .feedback, .hero .feedback,
.on-dark .checkpoint, .on-deep .checkpoint,
.on-dark .researcher-qa, .on-deep .researcher-qa,
.on-dark .assessment-box, .on-deep .assessment-box,
.on-dark .signpost, .on-deep .signpost {
  background: var(--ink-700); border-color: var(--hairline-dark);
}
.on-dark .feedback-title, .on-deep .feedback-title,
.on-dark .researcher-qa-title, .on-deep .researcher-qa-title,
.on-dark .assessment-title, .on-deep .assessment-title,
.on-dark .checkpoint-q, .on-deep .checkpoint-q,
.on-dark .qa-item summary, .on-deep .qa-item summary,
.on-dark .qa-answer strong, .on-deep .qa-answer strong,
.on-dark .assessment-signal strong, .on-deep .assessment-signal strong { color: #fff; }
.on-dark .feedback-intro, .on-deep .feedback-intro,
.on-dark .feedback-fallback, .on-deep .feedback-fallback,
.on-dark .researcher-qa-intro, .on-deep .researcher-qa-intro,
.on-dark .qa-answer, .on-deep .qa-answer,
.on-dark .checkpoint-a, .on-deep .checkpoint-a,
.on-dark .assessment-signal, .on-deep .assessment-signal,
.on-dark .signpost-row, .on-deep .signpost-row { color: var(--muted-dk); }
.on-dark .feedback-label, .on-deep .feedback-label,
.on-dark .checkpoint-label, .on-deep .checkpoint-label,
.on-dark .checkpoint summary, .on-deep .checkpoint summary,
.on-dark .qa-item summary::after, .on-deep .qa-item summary::after,
.on-dark .assessment-dir.dir-down, .on-deep .assessment-dir.dir-down,
.on-dark .signpost-stall .signpost-k, .on-deep .signpost-stall .signpost-k { color: var(--accent); }
.on-dark .researcher-qa-label, .on-deep .researcher-qa-label,
.on-dark .assessment-label, .on-deep .assessment-label,
.on-dark .signpost-label, .on-deep .signpost-label,
.on-dark .feedback-fallback-note, .on-deep .feedback-fallback-note { color: var(--label-dk); }
/* --cyan-deep is a fill, not a dark-ground text colour: 2.4:1 on ink-700.
   The raise/confirm direction takes 1j's own cyan here, at 8.28 on ink-800. */
.on-dark .assessment-dir.dir-up, .on-deep .assessment-dir.dir-up,
.on-dark .signpost-confirm .signpost-k, .on-deep .signpost-confirm .signpost-k { color: var(--cyan); }
/* The fallback panel was paper on mist; on ink-700 it steps the other way. */
.on-dark .feedback-fallback, .on-deep .feedback-fallback { background: var(--ink-900); }
.on-dark .qa-item, .on-deep .qa-item,
.on-dark .assessment-row, .on-deep .assessment-row { border-color: var(--hairline-dark); }
.on-dark .qa-answer a, .on-deep .qa-answer a { color: var(--accent); border-bottom-color: var(--hairline-mid); }
.on-dark .qa-answer code, .on-deep .qa-answer code { background: var(--ink-900); }

.on-dark .depgraph-wrap, .on-deep .depgraph-wrap { background: var(--ink-900); border-color: var(--hairline-dark); }
.on-dark .depnode rect, .on-deep .depnode rect { fill: var(--ink-700); stroke: var(--label-dk); }
.on-dark .depnode text, .on-deep .depnode text { fill: #fff; }
.on-dark .depnode.bottleneck rect, .on-deep .depnode.bottleneck rect { stroke: var(--accent); }
.on-dark .depnode:hover rect, .on-deep .depnode:hover rect,
.on-dark .depnode:focus-within rect, .on-deep .depnode:focus-within rect { fill: var(--mist); }
.on-dark .depnode:hover text, .on-deep .depnode:hover text,
.on-dark .depnode:focus-within text, .on-deep .depnode:focus-within text { fill: var(--ink-900); }
.on-dark .depgraph-col-label, .on-deep .depgraph-col-label { fill: var(--label-dk); }

/* ---------- Narrow widths for the families above ----------
   Kept beside the rules they modify rather than in the global Responsive
   section, which stays the place where the page skeleton reflows. */
@media (max-width: 900px) {
  .feature-grid { grid-template-columns: 1fr; gap: var(--s7); }
}
@media (max-width: 760px) {
  .feedback { padding: var(--s5) var(--s4); }
  .researcher-qa, .assessment-box { padding: var(--s5) var(--s4); }
  .allocation-block { padding: var(--s4); }
  .qa-item summary { font-size: 15px; padding-right: var(--s6); }
  /* 88px of mono label beside a 1fr signal leaves the signal unreadable once
     the measure drops; the two stack and the label keeps its colour. */
  .assessment-row { grid-template-columns: 1fr; gap: var(--s1); }
  .signpost-row { flex-direction: column; gap: var(--s1); }
  .signpost-row .signpost-k { min-width: 0; }
  .feature-stats { gap: var(--s5); }
  .feature-stat-value { font-size: 28px; }
  /* The old sheet shrank these to 0.65rem, which at its 17px root was the same
     11px they already are. Tracking is what buys the room instead; 1j's mono
     label floor is 11 and this is the one label on the page sitting on a
     coloured fill, so it is the last one to drop below it. */
  .allocation-segment { letter-spacing: 0; }
}

/* ---------- Glossary: inline term, script popover, glossary page ----------
   Three things share the `gloss` prefix and only one of them is a page.

   THE POPOVER IS POSITIONED BY SCRIPT, NOT BY THIS FILE.
   js/glossary-tooltip.v0.8.js appends `.gloss-pop` INSIDE the <dfn> and then
   writes top / left (or right, when the panel would run off the viewport) as
   inline styles. So `dfn.gloss` has to stay a positioning context and
   `.gloss-pop` has to stay absolutely positioned, or the panel resolves against
   the nearest positioned ancestor — in practice the page — and lands in the
   wrong place or off-screen. Nothing below sets an inset property: those belong
   to the script and a value here would fight it. There is no display/visibility
   toggle either; the script creates and removes the node.

   The v0.3 migration dropped the script and the tooltip went silent with every
   check green. Dropping either of the two load-bearing rules below would do the
   same thing again, and more quietly, because the script would still be there. */
dfn.gloss {
  position: relative;            /* the popover's containing block — load-bearing */
  font-style: normal;            /* <dfn> is italic by default; this is running prose */
  border-bottom: 1px dotted var(--muted);
  cursor: help;
}
dfn.gloss a { color: inherit; text-decoration: none; }
dfn.gloss:hover,
dfn.gloss[aria-expanded="true"] { color: var(--accent-text); border-bottom-color: var(--accent-text); }

/* One marked term sits in an Atlas table header, and this sheet paints those
   ink-800 — the old sheet's headers were light, so that ground is new and the
   dotted rule had to be told about it. */
.on-dark dfn.gloss, .on-deep dfn.gloss, .hero dfn.gloss,
.atlas thead th dfn.gloss { border-bottom-color: var(--muted-dk); }
.on-dark dfn.gloss:hover, .on-deep dfn.gloss:hover, .hero dfn.gloss:hover,
.atlas thead th dfn.gloss:hover,
.on-dark dfn.gloss[aria-expanded="true"], .on-deep dfn.gloss[aria-expanded="true"],
.atlas thead th dfn.gloss[aria-expanded="true"] { color: var(--accent); border-bottom-color: var(--accent); }

.gloss-pop {
  position: absolute;            /* script writes top/left — load-bearing */
  z-index: 40;
  max-width: 20rem;
  padding: var(--s3) var(--s4);
  background: var(--ink-900);
  color: #fff;
  border: 1px solid var(--ink-700);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius);
  font: 400 14px/1.6 var(--font-body);
  font-style: normal;            /* it is a child of the <dfn>; do not inherit italics */
  text-align: left;
  /* The one shadow this sheet has. "No shadows on dark sections" is about
     sections; this is an overlay whose whole job is to read as above the page. */
  box-shadow: var(--shadow-card);
}
/* The old rule drew this label in --gold-text, a value tuned for the paper
   ground, on an ink panel: 1.50:1. --accent is 6.90 on ink-900, and a dark
   ground is the one place 1j permits orange to carry text. */
.gloss-pop .gloss-pop-term {
  display: block; margin-bottom: var(--s1);
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--accent);
}
.gloss-pop .gloss-pop-more {
  display: block; margin-top: var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--label-dk);
}

.gloss-section-head {
  margin: var(--s7) 0 var(--s3); padding-bottom: var(--s2);
  border-bottom: 2px solid var(--ink-800);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.gloss-grid { display: grid; grid-template-columns: 1fr; margin: var(--s5) 0; }
.gloss-entry {
  display: grid; grid-template-columns: 200px 1fr; gap: var(--s5);
  padding: var(--s4) 0; border-bottom: 1px solid var(--hairline);
}
.gloss-entry:last-child { border-bottom: 0; }
.gloss-term { font: 600 17px var(--font-display); letter-spacing: -.01em; color: var(--ink-800); }
.gloss-term-id {
  display: block; margin-top: var(--s1);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--label);
}
.gloss-def { font-size: 15px; line-height: 1.7; color: var(--muted); }
.gloss-def p { margin: 0 0 var(--s2); }
.gloss-def p:last-child { margin-bottom: 0; }
.on-dark .gloss-term, .on-deep .gloss-term { color: #fff; }
.on-dark .gloss-term-id, .on-deep .gloss-term-id { color: var(--label-dk); }
.on-dark .gloss-def, .on-deep .gloss-def { color: var(--muted-dk); }

/* ---------- Back-references ("this page is referenced from") ----------
   A mist panel with a left rule, which is how the old sheet distinguished it
   from the prose above: a block you consult, not a block you read through. */
.backrefs-block {
  margin-top: var(--s8); padding: var(--s5) var(--s6);
  background: var(--mist);
  border-left: 2px solid var(--ink-700);
  border-radius: var(--radius);
}
/* --label is specified against white and measures 4.19 on mist, under the 4.5
   an 11px uppercase label needs. Every small label in this batch that sits on a
   mist panel draws --muted (5.20) instead; the ones on the white reading ground
   keep --label, like the rest of this file. */
.backrefs-label {
  margin-bottom: var(--s4);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted);
}
.backrefs-list { list-style: none; padding: 0; margin: 0; }
.backrefs-list li {
  display: grid; grid-template-columns: 140px 1fr; gap: var(--s4);
  margin-bottom: 0;              /* the base li margin would double the row rhythm */
  padding: var(--s2) 0; border-bottom: 1px solid var(--hairline-soft);
  font-size: 14px; line-height: 1.5;
}
.backrefs-list li:last-child { border-bottom: 0; }
.backrefs-kind {
  align-self: start;
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.backrefs-context { color: var(--ink-600); }
.backrefs-context a { color: var(--ink-800); text-decoration: none; border-bottom: 1px solid var(--hairline); }
.backrefs-context a:hover { color: var(--accent-text); border-bottom-color: var(--accent-text); text-decoration: none; }
.on-dark .backrefs-block, .on-deep .backrefs-block {
  background: var(--ink-900); border-left-color: var(--accent);
}
.on-dark .backrefs-label, .on-deep .backrefs-label,
.on-dark .backrefs-kind,  .on-deep .backrefs-kind  { color: var(--label-dk); }
.on-dark .backrefs-context, .on-deep .backrefs-context { color: var(--muted-dk); }
.on-dark .backrefs-context a, .on-deep .backrefs-context a { color: #fff; border-bottom-color: var(--hairline-dark); }

/* ---------- Learning module header ---------- */
.module-header {
  margin-bottom: var(--s8); padding-bottom: var(--s6);
  border-bottom: 2px solid var(--ink-800);
}
.module-id {
  margin-bottom: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.module-title { margin: 0 0 var(--s4); }   /* an <h1>; the base rule already sizes it */
/* The old rule set this in the display face, italic. Space Grotesk ships no
   italic, so that would be a synthesised oblique across four lines of the one
   sentence the module is accountable to. Body face, one step up, accent rule. */
.module-goal {
  max-width: 60ch; margin: 0 0 var(--s6);
  padding: var(--s3) 0 var(--s3) var(--s5);
  border-left: 3px solid var(--accent);
  font-size: 1.08rem; line-height: 1.6; color: var(--ink-600);
}
.module-horizon {
  display: flex; flex-wrap: wrap; gap: var(--s1) var(--s5);
  margin-top: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--label);
}
.module-frame { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s5); margin-top: var(--s5); }
.module-frame-block {
  padding: var(--s4) var(--s5);
  background: var(--mist); border: 1px solid var(--hairline); border-radius: var(--radius);
}
.module-frame-label {
  margin-bottom: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--muted);
}
.module-frame-list {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-wrap: wrap; gap: var(--s2);
}
.module-frame-list li { margin-bottom: 0; }   /* the base li margin fights the flex gap */
.module-frame-list a, .module-frame-empty {
  display: inline-block; padding: 5px 9px;
  background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius);
  font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-800); text-decoration: none;
}
.module-frame-list a:hover { border-color: var(--accent); color: var(--accent-text); text-decoration: none; }
/* Not a chip that does nothing — a sentence saying there is nothing. Lower
   case, italic, no border-as-affordance. */
.module-frame-empty {
  color: var(--muted); font-style: italic;
  letter-spacing: .02em; text-transform: none;
}
.on-dark .module-goal, .on-deep .module-goal { color: var(--muted-dk); }
.on-dark .module-horizon, .on-deep .module-horizon { color: var(--label-dk); }
.on-dark .module-frame-block, .on-deep .module-frame-block {
  background: var(--ink-900); border-color: var(--hairline-dark);
}
.on-dark .module-frame-label, .on-deep .module-frame-label { color: var(--label-dk); }
.on-dark .module-frame-list a, .on-deep .module-frame-list a {
  background: transparent; border-color: var(--hairline-mid); color: #fff;
}

/* ---------- Interactive readout row ----------
   The key/value pairs under a live diagram. `.interactive-readout` sets the
   row's mono face and size; these are only the two halves of a pair. --muted
   rather than --label because the readout sits inside a panel whose ground this
   batch does not own, and --muted clears 4.5 on white and on mist both. */
.readout-item { display: flex; gap: var(--s2); }
.readout-item .readout-k { color: var(--muted); }
.readout-item .readout-v { color: var(--ink-800); font-variant-numeric: tabular-nums; }
.on-dark .readout-item .readout-k, .on-deep .readout-item .readout-k { color: var(--label-dk); }
.on-dark .readout-item .readout-v, .on-deep .readout-item .readout-v { color: #fff; }

/* ---------- Advance Atlas: controls, rows, tags ----------
   1h: "the Atlas is a sortable table, flag column included." The table itself is
   ported under Tables above; this extends it rather than restating it. */
.atlas-controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  margin: var(--s5) 0 var(--s3);
}
.atlas-controls-label {
  margin-right: var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--label);
}
.atlas-filter {
  /* 2.5.8 asks 24px; the accessibility pass that shipped with v0.2.1 gave these
     44px. That is a behaviour, not a look, so it is carried verbatim. */
  min-height: 44px;
  padding: var(--s2) var(--s3);
  background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius);
  font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-600); cursor: pointer;
}
.atlas-filter:hover { border-color: var(--accent); color: var(--accent-text); }
.atlas-filter.active { background: var(--ink-800); border-color: var(--ink-800); color: #fff; }

/* Only the headers the script sorts should look clickable. The old sheet made
   all six pointer, including "Advance" and "Depends on", which sort nothing. */
.atlas thead th[data-key] { cursor: pointer; user-select: none; white-space: nowrap; }

/* Painted on the cell, not the row: the zebra stripe above is a row background,
   and a row background never shows through a cell that has one of its own. */
.atlas tbody tr.atlas-row:hover td { background: rgba(11, 23, 51, .06); }

.atlas-bucket-tag { font: 400 11px var(--font-mono); letter-spacing: .1em; color: var(--muted); }

/* ---------- Milestone list ---------- */
.milestones {
  list-style: none; padding: 0; margin: var(--s5) 0;
  display: flex; flex-direction: column; gap: var(--s2);
}
.milestones li {
  position: relative; margin-bottom: 0;
  padding: var(--s3) var(--s4) var(--s3) var(--s7);
  background: var(--mist); border-left: 2px solid var(--hairline);
  font-size: 15px; line-height: 1.6;
}
/* The marker is the only thing separating a met milestone from a target or a
   stretch goal, so it is a graphical object: --muted is 5.62 on the white it is
   filled with. The target and stretch variants recolour this box. */
.milestones li::before {
  content: ""; position: absolute; left: var(--s4); top: var(--s4);
  width: 12px; height: 12px;
  border: 2px solid var(--muted); background: var(--paper);
}
.milestones li strong { display: block; margin-bottom: var(--s1); color: var(--ink-800); }
.on-dark .milestones li, .on-deep .milestones li {
  background: var(--ink-900); border-left-color: var(--hairline-dark); color: var(--muted-dk);
}
.on-dark .milestones li::before, .on-deep .milestones li::before {
  border-color: var(--label-dk); background: var(--ink-800);
}
.on-dark .milestones li strong, .on-deep .milestones li strong { color: #fff; }

/* ---------- References list ----------
   The counter is the numbering the prose cites by, so it is behaviour: the
   reset, the increment and decimal-leading-zero all carry. */
.refs-list {
  list-style: none; padding: 0; margin: var(--s5) 0;
  counter-reset: ref;
  display: flex; flex-direction: column; gap: var(--s4);
}
.refs-list li {
  counter-increment: ref; position: relative;
  margin-bottom: 0; padding-left: var(--s7);
  font-size: 15px; line-height: 1.65; color: var(--ink-600);
}
.refs-list li::before {
  content: "[" counter(ref, decimal-leading-zero) "]";
  position: absolute; left: 0; top: 2px;
  font: 500 12px var(--font-mono); letter-spacing: .05em; color: var(--accent-text);
}
.refs-list a {
  color: var(--accent-text);
  text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: var(--hairline);
}
.refs-list a:hover { text-decoration-color: var(--accent-text); }
.on-dark .refs-list li, .on-deep .refs-list li { color: var(--muted-dk); }
.on-dark .refs-list li::before, .on-deep .refs-list li::before { color: var(--accent); }
.on-dark .refs-list a, .on-deep .refs-list a { color: var(--accent); text-decoration-color: var(--hairline-mid); }

/* ---------- Flag-history arrow ----------
   The "Speculative → Frontier · 2024" glyph inside `.flag-history`. It is the
   only part of that label carrying the direction of the change, hence the
   accent; the words on either side of it say the same thing. */
.flag-history .fh-arrow { padding: 0 .15em; color: var(--accent-text); }
.on-dark .flag-history .fh-arrow, .on-deep .flag-history .fh-arrow { color: var(--accent); }

/* ---------- Horizon timeline (1f: "horizons as one timeline") ----------
   Inline SVG, so these set fill/stroke, not color/background. The scroller and
   ground (`.timeline-wrap`) are not in this batch; everything below is computed
   against the white surface the old sheet gave it, and there is deliberately no
   .on-dark variant. That follows the convention `.allocation-block` already
   set in this file: a data graphic whose fills carry white labels keeps its
   light inset inside a dark section rather than restating six colours at a
   second value. `.timeline-wrap` has to keep a light ground for that to hold. */
.tl-grid { stroke: var(--hairline); stroke-width: 1; }
.tl-tick { font: 400 10px var(--font-mono); letter-spacing: .05em; fill: var(--label); }
.tl-axis-label, .tl-band-label {
  font: 500 10px var(--font-mono); letter-spacing: .14em;
  text-transform: uppercase; fill: var(--label);
}
/* A bar's right edge IS the horizon it encodes. Rounding it blurs the reading,
   so 0 here rather than the sheet's 2px — within 1j's "radius 2–3px only". */
.tl-bar rect { rx: 0; }
/* Three buckets, one family, separated by value rather than by three unrelated
   hues: the band letter at the left already names the bucket, so colour is
   reinforcement. Each fill carries white 10px mono at 15.4 / 6.7 / 5.6:1. */
.tl-bar-a rect { fill: var(--ink-700); }
.tl-bar-b rect { fill: var(--blue); }
.tl-bar-c rect { fill: var(--muted); }
.tl-bar-code { font: 500 10px var(--font-mono); fill: #fff; }
/* Fade the bar, not the group. The code label sits on the bar, so a fade of the
   whole <g> drops the label's contrast twice over; the old rule's 0.78 would
   put it at 3.52:1 on the lightest bucket. 0.9 on the rect alone holds 4.52. */
.tl-bar:hover rect { opacity: .9; }

/* ---------- Phase cards (roadmap) ---------- */
.phase-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s5); margin: var(--s6) 0; }
.phase-card {
  display: block; padding: var(--s5);
  background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  text-decoration: none; color: inherit;
}
/* The old hover lifted the card 2px. Same reason 1j caps the radius at 3: an
   instrument panel's readings do not move when you point at them. The border
   shorthand would reset the accent edge, so it is restated. */
.phase-card:hover {
  text-decoration: none;
  border-color: rgba(11, 23, 51, .25); border-top-color: var(--accent);
}
.phase-card-num {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.phase-card-title { margin-top: var(--s2); font: 600 20px var(--font-display); letter-spacing: -.01em; color: var(--ink-800); }
.phase-card-years { margin-top: var(--s1); font: 400 12px var(--font-mono); letter-spacing: .06em; color: var(--label); }
.phase-card-desc { margin: var(--s3) 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); }

/* ---------- Bottleneck emphasis ----------
   `.bottleneck` is never worn alone: it qualifies a dependency-graph node or a
   timeline bar (`class="tl-bar tl-bar-c bottleneck"`). The word travels
   separately, in `.bottleneck-star`, which is what keeps the mark from being
   colour-only where a reader meets it in the table.

   The outline is --accent-text and not --accent because the edge that has to be
   findable is the one against the chart's white ground, where #F97316 measures
   2.80 and #C2410C 5.18. Against the darkest bucket fill the inner edge is
   2.97; that is the boundary inside the shape, not the one that makes the mark
   perceivable, and it is why the stroke is 2px and centred on the outline.

   The graph node wears the same mark, set in the Dependency graph section
   above; it is not restated here, because a second copy of a colour is the
   thing this file exists to stop having. */
.tl-bar.bottleneck rect { stroke: var(--accent-text); stroke-width: 2; }
.bottleneck-star {
  font: 500 11px var(--font-mono); letter-spacing: .06em;
  color: var(--accent-text); white-space: nowrap;
}
.on-dark .bottleneck-star, .on-deep .bottleneck-star { color: var(--accent); }

/* ---------- Dependency-graph edges ----------
   The old rule drew these at 50% opacity, compositing to 2.09:1 on the graph's
   ground. The edges ARE the dependency relation — the entire argument of the
   figure — so they are a graphical object needing 3:1. --muted at 80% measures
   3.67 on white and 3.48 on mist, and still reads as mesh behind the nodes. */
.depedge { fill: none; stroke: var(--muted); stroke-width: 1; opacity: .8; }
/* The graph does get a dark variant (1f asks for bottlenecks "in a dark
   block"), and --muted at 80% is 2.62 on ink-900. At full opacity it is 3.44,
   still a step quieter than the --label-dk the nodes are stroked with, which is
   the relation the faded edges were drawing in the first place. */
.on-dark .depedge, .on-deep .depedge { opacity: 1; }

/* ---------- Responsive for the families above ----------
   Kept beside its families rather than merged into the shared Responsive
   section below, which is another agent's region. Same breakpoint as that one;
   these are the four grids the old sheet collapsed at 760px. */
@media (max-width: 760px) {
  .module-frame { grid-template-columns: 1fr; gap: var(--s4); }
  .phase-grid { grid-template-columns: 1fr; }
  .backrefs-list li { grid-template-columns: 1fr; gap: var(--s1); }
  .gloss-entry { grid-template-columns: 1fr; gap: var(--s2); }
  /* 20rem overflows a 320px viewport before the script's right-edge nudge can
     help, because the nudge only flips the anchor, not the width. */
  .gloss-pop { max-width: 16rem; }
}

/* ============================================================
   BATCH 3 — Explore, risk, cross-references, interactives, paths,
   hero meta, definitions.

   ONE RULE THAT APPLIES ACROSS ALL OF THEM: labels on the mist ground.
   --label is the 11px uppercase mono label colour and measures 4.53:1 on
   white, which is where the ported sections use it. On --mist it measures
   4.19, and an 11px uppercase mono label is small text needing 4.5. So every
   label in this batch that sits on a mist panel (.xref, .interactive,
   .definition-block) draws --muted (5.20) instead — same role, one step down,
   and a pair _contrastcheck.py already asserts.
   ============================================================ */

/* ---------- Explore: the search surface ----------
   The highest-reach family in this batch. `.ex-meta` alone is on 113 pages,
   and only three of them are the Explore page — see the note on `p.ex-meta`. */

/* 920px is neither --measure (the 12-column grid) nor --measure-read (the
   reading column). A result row is a title, a URL and a two-line excerpt: too
   wide at 1280, and 760 breaks the URLs. The old sheet chose 920 for the same
   reason and it is kept. */
.ex-wrap {
  max-width: 920px; margin-inline: auto;
  padding: var(--s7) var(--gutter) var(--s9);
}

/* Deliberately smaller than the h1 scale. Explore is a utility page; its title
   is a signpost, not the lede of an argument, and the old sheet made the same
   call at the same size. */
.ex-title {
  font: 600 clamp(1.8rem, 3.4vw, 2.25rem)/1.15 var(--font-display);
  letter-spacing: -.015em; color: var(--ink-800);
  margin: 0 0 var(--s3);
}
.ex-lede { max-width: 62ch; margin: 0 0 var(--s6); color: var(--muted); font-size: 1.05rem; }

.ex-input {
  width: 100%; min-height: 44px;          /* the touch target, carried */
  padding: var(--s3) var(--s4);
  font: 400 1.05rem var(--font-body);
  color: var(--ink-800); background: var(--paper);
  border: 1px solid var(--ink-800); border-radius: var(--radius);
}
.ex-input::placeholder { color: var(--label); }
/* Redundant with the global focus ring rather than a replacement for it: the
   ring sits 2px outside the field and this marks the field itself, which is
   what a reader watching the results list is looking at. */
.ex-input:focus { border-color: var(--accent-text); }

/* THREE ROLES, ONE CLASS.
   `.ex-meta` is the search label and the live result count (a <label> or a
   <div>), an inline meta tag in the experiments index (a <span>), and — on 112
   of its 113 pages — a <p> carrying whole sentences: 79 "this record moved"
   notices on citation shards, and the verdict line under each decisive
   experiment. The first two are micro-labels and keep the micro-label
   treatment. The third cannot: 1m says body copy never goes below 16px on any
   breakpoint, and the old rule set those sentences in 12px uppercase mono with
   .08em tracking, one of which is a full URL. Element-qualified so the split
   needs no markup change and no new class. */
.ex-meta {
  display: block; margin: var(--s4) 0 var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--label);
}
p.ex-meta {
  font: 400 15px/1.7 var(--font-body);
  letter-spacing: normal; text-transform: none; color: var(--muted);
  margin: var(--s4) 0; padding-left: var(--s4);
  border-left: 2px solid var(--hairline);   /* the .toc rail, reused as an aside mark */
}
/* The inline form is a tag inside a heading or a sentence, so it must not take
   the block margins above. */
span.ex-meta { display: inline; margin: 0; }

.ex-facets { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0 var(--s6); }
.ex-facet {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: var(--s2) var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-800); background: transparent;
  border: 1px solid var(--hairline); border-radius: var(--radius);
  cursor: pointer;
}
.ex-facet:hover { border-color: rgba(11, 23, 51, .3); background: var(--mist); }
/* 1b draws the selected filter chip as a solid accent fill carrying ink-800.
   That is 6.32:1 and it is the one construction where 1j lets #F97316 sit
   under text. */
.ex-facet.active { background: var(--accent); color: var(--ink-800); border-color: var(--accent); }

.ex-result { border-top: 1px solid var(--hairline); padding: var(--s5) 0; }
.ex-result h3 { font: 600 1.1rem/1.3 var(--font-display); margin: 0 0 var(--s1); }
.ex-result h3 a { color: var(--ink-800); text-decoration: none; }
.ex-result h3 a:hover { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
.ex-url { font: 400 12px var(--font-mono); letter-spacing: .04em; color: var(--label); word-break: break-all; }
.ex-excerpt { margin: var(--s2) 0 0; color: var(--muted); font-size: 15px; }
/* The hit itself. The old sheet marked it with a shade of the page ground,
   which on this palette is nearly invisible against white, so the mist wash is
   carried and an accent underline does the actual work: --accent-text is
   5.18 on white, well past the 3:1 a 2px graphical mark needs. */
.ex-excerpt mark {
  background: var(--mist); color: var(--ink-800); font-weight: 600;
  padding: 0 .12em; border-bottom: 2px solid var(--accent-text);
}
.ex-empty { padding: var(--s7) 0; color: var(--muted); }
.ex-empty code { font-family: var(--font-mono); font-size: .92em; }

/* ---------- Risk pills ----------
   Four levels on an ordinal scale, and the site already has a colour system
   that is NOT this one: the four flags say how mature a claim is, and a risk
   pill says how hard an advance will be. Painting risk in flag colours would
   teach a reader that blue means established in one table and low-risk in the
   next, so risk gets the one ramp the direction actually allows — neutral,
   darker neutral, accent outline, accent filled. Escalating weight, not four
   competing hues, and the word is in the markup either way.

   The old sheet defined .risk-pill twice, 1,022 lines apart, and the second
   definition (solid fill, --paper text) won on `color` while the first still
   won on `border` and on .risk-very-high's 8%-tint background — so every
   "Very High" pill on five pages shipped as white text on a pale pink ground.
   One definition here.

   .risk-veryhigh and .risk-very-high are the same level spelled two ways: the
   atlas table generator writes one, the advance and phase pages the other.
   Both ship, so both are styled, identically. */
.risk-pill {
  display: inline-block; white-space: nowrap;
  padding: 3px 8px; border-radius: var(--radius);
  border: 1px solid currentColor;
  font: 500 11px var(--font-mono); letter-spacing: .06em; text-transform: uppercase;
}
.risk-low      { color: var(--muted); border-color: var(--hairline); }
.risk-moderate { color: var(--ink-600); }
.risk-high     { color: var(--accent-text); }
.risk-very-high,
.risk-veryhigh { background: var(--accent-text); color: var(--paper); border-color: transparent; }

/* ---------- Cross-reference strips ----------
   `.xref` is an <aside> in article prose; `.xref-list` also appears on its own
   inside a .reading-list, which is already mist — so the list styles itself
   and does not lean on the wrapper for its ground. */
.xref {
  margin: var(--s6) 0 var(--s7);
  padding: var(--s4) var(--s5);
  background: var(--mist); border-radius: var(--radius);
  /* Structural, not emphatic: the accent left rule is spoken for by
     .definition-block and the accent top rule by .card and .interactive. */
  border-left: 2px solid var(--ink-800);
}
.xref-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--s2);
}
.xref-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5); }
.xref-list li { margin: 0; }
.xref-list a {
  font: 400 15px var(--font-body); color: var(--ink-800);
  text-decoration: none; padding-bottom: 1px; border-bottom: 1px solid var(--hairline);
}
.xref-list a:hover { color: var(--accent-text); border-bottom-color: var(--accent-text); text-decoration: none; }

/* A named module that is not yet linkable. The class names are deliberately
   NOT .reading-list-name / .reading-list-kind: those are the contract the
   citation scanner reads, and three placeholders that borrowed them were
   counted as real sources in the Citation Record from v0.13 to v0.14. */
.xref-pending { font-weight: 600; color: var(--ink-800); }
.xref-pending-status {
  display: inline-block; margin-left: var(--s2); vertical-align: 1px;
  padding: 2px 7px; border-radius: var(--radius);
  font: 500 11px var(--font-mono); letter-spacing: .06em;
  /* The same chip .card-status.draft uses, because it is the same statement:
     named, in progress, not shipped. */
  background: var(--flag-speculative-chip-bg); color: var(--flag-speculative-chip-fg);
}

/* ---------- Interactive toolkit: the tokens its four scripts read ----------
   An SVG presentation attribute cannot take a var(), so `interactive.v0.8.js`
   hands each widget a `cssVar()` helper and the four widgets resolve their
   colours off :root at draw time.

   They read v0.1 names: --ink, --ink-faint, --rule, --slate, --crimson,
   --paper-shade and --flag-<level>. This sheet renamed every one of them.
   getComputedStyle returns "" for a property that does not exist, an SVG
   attribute set to "" is ignored, and the initial values are stroke:none and
   fill:black — so all four diagrams would draw as black shapes with no lines,
   and nothing would say so. `_jscheck.py` only asserts that the markup and the
   script claim each other; browsercheck.mjs asserts the stage SVG has children
   and fits its viewBox, and children with no stroke are still children. That
   is the v0.5.1 failure — four diagrams dead, every gate green — one layer
   down, which is why the names are kept rather than left to the port.

   Each alias is the new token for the SAME ROLE, not the nearest hue. --rule
   is a chart axis: a graphical object on the white stage, needing 3:1, where
   the v0.1 #C9C2B0 measured 1.52 against its own ground. It could not have
   been carried across even if the name had survived. Every target here is a
   token _contrastcheck.py already asserts, so this adds no unchecked colour. */
:root {
  --ink:         var(--ink-800);       /* 17.73 on the white stage */
  --ink-faint:   var(--label);         /*  4.53 — 10px mono axis labels */
  --rule:        var(--muted);         /*  5.62 — axis, grid and edge lines */
  --slate:       var(--blue);          /*  6.70 — the measured/raised mark */
  --crimson:     var(--accent-text);   /*  5.18 — the predicted/suppressed mark */
  --paper-shade: var(--mist);
  /* The convergence map colours each edge by flag, and an edge is a line, so
     these take the `-mark` value rather than the chip. */
  --flag-established: var(--flag-established-mark);
  --flag-frontier:    var(--flag-frontier-mark);
  --flag-speculative: var(--flag-speculative-mark);
  --flag-handwave:    var(--flag-handwave-mark);

  /* Two more, found by a gate rather than by reading: `--gold` is referenced
     from inline styles on 12 shipped pages and `--ink-soft` on one. Neither is
     in any batch's class list, because neither is a class -- they are colours
     written into `style="..."` attributes in authored markup, which no CSS
     audit and no class check can see. Undeclared, they are invalid
     declarations: the browser drops them and the element inherits, silently.
     `--gold` was the decorative brand value and `--gold-text` the readable one;
     the instrument palette's decorative accent is orange, and the one place
     `--gold` is used at text size takes the readable substitution for the same
     reason 1j makes it for orange. */
  --gold:      var(--accent);          /*  6.32 on ink-800, decorative on light */
  --ink-soft:  var(--ink-600);         /* 12.48 on white — body-weight ink */
}

/* ---------- Interactives ----------
   The four bespoke diagrams. The accent top rule is .card's, because this is
   the other thing on the page a reader is meant to act on. */
.interactive {
  margin: var(--s8) 0;
  padding: var(--s5) var(--s6) var(--s5);
  background: var(--mist);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius);
}
.interactive-label {
  display: flex; align-items: baseline; gap: .6em; margin-bottom: var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.interactive-kicker { color: var(--muted); }
.interactive-title {
  margin: 0 0 var(--s3);
  font: 600 1.12rem/1.3 var(--font-display); color: var(--ink-800);
}

/* 1h draws the convergence map on the deepest ground with cyan strokes. It is
   not drawn that way here, and the reason is not taste: all four widgets
   resolve their colours from the tokens above at draw time and every one of
   them assumes a light stage — perturbation-predictor strokes a --paper halo
   around each node to separate it from the lines behind. A dark stage would
   need the four scripts rewritten, and this port does not touch scripts. The
   stage stays --paper; the instrument frame around it carries the dark.

   NOTHING HERE MAY HIDE OR COLLAPSE THE STAGE. `fitStage()` measures
   `svg.getBBox()` to re-fit the viewBox around what was actually drawn, and a
   display:none or zero-height subtree measures 0 — which makes the widget skip
   the re-fit AND makes browsercheck's own bbox assertion read as "draws
   outside its own viewBox". No display, height, max-height, overflow or
   content-visibility on .interactive-stage or any ancestor of it. */
.interactive-stage {
  background: var(--paper);
  border: 1px solid var(--hairline);
  padding: var(--s4); margin: 0 0 var(--s4);
}
.interactive-stage svg { display: block; width: 100%; height: auto; }

.interactive-controls {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--s5) var(--s6); margin: 0 0 var(--s4);
}
.interactive-toggles { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* Sliders. The 1px hairline track the old sheet used is a decorative rule;
   this one is a control affordance, so it takes --muted (5.20 on mist, past
   the 3:1 a graphical object needs) and the thumb takes --accent-text rather
   than --accent, which is 2.70 on mist and would be the one control on the
   page a reader cannot see the position of. */
.interactive input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 2px; margin: var(--s2) 0;
  background: var(--muted); cursor: pointer;
}
.interactive input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px; border: 0; border-radius: var(--radius);
  background: var(--accent-text); cursor: pointer;
}
.interactive input[type="range"]::-moz-range-thumb {
  width: 14px; height: 14px; border: 0; border-radius: var(--radius);
  background: var(--accent-text); cursor: pointer;
}
.interactive input[type="range"]:focus-visible { outline: 2px solid var(--focus-light); outline-offset: 4px; }

.interactive-btn {
  padding: .55em 1em; border-radius: var(--radius);
  font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-800); background: var(--paper);
  border: 1px solid var(--ink-800); cursor: pointer;
}
@media (prefers-reduced-motion: no-preference) {
  .interactive-btn { transition: background .12s, color .12s; }
}
.interactive-btn:hover { background: var(--ink-800); color: var(--paper); }
/* Pressed is the accent fill, the same construction as a selected filter chip. */
.interactive-btn[aria-pressed="true"] { background: var(--accent); color: var(--ink-800); border-color: var(--accent); }
.interactive-btn:disabled { opacity: .45; cursor: default; }
.interactive-btn:disabled:hover { background: var(--paper); color: var(--ink-800); }

.interactive-readout {
  display: flex; flex-wrap: wrap; gap: var(--s1) var(--s6);
  margin-top: var(--s2); padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  font: 400 13px var(--font-mono);
}
.interactive-caption {
  margin: var(--s4) 0 0; padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  font: 400 15px/1.7 var(--font-body); color: var(--ink-600);
}

/* The no-JS fallback is visible until the toolkit marks the box .is-live, and
   browsercheck fails the build if a live box still shows it. Specificity here
   is 0,3,0 against the plain 0,1,0 below it, so source order is not what is
   holding it up — but nothing appended after this may set `display` on
   .interactive-fallback at 0,3,0 or higher. */
.interactive.is-live .interactive-fallback { display: none; }
.interactive-fallback { font: 400 15px/1.7 var(--font-body); color: var(--ink-600); }
.interactive-fallback-note {
  display: block; margin-top: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}

/* Not in the markup: `interactive.v0.8.js` creates this aria-live region and
   appends it to the box if it is absent, which it always is. Without the clip
   every state change a widget announces is also painted into the panel. */
.interactive-status {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .interactive * { transition: none !important; animation: none !important; }
}

/* ---------- Learning paths ---------- */
.path-block {
  position: relative;                       /* the old rule's positioning context */
  margin-bottom: var(--s5); padding: var(--s6);
  background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
}
.path-header {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--s5); margin-bottom: var(--s3);
}
.path-title { margin: 0; font: 600 1.4rem/1.2 var(--font-display); letter-spacing: -.01em; color: var(--ink-800); }
.path-duration {
  white-space: nowrap;
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--label);
}
/* The goal line is italic in the old sheet and set in the display face. Space
   Grotesk has no italic in the loaded subset, so that asks the browser to
   slant the glyphs; the body face does load a true italic (the @import asks
   for ital 1,400), so the italic comes from there. */
.path-goal {
  margin: 0 0 var(--s5);
  font: italic 400 1.05rem/1.6 var(--font-body); color: var(--muted);
}

.path-sequence { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }
.path-step {
  display: grid; grid-template-columns: 56px 1fr auto;
  gap: var(--s4); align-items: center;
  padding: var(--s3) 0; color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--hairline-soft);
}
/* The old selector was `.path-step:last-child`, and every .path-step is the
   only child of its own <li> — so it matched all of them and the separators
   never drew at all. Scoped to the last row of the list, which is what it was
   written to mean. */
.path-sequence li:last-child .path-step { border-bottom: 0; }
.path-step:hover { text-decoration: none; padding-left: var(--s2); }
@media (prefers-reduced-motion: no-preference) {
  .path-step { transition: padding-left .2s; }
}
/* A step pointing at content that does not exist yet. The old rule dropped the
   whole row to opacity .5, which takes the title to about 4.4:1 — below what
   the text needs, to say something the cursor and the colour already say. */
.path-step[href="#"] { cursor: not-allowed; }
.path-step[href="#"] .path-step-title,
.path-step[href="#"] .path-step-num { color: var(--muted); }
.path-step[href="#"]:hover { padding-left: 0; }

.path-step-num { font: 500 14px var(--font-mono); letter-spacing: .05em; color: var(--accent-text); font-variant-numeric: tabular-nums; }
.path-step-title { font: 600 1.02rem/1.35 var(--font-display); color: var(--ink-800); }
.path-step-sub { font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--label); }

/* ---------- Hero meta rail ----------
   This IS the hero rail already ported above as .hero-rail / -k / -v — the
   "Coordinates / Current focus / Working language" row under the lede. The
   shipped markup on index.html spells it .hero-meta, so the shipped spelling
   gets the rule; the two are one component and a change to either belongs in
   both. The old sheet had this rail on a light ground, but .hero is ink-900
   here, so it draws the dark-ground values. */
.hero-meta {
  display: flex; flex-wrap: wrap; gap: var(--s7);
  margin-top: var(--s6); padding-top: var(--s5);
  border-top: 1px solid rgba(255, 255, 255, .12);
}
.hero-meta-item { display: flex; flex-direction: column; gap: var(--s1); }
.hero-meta-label {
  font: 500 11px var(--font-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--label-dk);
}
/* #E8EDF7 is not a token and is copied verbatim from .hero-rail-v rather than
   substituted: the two spellings are one rail, and a value that drifts between
   them is the component breaking in half. If it becomes a token, both move. */
.hero-meta-value { font: 500 15px var(--font-body); color: #E8EDF7; }

/* ---------- Definition block ----------
   The numbered-criteria callout (the AIHS definition and its like). This one
   keeps the accent as a left rule: it is the emphatic block of the three mist
   panels in this batch, which is what the old sheet's crimson rule said. */
.definition-block {
  margin: var(--s7) 0; padding: var(--s5) var(--s6);
  background: var(--mist); border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.definition-label {
  margin-bottom: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.definition-term { margin-bottom: var(--s3); font: 600 1.3rem/1.25 var(--font-display); letter-spacing: -.01em; color: var(--ink-800); }
.definition-criteria { list-style: none; padding: 0; margin: var(--s4) 0 0; counter-reset: criteria; }
.definition-criteria li {
  counter-increment: criteria; position: relative;
  padding-left: var(--s7); margin-bottom: var(--s3);
  font-size: 1.02rem; line-height: 1.6;
}
/* The old badge was a circle. 1j allows 2–3px and nothing rounder — "this is
   an instrument panel, not a consumer app" — so it is a square plate. */
.definition-criteria li::before {
  content: counter(criteria);
  position: absolute; left: 0; top: 2px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  background: var(--ink-800); color: var(--paper);
  font: 500 12px var(--font-mono); border-radius: var(--radius);
}

/* ---------- Batch 3 responsive ----------
   Placed with the rules it modifies rather than in the site-wide Responsive
   section below, which is the composition breakpoints. Both old breakpoints
   for this batch (620 and 760) are folded into the file's own 760. */
@media (max-width: 760px) {
  .ex-wrap { padding-block: var(--s6) var(--s7); }
  .interactive { padding: var(--s4); }
  .interactive-controls { gap: var(--s4); }
  .path-block { padding: var(--s5) var(--s4); }
  .path-step { grid-template-columns: 40px 1fr; gap: var(--s3); }
  .path-step-sub { grid-column: 2; margin-top: var(--s1); }
  .hero-meta { gap: var(--s5); }
  .definition-block { padding: var(--s4) var(--s5); }
  .definition-criteria li { padding-left: var(--s6); }
}

/* ============================================================
   BATCH 2 — review, advance, deps, gate, control, lb, milestone,
   flag-history, topic, sort, audience, study, timeline, mh.

   ONE MEASUREMENT GOVERNS MOST OF WHAT FOLLOWS
   --------------------------------------------
   --label (#6D768F) is 4.53:1 on --paper and 4.19:1 on --mist. It is declared
   for the white ground and PAIRS only asserts it there, so an 11px uppercase
   mono label drawn in it on the mist ground is below AA and nothing catches it.
   Half the components in this batch either sit on mist (the advance summary
   grid, the deps block, the interactive controls) or turn mist on hover
   (every card here, following .branch-item). So every small light-ground label
   below draws --muted (#5B6880) instead: 5.62 on paper, 5.20 on mist, both
   already asserted, one value for a component that moves between grounds.
   --label survives where a rule inherits it from a parent this batch does not
   own, and --label-dk is unaffected — it is a dark-ground token.
   ============================================================ */

/* ---------- Review state (article meta) ----------
   .review-state is one span in .article-meta, and the only one there that is a
   disclosure rather than a fact about the file. The old sheet marked it with
   `font-style: italic` and wrote down why: "not independently reviewed" is the
   ordinary condition of this corpus, not an alarm, so it carries the same
   weight as the licence beside it and no warning colour. That reasoning is the
   class and it is kept — including the colour, which is inherited from
   .article-meta and therefore correct on whatever ground that row lands on.

   The mechanism changes. No mono face here is loaded with an italic — the
   @import asks for IBM Plex Mono 400 and 500 and nothing else, exactly as the
   old sheet asked for JetBrains Mono 400/500/600 — so `font-style: italic` has
   always been a synthesised oblique applied to 11px uppercase mono at .1em
   tracking, which is the least legible thing on the page. IBM Plex Sans IS
   loaded with a real italic (the `1,400` axis). So the disclosure steps out of
   the mono run into body italic at the same colour and the same optical size:
   still marked, still quiet, no longer slanted by the rasteriser. */
.review-state {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 12px;
  letter-spacing: normal;
  text-transform: none;
}

/* v1.0.0: the overdue state. "Review due" is the one review disclosure that IS
   an alarm — the policy (option a, decided 14 September 2026) chose visible
   overdue over a clock that quietly runs out. It borrows the accent-ink pair
   already declared for action text on light grounds; `_contrastcheck.py`
   carries the pair, so the alarm cannot ship below AA the way the flag marks
   once did. */
.review-state-due {
  font-style: normal;
  font-weight: 600;
  color: var(--accent-text);
}

/* ---------- Advance pages (list, item, header, summary) ---------- */
.advance-list { display: flex; flex-direction: column; gap: var(--s4); margin: var(--s6) 0; }

/* 90px / 1fr / auto is the row: id, body, risk. Kept — the id column is sized
   to "C1"-style codes and the risk column to a pill plus a horizon. */
.advance-item {
  display: grid; grid-template-columns: 90px 1fr auto;
  gap: var(--s5); align-items: start;
  padding: var(--s5); background: var(--paper);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  text-decoration: none; color: inherit;
}
.advance-item:hover { text-decoration: none; border-color: var(--accent); background: var(--mist); }
.advance-item-id {
  font: 500 13px var(--font-mono); letter-spacing: .1em;
  color: var(--accent-text); padding-top: 2px;
}
.advance-item-body { display: flex; flex-direction: column; gap: var(--s1); }
.advance-item-title { font: 600 16px/1.3 var(--font-display); color: var(--ink-800); }
.advance-item-summary { font-size: 14px; line-height: 1.6; color: var(--muted); }
.advance-item-risk {
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s1);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
}

/* The advance/phase detail header. Same job as .article-header, so the same
   2px ink rule the article body already uses to open a section. */
.advance-header {
  margin-bottom: var(--s7); padding-bottom: var(--s6);
  border-bottom: 2px solid var(--ink-800);
}
.advance-breadcrumb {
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--s4);
}
/* The crumbs are links but this is a micro-label row, so they stay the row's
   colour and announce themselves on hover rather than sitting in accent. */
.advance-breadcrumb a { color: var(--muted); text-decoration: none; }
.advance-breadcrumb a:hover { color: var(--accent-text); }
.advance-breadcrumb span:not(:last-child)::after { content: "/"; margin: 0 .5em; }

/* auto-fit rather than a fixed count: the header carries two to four summary
   blocks depending on the page, and 180px is the width at which a label and a
   value stop wrapping. Both kept from the old rule. */
.advance-summary-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s5); margin: var(--s6) 0 0;
  padding: var(--s5); background: var(--mist);
  border: 1px solid var(--hairline); border-radius: var(--radius);
}
.advance-summary-block { display: flex; flex-direction: column; gap: var(--s1); }
.advance-summary-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted);
}
.advance-summary-value { font: 500 17px/1.3 var(--font-display); color: var(--ink-800); }

/* .dir-up / .dir-down are NOT redefined here. They only ever modify
   .assessment-dir, and the Assessment box section above already carries both,
   on both grounds, with the reasoning for the cyan/orange pair. A second
   declaration of the same two classes further down the cascade is how a colour
   quietly stops matching the comment that explains it. */

/* ---------- Dependency map ----------
   The dashed border is the point of the component: a deps block is derived from
   the advance graph, not authored, and dashed is how the corpus marks
   provisional structure (.signpost carries the same note). A dashed rule at
   --hairline's .12 alpha is not visible enough to read as a container, so the
   panel takes the mist ground and the chips invert to white — the enclosure
   comes from the fill, the dashes from the border. */
.deps-block {
  margin: var(--s7) 0; padding: var(--s5);
  background: var(--mist);
  border: 1px dashed var(--hairline); border-radius: var(--radius);
}
.deps-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--s3);
}
.deps-list { display: flex; flex-wrap: wrap; gap: var(--s2); }
.deps-list a {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 6px 10px; background: var(--paper);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  font: 400 12px var(--font-mono); color: var(--ink-800); text-decoration: none;
}
.deps-list a:hover { border-color: var(--accent); color: var(--accent-text); text-decoration: none; }
.deps-list a::before { content: "\2192"; color: var(--muted); }

/* ---------- Decision gate ----------
   1m, screen 1f: "bottlenecks in a dark block". The gate is the study's other
   dark block and the old sheet already built it that way (ink ground, paper
   text, 4px accent edge), so this is the one component in the batch that ports
   ground and all.

   It is dark WITHOUT carrying .on-dark, so the dark-ground rules at the top of
   this file do not reach it: link colour and focus ring are restated here or a
   link inside a gate draws --accent-text at 2.4:1 on ink-800 and the focus ring
   draws --focus-light at 1.8:1. One shipped gate block contains a link. */
.gate-block {
  position: relative; margin: var(--s7) 0; padding: var(--s6) var(--s7);
  background: var(--ink-800); color: #fff; border-radius: var(--radius);
}
.gate-block::before {
  content: ""; position: absolute; top: 0; left: 0; bottom: 0;
  width: 4px; background: var(--accent);
}
.gate-block a { color: var(--accent); }
.gate-block :focus-visible { outline-color: var(--focus-dark); }
.gate-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s2);
}
.gate-title { font: 600 22px/1.2 var(--font-display); color: #fff; margin-bottom: var(--s3); }
/* The criterion is the question the whole gate exists to ask, so it is the
   brightest thing in the block. Italic in --font-body, not --font-display:
   Space Grotesk is imported without an italic axis, so display italic here
   would be a synthesised slant on the block's largest text. */
.gate-criterion {
  font: italic 400 17px/1.55 var(--font-body); color: #fff;
  margin: var(--s4) 0; padding: var(--s3) 0 var(--s3) var(--s4);
  border-left: 2px solid var(--accent);
}
.gate-block p { font-size: 15px; line-height: 1.7; color: var(--muted-dk); margin: var(--s3) 0; }
.gate-options { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); margin-top: var(--s5); }
/* --ink-700 rather than a white overlay: the overlay the old sheet used gives a
   ground no token names, and a ground no token names is a ground
   _contrastcheck.py cannot measure the option label against. */
.gate-option {
  padding: var(--s4); background: var(--ink-700);
  border: 1px solid var(--hairline-dark); border-radius: var(--radius);
}
/* The old sheet drew this label in --gold-text, a colour chosen for its
   contrast on the LIGHT ground and used here on ink: it measured about 2.2:1.
   --accent is 5.49 on ink-700 and is what an instrument panel marks a branch
   with anyway. */
.gate-option-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s2);
}
.gate-option p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--muted-dk); }

/* ---------- Interactive controls ---------- */
.control { display: flex; flex-direction: column; gap: var(--s1); min-width: 8rem; }
/* justify-content: space-between is what puts the live readout at the right
   edge of its own label; the label and the value are one line in the markup. */
.control-label {
  display: flex; justify-content: space-between; gap: var(--s3);
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
/* tabular-nums so a slider readout does not shift its neighbours as it counts. */
.control-label .control-value { color: var(--ink-800); font-variant-numeric: tabular-nums; }
.on-dark .control-label, .on-deep .control-label { color: var(--label-dk); }
.on-dark .control-label .control-value, .on-deep .control-label .control-value { color: #fff; }

/* ---------- Layered brief: key-value list ----------
   A <dl>. The auto/1fr grid is what aligns every value to one column no matter
   how long the terms are, and it is the class. */
.lb-kv {
  display: grid; grid-template-columns: auto 1fr;
  gap: var(--s3) var(--s5); margin: var(--s5) 0 0;
  align-items: baseline; font-size: 15px;
}
.lb-kv dt {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted);
}
.lb-kv dd { margin: 0; color: var(--ink-600); }
.on-dark .lb-kv dt, .on-deep .lb-kv dt { color: var(--label-dk); }
.on-dark .lb-kv dd, .on-deep .lb-kv dd { color: var(--muted-dk); }

/* ---------- Milestone markers ----------
   Modifiers on the .milestones marker above, so only the three properties that
   separate a target from a stretch are set here; the marker's geometry stays
   where it is defined and these rules survive a change to it.

   A target is filled brighter than its row and outlined solid; a stretch is
   flush with its row and outlined dashed. Fill, border-style and hue all differ,
   so no reading of this list depends on colour alone.

   --accent is NOT the target's outline on a light ground. The marker is a 2px
   border — a graphical object at 3:1 — and #F97316 is 2.80 on the white it is
   filled with: the same measurement that moved the focus ring to --accent-text,
   which is 5.18. On ink the accent is 6.32 and is used. */
.milestones li.milestone-target::before {
  border-color: var(--accent-text); background: var(--paper);
}
.milestones li.milestone-stretch::before {
  border-color: var(--muted); background: var(--mist); border-style: dashed;
}
.on-dark .milestones li.milestone-target::before,
.on-deep .milestones li.milestone-target::before {
  border-color: var(--accent); background: var(--ink-800);
}
.on-dark .milestones li.milestone-stretch::before,
.on-deep .milestones li.milestone-stretch::before {
  border-color: var(--label-dk); background: var(--ink-900); border-style: dashed;
}

/* ---------- Flag history ----------
   "Speculative → Frontier · 2024" beside an inline flag: the claim's flag has
   moved, and the title attribute carries why, which is why cursor:help is part
   of the class rather than decoration.

   Outlined, not filled, so it never reads as a fifth flag chip next to the real
   one it annotates. Shipped at 11px rather than the old rule's ~10.2px, for the
   same reason the tokens fix the artboard's 9px labels: this sits on the same
   baseline as .flag and would otherwise be the smallest type in the corpus.
   It does not touch .flag-established and friends — the selector matches the
   class `flag-history`, not `flag`. The arrow inside it is already styled above
   and is deliberately not restated here. */
.flag-history {
  display: inline-block;
  font: 500 11px var(--font-mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--hairline); border-radius: var(--radius);
  padding: 1px 5px; margin-left: var(--s1);
  white-space: nowrap; cursor: help; vertical-align: baseline;
}
.on-dark .flag-history, .on-deep .flag-history, .hero .flag-history {
  color: var(--label-dk); border-color: var(--hairline-mid);
}

/* ---------- Topic / module card grid ----------
   Not the 318-slot topic map from 1b — that is .ip-topicmap and its 18/9/6
   ladder. This is the learn page's module grid, and auto-fit at 280px is what
   lets one category render three cards and the next render seven without a
   per-category column count. Kept. */
.topic-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--s4); margin-top: var(--s6);
}
.topic-card {
  display: block; padding: var(--s5); background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  text-decoration: none; color: inherit;
}
.topic-card:hover { text-decoration: none; border-color: var(--accent); background: var(--mist); }
.topic-card-id {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s2);
}
.topic-card-title { font: 600 17px/1.3 var(--font-display); color: var(--ink-800); margin-bottom: var(--s1); }
.topic-card-sub { font-size: 14px; line-height: 1.55; color: var(--muted); }

/* ---------- Atlas sort indicator ----------
   One glyph, "⇅", which the sort script never rewrites — it only moves a
   `sorted` class between headers. So this rule is the entire indication that a
   column is the sorted one, and it has to carry that on two channels.

   The old rule dimmed the resting state with opacity .4. That was a value for a
   light header; the header is now ink-800, where .4 of #C8D4EA lands near 2.5:1
   and the indicator disappears from the column a reader is trying to find. A
   token replaces it — --label-dk is 8.08 there and still a visible step below
   the header's own #C8D4EA — and the sorted state adds a 2px underline in the
   accent so the state is not read from hue alone. */
.atlas th .sort-ind { font-size: .85em; color: var(--label-dk); }
.atlas th.sorted .sort-ind { color: var(--accent); border-bottom: 2px solid var(--accent); }

/* ---------- Audience cards ---------- */
.audience-block { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s4); margin: var(--s6) 0; }
.audience-card {
  display: block; padding: var(--s5); background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  text-decoration: none; color: inherit;
}
.audience-card:hover { text-decoration: none; border-color: var(--accent); background: var(--mist); }
.audience-card-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s2);
}
.audience-card-title { font: 600 16px/1.25 var(--font-display); color: var(--ink-800); margin-bottom: var(--s1); }
.audience-card-desc { font-size: 14px; line-height: 1.55; color: var(--muted); }

/* ---------- Study hub overview ---------- */
.study-overview { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s4); margin: var(--s7) 0; }
/* flex column + `flex: 1` on the description is what holds the meta line to the
   bottom of every block regardless of how long the descriptions run. Kept.
   The old rule's 3px top bar was an absolutely positioned ::before; it is a
   border-top here, which is the same 2–3px bar and the same construction the
   ported .card already uses. The hover lift (translateY(-2px)) is dropped —
   nothing else in this language lifts, and it was the only motion in the
   component that prefers-reduced-motion would have had to answer for. */
.study-overview-block {
  display: flex; flex-direction: column;
  padding: var(--s5) var(--s6); background: var(--paper);
  border: 1px solid var(--hairline); border-top: 2px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  text-decoration: none; color: inherit;
}
.study-overview-block:hover { text-decoration: none; border-color: var(--accent); background: var(--mist); }
.study-overview-id {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s3);
}
.study-overview-title { font: 600 19px/1.2 var(--font-display); color: var(--ink-800); margin-bottom: var(--s2); }
.study-overview-desc { flex: 1; font-size: 14px; line-height: 1.6; color: var(--muted); margin: 0 0 var(--s4); }
.study-overview-meta {
  padding-top: var(--s3); border-top: 1px solid var(--hairline-soft);
  font: 500 11px var(--font-mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}

/* ---------- Horizon timeline wrapper ----------
   The scroller and the 680px floor are the component: the SVG is authored at
   680px and squeezing it below that collapses the year ticks onto each other,
   so it scrolls instead. Both kept.
   Deliberately no dark-ground variant: the tl-* classes inside the SVG are
   tuned for a light ground and are not this batch's to move, so the panel stays
   a light instrument face wherever the section around it goes. */
.timeline-wrap {
  margin: var(--s5) 0 0; overflow-x: auto;
  background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius);
}
.timeline-wrap svg { display: block; min-width: 680px; width: 100%; height: auto; }

/* ---------- Module horizon separator ----------
   The "·" between "Last reviewed" and "Horizon". Deliberately below text
   contrast: the flex gap does the separating and the dot carries no
   information, which is the role --rule played for it in the old sheet. */
.module-horizon .mh-sep { color: var(--hairline); }
.on-dark .module-horizon .mh-sep, .on-deep .module-horizon .mh-sep { color: var(--hairline-dark); }

/* ---------- Batch 2: dark-ground card variants ----------
   Rule: any of these can be dropped into a dark closing section by a template
   that adds .on-dark. The cards are the only pieces that carry their own light
   ground and so the only ones that need restating. */
.on-dark .advance-item, .on-deep .advance-item,
.on-dark .topic-card, .on-deep .topic-card,
.on-dark .audience-card, .on-deep .audience-card,
.on-dark .study-overview-block, .on-deep .study-overview-block {
  background: var(--ink-700); border-color: var(--hairline-dark); box-shadow: none;
}
.on-dark .advance-item:hover, .on-deep .advance-item:hover,
.on-dark .topic-card:hover, .on-deep .topic-card:hover,
.on-dark .audience-card:hover, .on-deep .audience-card:hover,
.on-dark .study-overview-block:hover, .on-deep .study-overview-block:hover {
  background: var(--ink-700); border-color: var(--accent);
}
.on-dark .advance-item-title, .on-deep .advance-item-title,
.on-dark .topic-card-title, .on-deep .topic-card-title,
.on-dark .audience-card-title, .on-deep .audience-card-title,
.on-dark .study-overview-title, .on-deep .study-overview-title,
.on-dark .advance-summary-value, .on-deep .advance-summary-value { color: #fff; }
.on-dark .advance-item-summary, .on-deep .advance-item-summary,
.on-dark .topic-card-sub, .on-deep .topic-card-sub,
.on-dark .audience-card-desc, .on-deep .audience-card-desc,
.on-dark .study-overview-desc, .on-deep .study-overview-desc { color: var(--muted-dk); }
.on-dark .advance-item-id, .on-deep .advance-item-id,
.on-dark .topic-card-id, .on-deep .topic-card-id,
.on-dark .audience-card-label, .on-deep .audience-card-label,
.on-dark .study-overview-id, .on-deep .study-overview-id { color: var(--accent); }
.on-dark .advance-item-risk, .on-deep .advance-item-risk,
.on-dark .advance-breadcrumb, .on-deep .advance-breadcrumb,
.on-dark .advance-summary-label, .on-deep .advance-summary-label,
.on-dark .deps-label, .on-deep .deps-label,
.on-dark .study-overview-meta, .on-deep .study-overview-meta { color: var(--label-dk); }
.on-dark .advance-breadcrumb a, .on-deep .advance-breadcrumb a { color: var(--label-dk); }
.on-dark .advance-breadcrumb a:hover, .on-deep .advance-breadcrumb a:hover { color: var(--accent); }
.on-dark .advance-header, .on-deep .advance-header { border-bottom-color: var(--hairline-mid); }
.on-dark .study-overview-meta, .on-deep .study-overview-meta { border-top-color: var(--hairline-dark); }
.on-dark .advance-summary-grid, .on-deep .advance-summary-grid,
.on-dark .deps-block, .on-deep .deps-block {
  background: var(--ink-700); border-color: var(--hairline-dark);
}
.on-dark .deps-list a, .on-deep .deps-list a {
  background: var(--ink-800); border-color: var(--hairline-dark); color: #fff;
}
.on-dark .deps-list a::before, .on-deep .deps-list a::before { color: var(--label-dk); }
.on-dark .deps-list a:hover, .on-deep .deps-list a:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- Batch 2: breakpoints ----------
   These live with their components rather than in the Responsive section below,
   because that section is not this batch's to rewrite while four agents are
   appending to one file. They touch only batch-2 selectors, so nothing later in
   the cascade contends with them.

   The old sheet collapsed all of these at 620px. The ladder here is 1m's —
   1100 / 900 / 760, "programme grids 3 → 2 → 1" — and these are programme
   grids; the exception is .lb-kv, whose 400px rule is a sub-ladder fallback for
   a two-column definition list and is carried at its own value. */
@media (max-width: 1100px) {
  .audience-block, .study-overview { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 760px) {
  .audience-block, .study-overview, .gate-options { grid-template-columns: 1fr; }
  .advance-item { grid-template-columns: 1fr; gap: var(--s2); }
  .advance-item-risk { flex-direction: row; align-items: center; gap: var(--s3); }
  .gate-block { padding: var(--s5); }
  .control { min-width: 100%; }
}
@media (max-width: 400px) {
  .lb-kv { grid-template-columns: 1fr; gap: var(--s1) 0; }
  .lb-kv dd { margin-bottom: var(--s3); }
}

/* ---------- Batch 2: print ----------
   .gate-block is dark but does not carry .on-dark, so the print block at the
   foot of this file does not reach it, and a browser that drops backgrounds in
   print would render white text on white paper — the gate, which is the part of
   a study a funder prints. The same for the timeline's horizontal scroller,
   which has no meaning on paper. Placed here rather than in that block for the
   same append-only reason as the breakpoints above. */
@media print {
  .gate-block { background: #fff !important; color: #000 !important; border: 1px solid #000; }
  .gate-block::before { background: #000 !important; }
  .gate-title, .gate-criterion, .gate-block p, .gate-option p, .gate-label, .gate-option-label {
    color: #000 !important;
  }
  .gate-option { background: #fff !important; border-color: #000; }
  .gate-criterion { border-left-color: #000; }
  .timeline-wrap { overflow: visible; }
  .timeline-wrap svg { min-width: 0; }
}

/* ---------- Topic map (artboard 1b) ----------
   "1b Topic map — 318 cells is the whole point; render categories lazily,
   keep the filter state in the URL so a filtered view is linkable." (1m)

   The cells, the category blocks and the category chips are all ADDED by
   `topic-map.v0.9.js`, built from the rows of `#topic-index-table`; the table
   itself keeps every rule it already has above and is not touched. A page
   where that script does not run is the page it was before — the controls and
   the 408-row table — rather than a page with an empty grid in it.

   There are 408 cells in eleven categories, not the artboard's 318 in ten:
   1b was drawn before Category XI. No count is written down anywhere in this
   family; the map is as long as the table is.

   WHAT THE COLOUR MEANS HERE
   --------------------------
   1m: "Flags are data, not decoration… one source of truth, one color map."
   1b filters by evidence flag, but the topic index does not record a flag per
   topic — it records a publication status, and that status is already in the
   markup as `.card-status live|draft|planned` on every row. So the map takes
   the class off the row and this block maps it to the flag tokens exactly as
   `.card-status` is mapped above: live → established, draft → speculative,
   planned → handwave. The map and the table's chips therefore cannot drift,
   because they are the same three colours resolved from the same tokens.

   The five statuses the page filters on (live, programme-covered,
   cross-listed, ongoing, planned) share those three colours, so colour is
   NEVER the whole signal: every cell also carries a shape, a `title`, and the
   status word in its accessible name, and the key at the top of the map pairs
   each shape with its word. 1m: "Flags must never be color-only — always the
   word." */
/* The map is wider than the prose it sits in. `.article-body` is 72ch — the
   measure for reading, and 733px of it at 1440, where eighteen columns is a
   37px cell and not a map of anything. The script widens it to the container's
   content box.

   There is deliberately no `max-width` here. The first version of this rule
   carried `max-width: 100%` as a harmless-looking safety net, and it capped
   the map straight back to the reading measure, because 100% is 100% of the
   733px parent — the map rendered at 37px cells and every measurement of it
   was correct. A block element is already its parent's width, so the
   script-less fallback needs no rule at all. */
.ip-topicmap { margin: var(--s6) 0 var(--s7); }

.ip-topicmap-head {
  padding-bottom: var(--s3);
  border-bottom: 2px solid var(--ink-800);
}
.ip-topicmap-title {
  margin: 0; font: 600 18px var(--font-display); color: var(--ink-800);
}
.ip-topicmap-meta {
  margin: var(--s1) 0 0;
  font: 400 11px var(--font-mono); letter-spacing: .06em; color: var(--label);
}

/* The key. This is where each status's WORD lives in plain sight, against the
   same shape and the same colour its cells carry. */
.ip-topicmap-key {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  font: 500 11px var(--font-mono); letter-spacing: .04em; color: var(--ink-600);
}
.ip-topicmap-key-item { margin: 0; display: flex; align-items: baseline; gap: var(--s1); }
.ip-topicmap-key-item b { font-weight: 500; color: var(--label); }

.ip-topicmap-readout {
  margin: var(--s3) 0 0;
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-text);
}

/* The category chips. They are `.atlas-filter`, the page's own chip, so they
   are the same control as the status chips and carry the same 44px target from
   the v0.2.1 accessibility pass. Two things about them are not the same, and
   both are consequences of the content rather than preferences:

   They are not uppercased. `.atlas-filter` sets uppercase and .08em tracking,
   which is right for a four-word status label and wrong for eleven proper
   names of up to 38 characters: measured, "III · BIOLOGY & HUMAN ENHANCEMENT
   57" is 296px against 238px for the same words set as written, and the row
   went from seven wrapped lines to three at 1440.

   Below 760px they stop wrapping and scroll sideways instead. Eleven category
   names on a 342px viewport is twelve stacked rows and 560px of controls — the
   map, which is the screen, started below the fold and the chips were the page.
   One horizontal rail keeps every name readable and the map on the screen. It
   is the chips that scroll, not the page. */
.ip-topicmap-catbar .atlas-filter {
  text-transform: none; letter-spacing: .01em;
  font-family: var(--font-body); font-size: 13px;
}
@media (max-width: 760px) {
  .ip-topicmap-catbar {
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    /* Room for the focus ring of a chip at either end, which `overflow` would
       otherwise clip to the rail's edge. */
    padding: 3px; margin-inline: -3px;
  }
  .ip-topicmap-catbar .atlas-filter { flex: none; }
  .ip-topicmap-catbar .atlas-controls-label { flex: none; }
}

/* The status → colour map, scoped to the map so the bare status class names
   never leak into the rest of the sheet. One custom property carries it to the
   cell's edge, the cell's shape mark and the key's swatch at once, which is
   what makes "one color map" true rather than merely intended. */
.ip-topicmap .live    { --ip-mark: var(--flag-established-mark); }
.ip-topicmap .draft   { --ip-mark: var(--flag-speculative-mark); }
.ip-topicmap .planned { --ip-mark: var(--flag-handwave-mark); }

.ip-topicmap-cat { margin-top: var(--s5); }
.ip-topicmap-cat-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2);
  margin: 0 0 var(--s2);
  font: 600 15px var(--font-display); color: var(--ink-800);
}
.ip-topicmap-cat-count {
  font: 400 11px var(--font-mono); letter-spacing: .06em; color: var(--label);
}
/* Only present while a filter is on, and in the accent so the live number is
   not mistaken for the coverage figure beside it. */
.ip-topicmap-cat-shown {
  font: 500 11px var(--font-mono); letter-spacing: .06em; color: var(--accent-text);
}

/* 1m: "Topic map 18 → 9 → 6 columns." The ladder is at the foot of this block
   rather than in the Responsive section because the whole family is new and
   reads better in one piece; the site's own breakpoints are used. */
.ip-topicmap-grid {
  display: grid; grid-template-columns: repeat(18, minmax(0, 1fr));
  gap: var(--s1);
  list-style: none; margin: 0; padding: 0;
}
.ip-topicmap-slot { margin: 0; min-width: 0; }

.ip-topicmap-cell {
  position: relative; display: block; height: 100%;
  padding: 5px 6px 6px;
  background: var(--mist);
  /* A transparent hairline on three sides so a written cell and an open slot
     are the same size: the open slot draws that border and would otherwise sit
     2px smaller inside the same grid track. */
  border: 1px solid transparent;
  border-left: 3px solid var(--ip-mark, var(--muted));
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--ink-800); text-decoration: none; overflow: hidden;
}
a.ip-topicmap-cell:hover { background: var(--paper); text-decoration: none; }
a.ip-topicmap-cell:hover .ip-topicmap-cell-name { text-decoration: underline; }

/* The artboard draws an unwritten slot on a lighter ground with a dashed edge.
   That is the second, non-colour signal for the one status a reader most needs
   to pick out of a dense grid, and it costs no space in a 64px cell. */
.ip-topicmap-cell.planned {
  background: var(--paper); border-color: var(--hairline);
  border-left: 3px dashed var(--ip-mark, var(--muted));
}
.ip-topicmap-cell.planned .ip-topicmap-cell-name { color: var(--muted); }

.ip-topicmap-cell-code {
  display: block;
  /* 10px, below 1j's 11px label floor and deliberately: this is a code, not
     one of 1j's tracked uppercase labels, it is set in --muted (5.62 on white,
     5.20 on mist — small-text AA either way) rather than in the pale grey that
     rule exists to outlaw, and the table's own `.atlas-code` ships at 12px two
     screens up. The full code is in the cell's title and accessible name; what
     is shown is the code minus the prefix every code on the page shares, which
     the script measures rather than assumes. */
  font: 500 10px/1.4 var(--font-mono); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ip-topicmap-cell-name {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
  font: 400 12px/1.25 var(--font-body); color: var(--ink-800);
  /* Eighteen columns is a 49px text column, which is narrower than
     "Modification". Something has to give, and the order it gives in matters:
     hyphenate first (the page is `lang="en"`, so the browser hyphenates at
     real syllables), break inside a word only when hyphenation cannot save it,
     and clamp at three lines rather than two.

     The first version clamped at two with `overflow-wrap: anywhere`, which
     broke every long word at whatever character reached the edge — the map
     read "Artificia / l…" and "Gravity / Modifi…" down the whole first row.
     `break-word` breaks only words that would not fit on a line of their own,
     which is the difference between a hyphenated map and a shredded one. */
  hyphens: auto; -webkit-hyphens: auto; overflow-wrap: break-word;
}

/* The shape. Cells are 64px wide at eighteen columns and the status word does
   not fit in one, so the corner carries the shape and the key carries the word.
   Decorative in the accessibility sense — the word is in the cell's text — so
   the script marks it aria-hidden. */
.ip-topicmap-mark {
  position: absolute; top: 2px; right: 3px;
  font-size: 8px; line-height: 1; color: var(--ip-mark, var(--muted));
}
.ip-topicmap-key .ip-topicmap-mark {
  position: static; font-size: 10px; color: var(--ip-mark, var(--muted));
}

/* The status word, present in every cell for anything that reads the page and
   clipped for anything that looks at it. Not `.sr-only`: that class is on this
   page but neither stylesheet defines it — its appearance is an inline style in
   the template — so relying on it would be relying on a rule that is not here. */
.ip-topicmap-vh {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* The table's disclosure. A real button with `aria-expanded` and
   `aria-controls`, styled as the brief reader's toggle is, because they are the
   same kind of control: a reading preference, not a filter. */
.ip-topicmap-tabletoggle {
  appearance: none; cursor: pointer;
  margin: var(--s5) 0 0;
  padding: 8px 14px; min-height: 44px;
  border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper);
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.ip-topicmap-tabletoggle:hover { border-color: var(--muted); color: var(--ink-800); }
.ip-topicmap-tabletoggle::before { content: "\25BE"; margin-right: var(--s2); }
.ip-topicmap-tabletoggle[aria-expanded="false"]::before { content: "\25B8"; }

/* At eighteen columns a cell is 64.7px and its text column is 50px, which is
   narrower than "Artificial". The type comes down half a point and the padding
   by a pixel, which is the difference between "Artificial / Gravity" on two
   lines and "Artificia / l / Gravity" on three. Nine columns is a 102px cell
   and needs neither. This is the only place the map's type changes with the
   viewport, and it changes because the CELL changed size, not the reader. */
@media (min-width: 1101px) {
  .ip-topicmap-cell { padding: 5px 5px 6px; }
  .ip-topicmap-cell-name { font-size: 11.5px; }
}
@media (max-width: 1100px) {
  .ip-topicmap-grid { grid-template-columns: repeat(9, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .ip-topicmap-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  /* Six columns of a 342px viewport is a 53.7px cell — TIGHTER than the 64.7px
     the eighteen-column desktop ladder produces, which is not an obvious
     consequence of "18 → 9 → 6" until it is measured. So the padding comes all
     the way in and the type takes the same half-point step it takes at
     eighteen columns. At 11px it is the size of the site's own mono labels,
     and the map is the one place on the page where forty cells have to be on
     the screen at once for the screen to mean anything. */
  .ip-topicmap-cell { padding: 3px 3px 4px; }
  .ip-topicmap-cell-name { font-size: 11px; }
  .ip-topicmap-mark { top: 1px; right: 2px; }
}
@media print {
  /* The toggle is furniture; the map prints as six columns because a printed
     page is narrow, and it prints whatever the reader left open. */
  .ip-topicmap-tabletoggle { display: none; }
  .ip-topicmap-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* ---------- Learn ladder progress (artboard 1c) ----------
   1m: "1c Learn — module rows with progress in localStorage; one active row
   styled in orange, completed rows in cyan, locked rows at 68% opacity."

   Every rule below is scoped to a class `learn-progress.v0.9.js` puts on a row
   (`.ladder-row`) or to an element that script creates. None of it reaches the
   page when the script does not run: the ladder is then 25 `.path-step` links
   laid out by the `Learning paths` rules above, exactly as it is today. Added,
   never substituted — the v0.3 lesson, where four interactive diagrams became
   empty boxes and every check passed.

   THE 68% IS NOT SHIPPED, AND THIS IS THE MEASUREMENT
   ---------------------------------------------------
   The artboard's third state is "locked rows at 68% opacity". Neither half of
   that survives contact with this site.

   Locked is untrue. All 25 modules are published and readable now, in any
   order; the page's own lede says "Start anywhere. Follow what interests you."
   A row the reader has not reached is not a row they may not read, so nothing
   here dims, disables or re-points the link, and the row must not be
   confusable with `.path-step[href="#"]` above, which is the genuinely
   unavailable case and already owns `cursor: not-allowed` and a muted title.
   That is why the title on a not-yet-reached row stays at full --ink-800
   strength: the difference between "you have not got here yet" and "this does
   not exist" has to survive being looked at quickly.

   68% is also below threshold, and `_contrastcheck.py` cannot see it: the
   checker measures token pairs and an opacity is not a token, so these were
   composited by hand. Over --paper at .68 the row's own type becomes
   `.path-step-title` (--ink-800) #596174 at 6.20:1, which survives, and
   `.path-step-sub` (--label, 11px uppercase mono, the hardest type on the
   page) #9CA2B3 at 2.55:1, which is a plain failure against the 4.5 small text
   needs — down from 4.53 at full strength. One declaration, two outcomes, and
   the one that fails is the one nobody would think to check. So no opacity is
   applied to any text in any of the three states. Emphasis is carried instead
   by the number plate's fill, by the left rule, and by what is present on the
   row rather than by how faint it is. */

/* The row shell. The control has to be a SIBLING of the link — a <button>
   inside an <a> is invalid and unreachable — so the <li> that is already
   around every step becomes the row, and the link keeps its own grid.

   The shell is selected as a bare `.ladder-row` rather than as
   `.path-sequence li.ladder-row`, which is what it was first written as. The
   longer form is 0,2,1 and the three state rules below it are 0,2,0, so the
   shell's own `border-left-color: transparent` beat all three and every row
   drew the same colourless rule. Specificity is not a detail in this sheet;
   the brief-ladder banner above records the same mistake in another place. */
.ladder-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  padding-left: var(--s4);
  border-left: 3px solid transparent;
}
.ladder-row > .path-step { flex: 1 1 20rem; min-width: 0; }

/* The artboard's 34px plate. It replaces nothing: `.path-step-num` is the
   number the renderer already emits, given a ground so the three states have
   somewhere to differ that is not the prose. */
.ladder-row .path-step-num {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--radius);
  font-size: 13px;
  background: var(--flag-handwave-chip-bg); color: var(--flag-handwave-chip-fg);
}

/* Completed — "in cyan". 1j's --cyan is #38BDF8, which is 2.14:1 on white and
   cannot be the rule on a paper card; --cyan-deep is the same hue as a light
   ground fill and measures 7.31. The plate inverts instead and gets the
   artboard's own pairing, --cyan on --ink-800 at 8.28, which PAIRS already
   asserts for the signpost key. */
.ladder-row.is-complete { border-left-color: var(--cyan-deep); }
.ladder-row.is-complete .path-step-num { background: var(--ink-800); color: var(--cyan); }

/* Current — "styled in orange". The rule is --accent-text rather than 1j's
   --accent for the reason the focus ring gives above: #F97316 is 2.80:1 on
   white, below the 3:1 a graphical object needs, and 1j itself rules that it
   never carries text on a light ground. The plate keeps the bright accent
   because it is a FILL there, with --ink-800 on it at 6.32.

   The tint is --flag-speculative-chip-bg (#FFEDD5) rather than the artboard's
   #fff7ed, which is not a token and would be a colour the checker cannot see.
   It costs one override: --label on that tint is 3.95:1, the same finding the
   Batch 2 banner records for --label on --mist, so the sub takes the chip's
   own foreground at 6.38 instead. */
.ladder-row.is-current {
  border-left-color: var(--accent-text);
  background: var(--flag-speculative-chip-bg);
}
.ladder-row.is-current .path-step-num { background: var(--accent); color: var(--ink-800); }
.ladder-row.is-current .path-step-sub { color: var(--flag-speculative-chip-fg); }

/* Not yet reached. A hairline where the other two states have a colour, and
   the quiet plate. No opacity, per the banner. */
.ladder-row.is-ahead { border-left-color: var(--hairline); }

/* The current row's word, because the state is never colour-only — 1m is
   explicit about that for flags and the reasoning does not stop at flags. A
   solid chip rather than an outline so it is not read as a second button
   beside the real one; the script decides between "Start here" and "Resume
   here" from the block's own count. */
.ladder-now {
  flex: none;
  padding: 3px 9px; border-radius: var(--radius);
  background: var(--accent-text); color: var(--paper);
  font: 500 11px var(--font-mono); letter-spacing: .1em; text-transform: uppercase;
  white-space: nowrap;
}

/* The toggle. Outlined, so it reads as the control on the row and the status
   chip beside it does not. `aria-pressed` carries the state and drives the
   styling from the same attribute, which means the two cannot drift. */
.ladder-mark {
  flex: none; margin-left: auto;
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 5px 10px;
  border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper); color: var(--muted);
  font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase;
}
.ladder-mark:hover { border-color: var(--muted); color: var(--ink-800); }
.ladder-box {
  width: 14px; height: 14px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--muted); border-radius: var(--radius);
  background: var(--paper);
}
.ladder-mark[aria-pressed="true"] { border-color: var(--cyan-deep); color: var(--cyan-deep); }
.ladder-mark[aria-pressed="true"] .ladder-box {
  background: var(--cyan-deep); border-color: var(--cyan-deep);
}
/* The tick is drawn from two borders rather than fetched or set in a data URI,
   because a data URI would carry a literal colour the token block could not
   reach and `_contrastcheck.py` could not read. --paper on --cyan-deep is
   already asserted at 7.31. */
.ladder-mark[aria-pressed="true"] .ladder-box::after {
  content: ""; width: 3px; height: 7px;
  border: solid var(--paper); border-width: 0 2px 2px 0;
  transform: translateY(-1px) rotate(45deg);
}

/* The per-path band — 1c's "YOUR PATH / 2 of 5 done / ~2.5 hrs total". It
   takes a full line of `.path-header` rather than a third slot beside the
   title and the duration, which would leave none of the three room. The label
   draws --accent-text, as `.definition-label` does on the same mist ground;
   --label would be 4.19 there. */
.ladder-summary {
  flex: 1 0 100%; align-self: stretch;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s4);
  margin-top: var(--s4); padding: var(--s3) var(--s4);
  background: var(--mist);
  border: 1px solid var(--hairline); border-radius: var(--radius);
}
.ladder-summary-label {
  font: 500 11px var(--font-mono); letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent-text);
}
.ladder-summary-count { font: 600 15px var(--font-display); color: var(--ink-800); }
.ladder-summary.is-done .ladder-summary-count { color: var(--cyan-deep); }
/* The bar is aria-hidden and says nothing the count beside it does not, so it
   is decoration; the fill is --accent-text anyway, at 4.79 on mist, because a
   bar drawn in --accent would be 2.59 there and this sheet has twice now
   decided that the light-ground orange is the dark one. */
.ladder-bar {
  flex: 1 1 140px; min-width: 90px; height: 4px;
  background: var(--hairline); border-radius: var(--radius); overflow: hidden;
}
.ladder-bar i { display: block; height: 100%; background: var(--accent-text); }
.ladder-summary-time { font-size: 13px; color: var(--muted); }
.ladder-note { flex: 1 0 100%; font-size: 13px; color: var(--muted); }

/* Reset. `.ladder-reset` is per path and immediate — five marks are five
   clicks to rebuild and the button names the path it empties. The global one
   arms first, because it can empty every mark the reader has ever recorded and
   that is not a thing to lose to a mis-click. */
.ladder-reset {
  appearance: none; cursor: pointer;
  padding: 4px 10px;
  border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper); color: var(--muted);
  font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase;
}
.ladder-reset:hover { border-color: var(--muted); color: var(--ink-800); }
.ladder-reset-all.is-armed {
  background: var(--flag-speculative-chip-bg);
  border-color: var(--flag-speculative-chip-fg);
  color: var(--flag-speculative-chip-fg);
}
.ladder-global {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s5); padding: var(--s3) var(--s4);
  background: var(--mist);
  border: 1px solid var(--hairline); border-radius: var(--radius);
}
.ladder-global-count {
  font: 500 11px var(--font-mono); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}

/* One live region for the whole page, not one per path: a single module can
   sit in three paths and three regions announcing the same click is three
   times the noise for one fact. Same clip as `.interactive-status`, and for
   the same reason — without it the announcement is also painted onto the page. */
.ladder-for, .ladder-status {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* The script hides a control by setting `hidden`, and every display rule above
   would otherwise outrank the UA default. Specificity here is 0,2,0 against
   their 0,1,0, which is what makes `hidden` mean hidden. */
.ladder-row [hidden], .ladder-summary [hidden], .ladder-global[hidden] { display: none; }

@media (max-width: 760px) {
  /* At this width `.path-step` is already two columns with the sub wrapped
     under the title, so the link takes the whole line and the status chip and
     the toggle form a second one rather than squeezing a 34px plate, a title
     and two controls into 390px. */
  .ladder-row { padding-left: var(--s3); gap: var(--s2); }
  .ladder-row > .path-step { flex: 1 1 100%; }
  .ladder-mark { margin-left: 0; }
  .ladder-summary { gap: var(--s2) var(--s3); }
}
@media print {
  /* A printed ladder keeps what it says and loses what it does. */
  .ladder-mark, .ladder-reset, .ladder-global, .ladder-bar { display: none; }
}

/* ---------- Programme filter (artboard 1d) ----------
   1d draws a chip row between the hero and the programmes list, and 1m says
   what the chips are made of: "Flags are data, not decoration… they drive:
   inline underline color in body copy, chip color, programme card top border,
   topic-map cell color, and the filter chips on 1b and 1d. One source of truth,
   one color map."

   Everything in this family except the card's top border is ADDED to the DOM by
   `programme-filter.v0.9.js`, built from the page's own sections and cards.
   None of it reaches a page where that script does not run, which is the v0.3
   lesson: four interactive diagrams became empty boxes and every check passed.

   HOW THE ONE COLOUR MAP IS KEPT
   ------------------------------
   The script gives each status chip the card's own `.card-status live` (or
   `draft`, or `planned`) class and nothing else. `.card-status.live` is two
   classes and so outranks `.atlas-filter`'s single-class `background: --paper`
   no matter which is written first — so the chip takes its colour from the map
   already declared under `Cards grid` above, and this block declares no flag
   colour at all. A second copy of that mapping is the defect this file spends
   most of its length removing; the topic map already carries the second one. */
.ip-progfilter {
  background: var(--mist);
  border-bottom: 1px solid var(--hairline);
  padding-block: var(--s5);
}
/* `.atlas-controls` carries its own vertical rhythm for a control row standing
   in prose. Inside this band the band supplies it, so the rows only need to be
   told apart from each other. */
.ip-progfilter-row { margin: 0 0 var(--s2); }

/* Set in the body face rather than `.atlas-filter`'s 11px uppercase mono, for
   the reason measured on the topic map's category bar: uppercase mono with
   .08em tracking is right for a one-word status and wrong for eight programme
   names of up to 34 characters. "How Frontier Research Actually Works" is
   296px uppercase-tracked against 231px set as written, which is the
   difference between four rows of chips at 1440 and two. */
.ip-progfilter-row .atlas-filter {
  display: inline-flex; align-items: baseline; gap: var(--s2);
  text-transform: none; letter-spacing: .01em;
  font-family: var(--font-body); font-size: 13px;
}
/* The count is the page's own arithmetic and is set apart from the name so a
   reader scanning for a programme is not reading digits — the same separation
   the brief ladder makes between a section's number and its name. It inherits
   its colour, so on a tinted status chip it is the chip's own asserted pair
   rather than a fourth grey nobody measured. */
.ip-progfilter-n { font: 500 11px var(--font-mono); color: inherit; }

/* The two rows are one control, and with the labels at their natural widths the
   chips of one started 25px inside the chips of the other — two bars rather
   than one. --s9 is the wider label plus its gap, taken from the scale rather
   than measured off the screen. Only where the rows wrap: on the phone rail
   below 760px every pixel of a 390px viewport belongs to the chips.

   The hover state is left as `.atlas-filter` sets it. It recolours the label to
   --accent-text, which on the three flag tints measures 4.51 / 4.52 / 4.66 —
   passing, and by so little that the three pairs are now asserted in
   _contrastcheck.py rather than remembered here. */
@media (min-width: 761px) {
  .ip-progfilter-row .atlas-controls-label { min-width: var(--s9); }
}

/* The pressed chip. This is `.atlas-filter.active`'s look, restated because
   the state here is carried by `aria-pressed` rather than by a class: these are
   real buttons and the pressed state has to be the one assistive technology
   reads, not a class that only the stylesheet knows about. Ink over the flag
   tint on purpose — the tint says which flag, the ink says which is on. */
.ip-progfilter-chip[aria-pressed="true"] {
  background: var(--ink-800); border-color: var(--ink-800); color: #fff;
}
.ip-progfilter-chip[aria-pressed="true"]:hover { color: #fff; }

.ip-progfilter-readout {
  margin: var(--s3) 0 0;
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-text);
}

/* An empty result says so in words. The grid it would otherwise leave behind is
   not a smaller list, it is a page with nothing on it and no statement of why. */
.ip-progfilter-empty {
  margin-top: var(--s4); padding: var(--s4);
  background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius);
}
.ip-progfilter-empty-text { margin: 0 0 var(--s3); color: var(--ink-600); }

/* What filtering actually does. `.card` is `display: block` and `.section` has
   its own padding, so both outrank the `[hidden]` the script sets; these two
   rules are what make `hidden` mean hidden. Scoped to the class the script adds
   to `<main>` on its last line, so nothing here can hide anything on a page
   where the script did not finish. */
.ip-progfilter-live .card[hidden],
.ip-progfilter-live .section[hidden] { display: none; }

/* 1k: "Top border color = dominant flag of the programme." The programme cards
   carry no `data-flag` — no page in the built site does — but every one of them
   prints its status, so the border reads the status the card already shows and
   resolves it through the same three tokens `.card-status` uses.

   `:first-of-type` is what makes "dominant" mean something on the one card that
   carries two statuses: Category VIII ships "26/33 live" beside "2 ongoing",
   and the border follows the first status the card prints, which is the one a
   reader takes the card's state from. A browser without `:has()` drops these
   three rules and the border stays the accent it is today — a quieter card, not
   a wrong one, and no status is ever carried by colour alone because the card
   prints the word either way. */
.card:has(.card-meta .card-status.live:first-of-type)    { border-top-color: var(--flag-established-mark); }
.card:has(.card-meta .card-status.draft:first-of-type)   { border-top-color: var(--flag-speculative-mark); }
.card:has(.card-meta .card-status.planned:first-of-type) { border-top-color: var(--flag-handwave-mark); }

@media (max-width: 760px) {
  /* Eight programme names on a 390px viewport is eleven stacked rows and most
     of a screen of controls before the list starts. One horizontal rail keeps
     every name readable and the programmes above the fold; it is the chips that
     scroll, not the page. The 3px padding is room for a chip's focus ring at
     either end, which `overflow` would otherwise clip to the rail's edge. The
     topic map's category bar is the same decision for the same reason. */
  .ip-progfilter-row {
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    padding: 3px; margin-inline: -3px;
  }
  .ip-progfilter-row .atlas-filter,
  .ip-progfilter-row .atlas-controls-label { flex: none; }
}
@media print {
  /* The bar is the control; the list is the page. A printed page keeps whatever
     the reader filtered it to and loses the means of filtering it, which is the
     rule the topic map's own print block already sets. */
  .ip-progfilter { display: none; }
}

/* ---------- Study S-001 (artboard 1f) ----------
   1m, verbatim: "1f Study — buckets as bars, horizons as one timeline,
   bottlenecks in a dark block. The PDF is the same page printed, not a separate
   layout."

   All four clauses are CSS here. Nothing in this family is scripted, because
   nothing in it needs to be: the buckets, the horizons and the bottlenecks are
   all already in the authored markup of the study's pages — as `data-bucket` /
   `data-risk` on the atlas rows, as the four `.phase-card`s of the roadmap, and
   as `.depnode.bottleneck` in the dependency graph. A transform that CSS can do
   from existing markup is a transform that survives a page this file has never
   seen, which is the whole reason the redesign restyles a vocabulary instead of
   emitting a new one.

   WHAT IS NOT HERE, AND WHY
   -------------------------
   The study's own page presents its three capability buckets as
   `.study-overview-block`s whose magnitude — "5 required advances · 10-20y
   horizon" — is a sentence inside `.study-overview-meta`. A bar drawn from a
   number parsed out of that sentence would be a chart of this stylesheet's
   guess rather than of the study's data, so there is none. The blocks take the
   bucket's colour (below) and keep their words. */

/* ONE RAMP FOR THE THREE BUCKETS.
   This file had two. `Funding allocation bar` fills bucket A with --cyan-deep,
   B with --blue and C with --ink-700, stepping back through the mesh's own
   cyan → blue → navy depths; `Horizon timeline` fills A with --ink-700, B with
   --blue and C with --muted, which is the same three buckets on the reverse
   ramp. A reader who meets both on one study learns that dark navy means
   therapeutics on one page and diagnostics on the next, and 1m's "one source of
   truth, one color map" is exactly the rule that forbids it.

   The allocation ramp wins because it is the one whose fills are named in the
   legend beside the bucket names, and because its five values are the pairs
   `_contrastcheck.py` already asserts. White 10px mono on the three measures
   7.31 / 6.70 / 15.39 — all clear of the 4.5 a label inside a fill needs, which
   is what the timeline's own note was measuring when it recorded 15.4 / 6.7 /
   5.6 for the ramp this replaces. The note above `.tl-bar-a` describes the
   ramp, not this one. Bucket B is --blue on both ramps and needs no rule. */
.tl-bar-a rect { fill: var(--cyan-deep); }
.tl-bar-c rect { fill: var(--ink-700); }

/* The ramp, resolved once per row from the bucket the markup already declares,
   and carried to the bar and the tag by one custom property. */
.atlas-row[data-bucket="A"] { --bucket-fill: var(--cyan-deep); }
.atlas-row[data-bucket="B"] { --bucket-fill: var(--blue); }
.atlas-row[data-bucket="C"] { --bucket-fill: var(--ink-700); }
/* The study page's own bucket blocks, keyed to the bucket page each one links
   to. The bucket letter on those blocks is prose inside a div — "Bucket A" —
   and the href is the only structured statement of which bucket a block is. If
   a bucket page is ever renamed the block falls back to the accent edge every
   other card carries, which is a quieter card rather than a wrong one. */
.study-overview-block[href$="bucket-a.html"] { --bucket-fill: var(--cyan-deep); border-top-color: var(--cyan-deep); }
.study-overview-block[href$="bucket-b.html"] { --bucket-fill: var(--blue);      border-top-color: var(--blue); }
.study-overview-block[href$="bucket-c.html"] { --bucket-fill: var(--ink-700);   border-top-color: var(--ink-700); }

/* BUCKETS AS BARS.
   The atlas is the study's bucket table: seventeen advances, each row declaring
   its bucket and its risk as `data-bucket` and `data-risk`. The risk column is
   therefore a magnitude grouped by bucket, which is a bar chart that has simply
   never been drawn, and drawing it needs no new markup — only the cell that
   holds the pill, found with `:has()`.

   The bar is a BORDER on a pseudo-element rather than a background, and that is
   a print decision taken on screen: a browser printing without background
   graphics drops a background-image bar and keeps a border, and 1m requires
   this page to print as itself. The word stays in the pill above it, so the
   length is reinforcement and never the only reading. */
.atlas-row[data-risk="1"] { --risk-w: 25%; }
.atlas-row[data-risk="2"] { --risk-w: 50%; }
.atlas-row[data-risk="3"] { --risk-w: 75%; }
.atlas-row[data-risk="4"] { --risk-w: 100%; }
/* The bar is a block laid out after the pill rather than a box positioned
   inside the cell. The first version absolutely positioned it at the bottom of
   the cell, which is right only while every cell is one line tall: the Advance
   column wraps to five lines, so the bar ended up 70px below the pill it
   measures and read as a rule under the row. A static block is always the line
   under its own pill however tall the row grows. */
.atlas-row td:has(> .risk-pill)::after {
  content: ""; display: block; margin-top: var(--s2);
  width: var(--risk-w, 0); height: 0;
  border-bottom: 3px solid var(--bucket-fill, var(--muted));
}
/* A bottleneck row is the study's critical path, and the atlas prints "◆
   bottleneck" in its last cell. The edge repeats that in the one place a
   reader's eye lands first — the code — in --accent-text, 5.18 on white and
   4.79 on the zebra's mist, where the bright accent would be 2.80. */
.atlas-row[data-bottleneck="true"] td:first-child {
  border-left: 3px solid var(--accent-text);
}

/* BOTTLENECKS IN A DARK BLOCK.
   The dependency graph is where the study's four named bottlenecks are marked,
   and 1f puts that block on a dark ground. The graph could not simply be given
   `.on-dark`: it is authored markup on a shipped page and rule 3 of the screen
   brief forbids editing it, which is why the `.on-dark .depedge` rule written
   further up this file — added, by its own comment, because "1f asks for
   bottlenecks in a dark block" — has never once matched anything. It is
   restated here against the wrapper that actually exists.

   Measured on the block's own grounds: node labels are #fff on --ink-700
   (15.39), the node stroke --label-dk on --ink-900 (8.82), the bottleneck
   stroke --accent on --ink-700 (5.49) where --accent-text would be 2.97, and
   the edges --muted at full opacity (3.44) — a graphical object at 3:1, one
   step quieter than the nodes, which is the relation the faded edges were
   drawing on the light ground. */
.depgraph-wrap {
  background: var(--ink-900);
  border-color: var(--hairline-dark);
}
.depgraph-wrap .depnode rect { fill: var(--ink-700); stroke: var(--label-dk); }
.depgraph-wrap .depnode text { fill: #fff; }
.depgraph-wrap .depnode.bottleneck rect { stroke: var(--accent); stroke-width: 2; }
.depgraph-wrap .depedge { opacity: 1; }
.depgraph-wrap .depgraph-col-label { fill: var(--label-dk); }
/* The inversion that marks hover and focus has to invert against the new
   ground, not the old one: --ink-800 on a --ink-900 block is a node that
   disappears when you point at it. */
.depgraph-wrap .depnode:hover rect,
.depgraph-wrap .depnode:focus-within rect { fill: var(--accent); }
.depgraph-wrap .depnode:hover text,
.depgraph-wrap .depnode:focus-within text { fill: var(--ink-800); }

/* HORIZONS AS ONE TIMELINE.
   The roadmap's four phases are the study's horizon list, and the old shape was
   a 2×2 grid of bordered cards — four separate objects for one continuous span
   of years. The grid becomes a single rail with four stops on it: one column,
   no boxes, one rule running the whole height, and a marker per phase. Nothing
   about the markup changes; what changes is that the reader can see that Phase
   II starts where Phase I ends.

   The rail and the markers are drawn with borders rather than fills so that the
   timeline survives a print without background graphics — see the print block
   at the foot of this family. */
.phase-grid {
  position: relative;
  grid-template-columns: 1fr; gap: 0;
  padding-left: var(--s7);
}
.phase-grid::before {
  content: ""; position: absolute; left: 5px; top: var(--s5); bottom: var(--s5);
  width: 0; border-left: 2px solid var(--hairline);
}
.phase-card {
  position: relative;
  background: none; border: 0; border-radius: 0; box-shadow: none;
  padding: var(--s5) 0;
}
.phase-card + .phase-card { border-top: 1px solid var(--hairline-soft); }
/* The stop. An outlined square rather than a filled one, which is the marker
   `.milestones` already draws in this sheet, and --accent-text rather than the
   bright accent because a 12px mark on white is a graphical object at 3:1 and
   #F97316 is 2.80 there. */
.phase-card::before {
  content: ""; position: absolute; left: calc(-1 * var(--s7) + 1px); top: calc(var(--s5) + 4px);
  width: 8px; height: 8px;
  border: 2px solid var(--accent-text); background: var(--paper);
}
.phase-card:hover { border-color: transparent; }
.phase-card:hover .phase-card-title { text-decoration: underline; text-underline-offset: 2px; }
.phase-card:hover::before { background: var(--accent-text); }
@media (max-width: 760px) {
  /* The `Responsive for the families above` block collapses `.phase-grid` to
     one column at this width. It already is one; what a 390px viewport needs is
     the rail pulled in, because 48px of gutter is an eighth of the screen. */
  .phase-grid { padding-left: var(--s6); }
  .phase-card::before { left: calc(-1 * var(--s6) + 1px); }
  .phase-grid::before { left: 4px; }
}

@media print {
  /* "The PDF is the same page printed, not a separate layout." The page prints
     as itself; what changes is only what paper cannot do.

     A browser printing without background graphics — the default — drops every
     background-color and background-image and keeps borders, SVG fills and
     text. So the rule is: anything whose MEANING is a fill has to be given that
     meaning again in ink. The failure this block exists to prevent is the
     opposite one, a dark ground printed as a black rectangle with invisible
     text inside it. */
  .depgraph-wrap {
    background: #fff !important; border-color: #000;
    overflow: visible;
  }
  .depgraph-wrap svg { min-width: 0; }
  .depgraph-wrap .depnode rect { fill: #fff; stroke: #000; }
  .depgraph-wrap .depnode text { fill: #000; }
  .depgraph-wrap .depnode.bottleneck rect { stroke: #000; stroke-width: 3; }
  .depgraph-wrap .depedge { stroke: #000; }
  .depgraph-wrap .depgraph-col-label { fill: #000; }

  /* The atlas is wide and its head is a dark fill carrying pale blue text —
     invisible on paper the moment the fill goes. Both the head and the
     scroller are paper problems, not screen ones. */
  .atlas-table-wrap { overflow: visible; }
  .atlas thead th, .dir-table thead th {
    background: none !important; color: #000 !important;
    border-bottom: 2px solid #000;
  }
  /* The risk bar is already a border and prints as one; it only needs its
     colour taken to ink, because a printed page has no bucket ramp. */
  .atlas-row td:has(> .risk-pill)::after { border-bottom-color: #000; }

  /* `.risk-very-high` is a solid --accent-text fill carrying --paper text: with
     the fill dropped it prints white on white, which is the whole of what the
     cell says. It is not this family's rule, but it is this family's page. */
  .risk-very-high, .risk-veryhigh {
    background: none !important; color: #000 !important; border-color: #000;
  }

  /* "Buckets as bars" has to survive the same loss. The allocation bar's five
     segments are fills carrying white percentages; printed without them the bar
     is an empty outline. The segment widths are inline styles and do survive,
     so the bar prints as a divided rule with every percentage readable in
     black, which is the same bar in the medium's own terms. */
  .allocation-bar { border-color: #000; }
  .allocation-segment {
    background: none !important; color: #000 !important;
    border-right: 1px solid #000;
  }
  .allocation-segment:last-child { border-right: 0; }
  .allocation-swatch { border: 1px solid #000; }
  /* A 10% segment is narrower than its own label on paper as on screen, and the
     segment clips it. Clipped from the left as well as the right, "Integ · 10%"
     printed as "nteg · 10"; aligned to its start it prints as "Integ · 1" and
     the legend below carries the full name, which is the arrangement that
     block's own note describes. */
  .allocation-segment { justify-content: flex-start; }

  /* The numbered plate on the AIHS working definition is --paper on an --ink-800
     fill: the one element on the study's own page that the loss of backgrounds
     would leave as white text on white paper. Chromium currently lightens such
     a glyph to a mid-grey of its own accord — measured at 2.5:1 in the PDF this
     rule was written against — which is a mitigation, not a contract, and not a
     contrast anyone would sign off. The plate prints as an outlined number. */
  .definition-criteria li::before {
    background: none !important; color: #000 !important;
    border: 1px solid #000;
  }

  /* The roadmap rail is borders throughout and needs nothing but its colour;
     it is restated so the timeline is black rather than a 12% hairline that
     most printers will not put on paper at all. */
  .phase-grid::before { border-left-color: #000; }
  .phase-card::before { border-color: #000; background: #fff; }
  .phase-card + .phase-card { border-top-color: #000; }
}

/* ---------- Synthesis: the convergence map and the constraint atlas (artboard 1h) ----------
   1m: "1h Synthesis — the convergence map is SVG with hover isolation; the
   Atlas is a sortable table, flag column included."

   Two families, because 1h puts two products on one board. The map is
   `frontier-map.v0.8.js` drawing into the interactive stage this sheet already
   styles; the table is `atlas-table.v0.9.js` building rows out of the 1,138
   `.ca-item` entries the Constraint Atlas already ships. Both are ADDED by
   script: no page markup changes, and a page where neither runs is the page it
   was before — a diagram with its honest prose fallback, and five lists. */

/* --- The map's hover and focus isolation ---
   The isolation itself is in the script (which edges, which fronts, which
   nodes); what belongs here is the two things a stylesheet owns.

   A pointer target. The fronts and the nodes are <g> elements, and a group of
   <text> takes pointer events on the GLYPHS ONLY — `pointer-events` defaults to
   `visiblePainted`, so before the script drew itself a hit rectangle the hover
   target for a research front was the ink of the letters and the map flickered
   as a pointer crossed the space between two words. The cursor says the group
   is a control; the rectangle is the script's, because its size is a
   measurement of the label rather than a number this file could hold.

   And the state change. It is a change of state, not an animation, so it is
   short and it is linear; at 140ms the eye follows which sub-graph survived the
   dimming, and instantly it simply looks like a different picture. Reduced
   motion turns it off from two directions: this rule is inside
   `no-preference`, and the `.interactive *` rule further up this sheet already
   kills every transition in the box under `reduce`. It is written here as well
   because the script samples `prefers-reduced-motion` once, at load, and a
   media query keeps up with a reader who changes the setting afterwards. */
.fm-hit { cursor: pointer; }
@media (prefers-reduced-motion: no-preference) {
  .fm-edge, .fm-front, .fm-node {
    transition: opacity .14s linear, stroke-width .14s linear;
  }
}

/* --- The atlas as a table ---
   1h draws four columns: Constraint, Domain, Flag, "Δ since v0.4". Three of
   them are in the entries — the naming brief IS the domain, being the topic
   whose workback plan hits the constraint — and the fourth is not: nothing on
   the page records a previous release's atlas to difference against, and a
   column of invented deltas is the exact "decoration pretending to be data"
   1m's flag rule exists to forbid. The script replaces it with the count of
   naming briefs, which the page's own frontmatter calls the datum ("a token
   named by more than one brief keeps every naming brief — the recurrence IS
   the datum") and which four of the 1,138 constraints have more than one of.

   The table takes the site's own `.atlas` class and adds to it, so it stripes,
   hovers and rules like every other table on the site and this block holds only
   what is different: the sort buttons in the head, the flag chips in the body,
   and the two columns that should shrink to their content. */
.ip-atlas { margin: var(--s6) 0 var(--s7); }

.ip-atlas-head {
  padding-bottom: var(--s3);
  border-bottom: 2px solid var(--ink-800);
}
.ip-atlas-title {
  margin: 0; font: 600 18px var(--font-display); color: var(--ink-800);
}
.ip-atlas-meta {
  margin: var(--s1) 0 0;
  font: 400 11px var(--font-mono); letter-spacing: .06em; color: var(--label);
}

/* The key, which is where each kind's WORD and its expansion live in plain
   sight against the tint its chips carry. 1m: "Flags must never be color-only —
   always the word." A cell has room for the code and nothing else. */
.ip-atlas-key {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  font: 400 13px var(--font-body); color: var(--ink-600);
}
.ip-atlas-key-item { margin: 0; display: flex; align-items: center; gap: var(--s2); }
.ip-atlas-key-item b { font-weight: 500; color: var(--label); }

.ip-atlas-readout {
  margin: var(--s3) 0 0;
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-text);
}

/* THE FLAG COLUMN'S CHIP.
   The Constraint Atlas records no evidence flag — a constraint is not a claim,
   and nothing in an entry carries the four-flag enum. What every entry does
   carry is its KIND, the closed vocabulary the whole page is organised by, so
   the kind is the flag column and it is drawn the way 1m requires a flag to be
   drawn: a tinted chip carrying the word, with the expansion in the key above
   and in the chip's own accessible name.

   The tints are the four flag chip pairs this sheet already ships, addressed by
   the kind's POSITION in the vocabulary the script read off the page rather
   than by its name, and wrapping when there are more kinds than tints. Five
   kinds and four tints today, so INST and SUP share a tint — survivable exactly
   because the chip says INST or SUP in words, and the same trade the topic map
   documents where five statuses share three colours. A sixth kind gets a tint
   without anybody editing this file, and no new colour enters the sheet, so
   there is nothing here `_contrastcheck.py` is not already asserting. */
.ip-atlas-flag {
  display: inline-block; white-space: nowrap;
  padding: 2px 7px; border-radius: var(--radius);
  font: 500 11px var(--font-mono); letter-spacing: .06em;
}
.ip-atlas-flag[data-tone="0"] { background: var(--flag-established-chip-bg); color: var(--flag-established-chip-fg); }
.ip-atlas-flag[data-tone="1"] { background: var(--flag-frontier-chip-bg);    color: var(--flag-frontier-chip-fg); }
.ip-atlas-flag[data-tone="2"] { background: var(--flag-speculative-chip-bg); color: var(--flag-speculative-chip-fg); }
.ip-atlas-flag[data-tone="3"] { background: var(--flag-handwave-chip-bg);    color: var(--flag-handwave-chip-fg); }

/* The wrapper is `.atlas-table-wrap`, the page's own class for a table wider
   than its column — the summary matrix two screens up already scrolls that way,
   so the atlas table scrolls the same way rather than inventing a second
   behaviour. The minimum width is what four columns need before two of them are
   one word per line; below it the wrapper scrolls and the page does not. */
.ip-atlas-table { min-width: 34rem; }

/* The sort control. A real button, and the padding is moved off the header cell
   onto it so that the whole cell is the target rather than the seventeen pixels
   of type in the middle of it. */
.ip-atlas-table thead th { padding: 0; }
.ip-atlas-sort {
  display: block; width: 100%; min-height: 44px; padding: 10px 12px;
  appearance: none; -webkit-appearance: none; border: 0; background: none;
  cursor: pointer; text-align: left;
  font: inherit; letter-spacing: inherit; color: inherit;
}
.ip-atlas-sort:hover { background: rgba(255, 255, 255, .1); color: var(--paper); }

/* The focus ring inside the head, which is the one place on this screen the
   sheet's default would have been wrong. `:focus-visible` is 2px of
   --focus-light, chosen for a light ground where it measures 5.18; the table
   head is --ink-800, where the same colour is 2.4:1 and a keyboard user loses
   the only thing telling them where they are. --focus-dark is 6.32 there. This
   is the substitution `.on-dark :focus-visible` already makes for dark
   sections; the head is a dark strip inside a light one, so it makes it too. */
.ip-atlas-table thead :focus-visible { outline-color: var(--focus-dark); }

/* The sorted column, on two channels. The glyph is the site's own `.sort-ind`
   and the script rewrites it — "⇅" when a column can be sorted, "↑"/"↓" when it
   is the sorted one — and the accent underline repeats that so the state is
   never read from hue alone. Driven from `aria-sort` rather than from a class
   of its own: the accessible state and the visible state are then the same fact
   and cannot drift apart. */
.ip-atlas-table th .sort-ind { font-size: .85em; color: var(--label-dk); }
.ip-atlas-table th[aria-sort="ascending"] .sort-ind,
.ip-atlas-table th[aria-sort="descending"] .sort-ind {
  color: var(--accent); border-bottom: 2px solid var(--accent);
}

/* The constraint is the row's header, so it is a <th scope="row"> and needs the
   cell treatment `.atlas td` gives everything else. */
.ip-atlas-table tbody th {
  padding: 10px 12px; border-top: 1px solid var(--hairline);
  text-align: left; font: 500 14px/1.5 var(--font-body); color: var(--ink-800);
}
/* Two columns shrink to their content and two share what is left. `width: 1%`
   with `white-space: nowrap` is the shrink-to-fit idiom for an auto-layout
   table: the browser gives the column its minimum and the percentage is never
   honoured literally. */
/* The tail the script leaves at the bottom of the table: one row as tall as
   every row it has not built yet, so the page's height is the height it will
   have and the scrollbar does not shrink under the hand holding it. It is
   furniture — `aria-hidden` in the markup — so it takes none of the row
   treatment, and it outruns `.atlas`'s own striping rule, which would otherwise
   paint a fifty-thousand-pixel band of mist every other sort. */
.ip-atlas-table tbody tr.ip-atlas-spacer,
.ip-atlas-table tbody tr.ip-atlas-spacer td {
  background: none; border: 0; padding: 0;
}

.ip-atlas-c-kind, .ip-atlas-c-n { width: 1%; white-space: nowrap; }
.ip-atlas-c-name { width: 44%; }
.ip-atlas-c-n {
  text-align: right;
  font: 400 12px var(--font-mono); color: var(--muted);
}
.ip-atlas-table th.ip-atlas-c-n .ip-atlas-sort { text-align: right; }

/* The lists' disclosure, styled as the topic map's table toggle is, because
   they are the same control: a reading preference over a long block a reader
   has already been shown, not a filter. */
.ip-atlas-listtoggle {
  appearance: none; cursor: pointer;
  margin: var(--s5) 0 var(--s4);
  padding: 8px 14px; min-height: 44px;
  border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--paper);
  font: 500 11px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.ip-atlas-listtoggle:hover { border-color: var(--muted); color: var(--ink-800); }
.ip-atlas-listtoggle::before { content: "\25BE"; margin-right: var(--s2); }
.ip-atlas-listtoggle[aria-expanded="false"]::before { content: "\25B8"; }

/* The kind expansion inside a chip, and the table's caption: present for
   anything that reads the page, clipped for anything that looks at it. Not
   `.sr-only` — that class is used on this site but neither stylesheet defines
   it, its appearance being an inline style in a template, so relying on it
   would be relying on a rule that is not here. */
.ip-atlas-vh {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

@media print {
  /* The toggle is furniture, and the sort controls are inert on paper; the
     table prints as it stands, and the lists print whatever the reader left
     open. */
  .ip-atlas-listtoggle { display: none; }
  .ip-atlas-table thead th { padding: 10px 12px; }
  .ip-atlas-sort { min-height: 0; padding: 0; }
}

/* ---------- About / Method: the flag legend (artboard 1g) ----------
   1g draws the method page as three things: a dark hero, the fifteen-point
   framework as a numbered sequence of cells, and the four flags as a legend.
   Only the third is in `about.html`. The page has no `.hero` element and no
   fifteen-point list — it is `.article > .container-narrow > .article-header +
   .article-body`, nine headings of prose — and neither of the missing two can
   be conjured from what is there without adding DOM. They are named in the
   handoff report instead of being faked here.

   The legend is there, and it is the corpus's own house pattern: a paragraph
   that OPENS with a chip, four of them in a row.

     <p><span class="flag flag-established">Established</span> Settled science
        with strong empirical support. …</p>

   1g sets that pair as two columns — `width:56px;flex:none;text-align:center`
   on the chip, the definition beside it. This builds the same reading out of
   the chip alone, and out of nothing else, because the paragraph must not be
   reflowed: see the note on the gutter below. */

/* The chip column. Four flag words of two lengths — "Established" and
   "Speculative" measure 91.45px at 11px mono, "Frontier" and "Handwave" 70.34
   — so left to themselves the four chips rag by 21px and the definitions start
   at four different places, which is the one thing a legend must not do.
   14.5ch is 95.7px: the widest word plus a hair, in the units of the font that
   sets it. 13.5ch was tried first and is wrong — Chromium resolves it to
   89.11px against IBM Plex Mono's 6.6px "0", under the 91.45px "Established"
   actually occupies, so two of the four chips stayed 2.4px wide of the other
   two and the column was still ragged by an amount small enough to read as a
   rendering artefact rather than as a wrong number. A `ch` is the zero glyph,
   not the widest one, and the .04em tracking sits on top of it; only the
   measurement settles it. A fallback to Courier New widens the box rather than
   clipping a word, and clipping is the failure that matters here, because the
   word IS the flag — 1m: "flags must never be colour-only, always the word".

   `:first-child` is doing the semantics. A chip that opens a paragraph labels
   the whole paragraph and belongs in a column; a chip mid-sentence annotates
   that sentence and must stay tight against it, and the authored essays are
   full of the second kind (`content/ringgates/energy-conditions.html` has
   paragraphs carrying four). CSS ignores text nodes for `:first-child`, so the
   newline the renderer puts before the span does not defeat it. */
.article-body > p > .flag:first-child {
  min-width: 14.5ch;
  text-align: center;
  margin-right: var(--s2);
}

/* A run of chip-led paragraphs is one legend, not four paragraphs, so the rows
   inside a run close up: 24px to 16px, which is 1g's 7px gap at the 620px the
   artboard is drawn at. Only a paragraph whose NEXT sibling is also chip-led
   tightens, so the last row of a run keeps its full space before the prose that
   follows, and a lone flagged claim in an essay is untouched.

   Two `:has()` on one compound rather than one inside another: `:has()` may not
   be nested, and `p:has(+ p > .flag:first-child)` expresses the lookahead as a
   plain relative selector, which is legal. Where `:has()` is unsupported the
   run simply keeps the paragraph rhythm it has now — nothing is hidden and
   nothing is moved. */
.article-body > p:has(> .flag:first-child):has(+ p > .flag:first-child) {
  margin-bottom: var(--s4);
}

/* WHAT THIS DELIBERATELY DOES NOT DO: hang the definition in a gutter.
   A hanging indent reads as "this flag governs this block", and on the authored
   pages a chip-led paragraph often carries three or four more chips inside it —
   the opening flag governs its first claim, not the paragraph. Indenting those
   would state something false about the evidence, which is the one kind of
   defect this site treats as a regression rather than a style choice. The chips
   align; the text stays where the prose put it.

   Nor does it reach `.article-body > div > p > .flag`, the brief's shape. The
   briefs' section bodies are the 1e reading view's column and are laid out
   against the ladder; a second opinion about their chips belongs to that
   screen, not this one. */

/* ---------- Utility pages: the site directory and the licence (artboard 1i) ----------
   1i is the quiet end of the system — "dense, scannable, mono labels, generous
   hairlines, no decoration" — and it covers two pages that could hardly be less
   alike in size: `sitemap.html` (422 entries in ten blocks) and `licence.html`
   (five short clauses).

   LICENCE NEEDS NOTHING, and that is a finding rather than an omission. It is
   an `.article` of prose: eyebrow, title, subtitle, then `h2` clauses each
   opened by the 2px ink rule `.article-body h2` already draws. That is a legal
   statement set with care, and 1i's dark licence panel — orange mono label,
   muted-dk line, the version and the CC BY string — is already on the page as
   `.site-footer` + `.footer-base`. Restyling it would have been a second copy
   of a thing that exists.

   THE DIRECTORY IS THE WORK. Measured before this block, `sitemap.html` was
   19,133px tall at 1440 and 48,273px at 390, in one column, with every entry
   numbered as though it were a citation.

   The whole family is told apart by its element, not by its page: the sitemap's
   blocks are `<ul class="refs-list">` and the one bibliography that shares the
   class — `content/aihs/topics/references.html` — is `<ol class="refs-list">`.
   That is not a coincidence to lean on, it is the difference itself: an ordered
   list is cited BY ITS NUMBERS and an unordered one is not. So `ul` takes the
   directory treatment and `ol` keeps the `[01]` counter, and no page name
   appears anywhere below. */

/* The wrapper carries `style="margin-left: calc(80px + 2rem)"` in the built
   page — the offset of a left rail the old sheet drew and this one does not.
   It is markup and cannot be deleted, and at 390 it is not cosmetic: it leaves
   the index 230px wide inside a 390px viewport and the document scrolls
   sideways at 433px. An inline declaration yields only to `!important`, which
   is what the three of them in this section are for and the only place this
   file uses it outside `.flags-muted`. */
.section .container > div:has(> ul.refs-list) { margin-left: 0 !important; }

/* The section's own 96px of white reads as a gap rather than as rhythm when the
   first thing under the hero is a label instead of a heading. */
.section:has(> .container > div > ul.refs-list) { padding-block: var(--s7) var(--s8); }

/* The block labels. They arrive as `<h3 style="font-family:var(--font-display);
   font-weight:500;font-size:1.4rem;margin:2.5rem 0 1rem;letter-spacing:-0.01em">`
   — display type at 22.4px, which is a heading, and these are labels. 1i sets
   them mono, uppercase, accent; the tracking is this sheet's .2em rather than
   1i's .14em, because .2em is what every other label above a block already uses
   and a directory is not the place to start a second convention.

   Sticky because the tallest block is 319 entries: the name of the section you
   are reading stays on screen while you read it, which is the only thing that
   makes a 422-entry index usable at a glance. They stack rather than push —
   each label paints over the one before it, later-in-document on top — so the
   background has to be opaque paper, and the rule under it is what separates a
   stuck label from the rows sliding beneath. The masthead is static on this
   site, so `top: 0` is the top of the viewport and nothing is hidden behind. */
.section h3:has(+ ul.refs-list) {
  position: sticky; top: 0; z-index: 1;
  margin: var(--s7) 0 var(--s4) !important;
  padding-block: var(--s3);
  background: var(--paper);
  border-bottom: 1px solid var(--hairline);
  font: 500 11px/1.4 var(--font-mono) !important;
  letter-spacing: .2em !important;
  text-transform: uppercase;
  color: var(--accent-text);
}
.section h3:has(+ ul.refs-list):first-child { margin-top: 0 !important; }

/* The columns. `columns: 15rem 4` is a rule, not a column count: as many
   columns of at least 240px as the measure holds, up to four. 1232px of
   container gives four, a 390px phone gives one, and the tablet widths in
   between resolve themselves — which is the point, because the alternative is
   three breakpoints written down in a file that has been wrong about written-
   down numbers before. Four is the ceiling because a fifth column would put
   entries under 200px and the longest paths already wrap at 284. */
ul.refs-list {
  display: block;
  margin: 0 0 var(--s7);
  columns: 15rem 4;
  column-gap: var(--s6);
}

/* Rows, not paragraphs: a hairline each and 8/12px of padding, so the eye runs
   down a column instead of hopping 16px gaps. `break-inside: avoid` keeps a
   title and its path in the same column. */
ul.refs-list > li {
  break-inside: avoid;
  padding: var(--s2) 0 var(--s3);
  border-top: 1px solid var(--hairline-soft);
  line-height: 1.45;
}

/* The `[01]` counter is the numbering the prose cites a reference by. Nothing
   cites a sitemap, and 422 counters would cost 48px of gutter in every column
   to say what the row already says. The counter and its reset stay declared on
   `.refs-list` for the `ol`; this only stops it being drawn. */
ul.refs-list > li::before { content: none; }

/* 422 links in the accent is a wall of orange, and the accent is meant to be
   the one thing on a page you are being asked to act on. In a directory every
   row is a link, so colour is not what identifies them — the underline the
   family already draws is, and it stays. Ink-800 measures 17.73 on paper; the
   accent returns on hover, where it marks the one row under the pointer. */
ul.refs-list a { color: var(--ink-800); }
ul.refs-list a:hover { color: var(--accent-text); text-decoration-color: currentColor; }

/* The path under the name rather than beside it. It is the secondary fact —
   the page's own lede says "the path beside each entry is its location" — and
   inline it doubles the width a row needs. `anywhere` is load-bearing, not
   tidiness: `content/frontier/fr-additive-manufacturing-qualification.html` is
   400px of unbreakable mono at 11.5px and was one of the things pushing the
   390px viewport to a 433px scroll width. Colour and size come from the span's
   own inline style, where `--ink-faint` resolves to --label at 4.53 on paper. */
ul.refs-list > li > span {
  display: block;
  margin-top: var(--s1);
  overflow-wrap: anywhere;
}

@media print {
  /* Paper has no viewport to stick to, and two columns is what a page holds. */
  .section h3:has(+ ul.refs-list) { position: static; }
  ul.refs-list { columns: 2; }
}

/* ---------- The navigation sheet (artboard 1m) ----------
   1m, verbatim and the only thing it says about navigation below the desktop
   breakpoint: "Nav becomes a dark full-height sheet under 760px."

   `nav-sheet.v0.9.js` MOVES the masthead's own `nav.nav` into the panel and
   moves it back when the viewport widens. Nothing here may assume a second
   copy of the links exists, and nothing here may name one of them. The sheet
   and the toggle are absent from the DOM entirely above 760px, so most of this
   block never applies there and is not written as an override. */

.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  /* 44px: the target size the filter chips already settled on, and the floor
     for a control that is the only way to reach the site's navigation. */
  width: 44px; height: 44px; margin-right: calc(var(--s2) * -1);
  padding: 0; border: 1px solid var(--rule); border-radius: 2px;
  background: none; color: #fff; cursor: pointer;
}
.nav-toggle:hover { border-color: var(--accent); }
.nav-toggle-bars { display: block; width: 18px; }
.nav-toggle-bars i {
  display: block; height: 2px; background: currentColor; border-radius: 1px;
}
.nav-toggle-bars i + i { margin-top: 4px; }

/* The sheet is fixed and full height, over everything. `100dvh` rather than
   `100vh` because a phone browser's `vh` includes the address bar it then
   slides over the bottom of the panel; `vh` stays as the fallback for a
   browser that does not know the dynamic unit. */
.nav-sheet {
  position: fixed; inset: 0; z-index: 60;
  height: 100vh; height: 100dvh;
  background: rgba(6, 13, 31, .72);
}
.nav-sheet-panel {
  position: absolute; inset: 0 0 0 auto;
  width: min(320px, 86vw); height: 100%;
  background: var(--ink-900); color: #fff;
  border-left: 1px solid var(--rule);
  padding: var(--s8) var(--s5) var(--s5);
  overflow-y: auto;
}

/* The nav inside the panel. Its desktop rules are a horizontal flex row with a
   24px gap, so this is a genuine re-layout rather than a tweak -- which is why
   it is scoped to the panel and leaves `.nav` itself alone. */
.nav-sheet .nav {
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
}
.nav-sheet .nav a {
  padding: var(--s4) 0; font-size: 17px; color: #fff;
  border-bottom: 1px solid var(--rule);
}
.nav-sheet .nav a:hover { border-bottom: 1px solid var(--accent); }
.nav-sheet .nav a[aria-current="page"] {
  color: var(--accent); border-bottom: 1px solid var(--accent);
}
/* The call to action keeps being a button rather than becoming another row:
   it is the one item in the nav that is not a destination among peers, and a
   sheet that flattened it would lose the distinction 1a draws. */
.nav-sheet .nav a.nav-cta {
  margin-top: var(--s5); padding: var(--s3) var(--s4);
  font-size: 15px; text-align: center; color: var(--ink-900);
  border-bottom: 0;
}
.nav-sheet .nav a.nav-cta:hover { border-bottom: 0; }

/* The page behind the sheet does not scroll under it. Set on the root element
   by the script, so one class carries the whole "a sheet is open" state. */
.nav-sheet-open { overflow: hidden; }

@media (prefers-reduced-motion: no-preference) {
  .nav-sheet-panel { animation: nav-sheet-in .16s ease-out; }
  @keyframes nav-sheet-in { from { transform: translateX(12px); opacity: 0; } }
}

/* Above the breakpoint neither element exists. This rule is here for the one
   frame between the stylesheet applying and the script running on a wide
   viewport, and for a browser where the script does not run at all: whatever
   markup a future Shell might ship, a sheet is a phone thing. */
@media (min-width: 761px) {
  .nav-toggle, .nav-sheet { display: none; }
}

/* ---------- Responsive (1m) ----------
   "Programme grids 3 → 2 → 1. Topic map 18 → 9 → 6 columns. Brief sidebar
   collapses to a top accordion under 900px. Nav becomes a dark full-height
   sheet under 760px. Body copy never below 16px on any breakpoint." */
@media (max-width: 1100px) {
  .card-grid, .branch-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-band { grid-template-columns: repeat(2, 1fr); }
  .stat:nth-child(2) { border-right: 0; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 900px) {
  .toc { position: static; }
}
@media (max-width: 760px) {
  .card-grid, .branch-grid { grid-template-columns: 1fr; }
  .stat-band { grid-template-columns: 1fr; }
  .stat { border-right: 0; border-bottom: 1px solid var(--hairline-dark); }
  .footer-grid { grid-template-columns: 1fr; }
  .section, .hero > .container, .hero > .container-narrow { padding-block: var(--s7); }
  .nav { gap: var(--s4); flex-wrap: wrap; justify-content: flex-end; }
  .nav a { font-size: 13px; }
  .hero-rail { gap: var(--s5); }
  .reading-list { padding: var(--s4); }
}

/* ---------- Print ----------
   1m: the study PDF "is the same page printed, not a separate layout". */
@media print {
  .on-dark, .on-deep, .hero, .topbar, .site-footer, .stat-band {
    background: #fff !important; color: #000 !important;
  }
  .ip-mesh, .btn, .skip-link, .branch-block, .feedback { display: none !important; }
  .on-dark h1, .on-dark h2, .on-dark h3, .hero h1 { color: #000 !important; }
  a { color: #000; text-decoration: underline; }
  .article-body { max-width: none; }
}

/* ---------- Convergence map, narrow redraw (v1.0.0) ----------
   Below ~500px of stage width the 720-unit SVG renders its labels under any
   legibility floor, and the interactives rules forbid a horizontal scroller.
   So the widget redraws as a list at that width — same data, same flags —
   by toggling .fm-narrow on its container. The list is display:none at rest:
   it exists only when the SVG cannot be read, and the no-JS fallback block
   remains the JavaScript-free surface. */
.fm-list { display: none; }
.interactive.fm-narrow .interactive-stage { display: none; }
.interactive.fm-narrow .fm-list { display: block; margin: var(--s5) 0; }
.fm-list-node { margin-bottom: var(--s5); }
.fm-list-head {
  font-family: var(--font-display); font-size: 16px; font-weight: 700;
  margin: 0 0 var(--s2);
}
.fm-list-type {
  font: 500 9px var(--font-mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint); margin-left: 8px;
}
.fm-list-edges { list-style: none; padding-left: 0; margin: 0; }
.fm-list-edges li { padding: 6px 0; border-top: 1px solid var(--hairline); font-size: 14px; }

/* ---------- Claims (v1.0.0) ----------
   Every flag-led bolded statement carries its register id and is addressable
   as #c-<id>. The :target state exists for arrival by link — a challenge, a
   citation of a claim, a review note — and uses the same mist ground the rest
   of the sheet uses for emphasis-without-alarm. */
.claim:target {
  background: var(--mist);
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

/* ---------- The tech tree (v1.0.1) ----------
   The page is a drawn graph and it is deliberately not an SVG. A dependency
   graph over 234 briefs drawn as nodes and arcs is unreadable at any width a
   reader has, unreachable to a screen reader, and impossible to gate — and the
   thing the site actually owes a reader is WHICH BRIEF RESTS ON WHICH, which a
   layered list says exactly and a hairball says worse. So the layering is the
   drawing: ordered layers, each a list of cards, each card naming its own
   prerequisites as links. No script, no canvas, no layout file, nothing to go
   stale, and the whole page is as usable at 400px and with JavaScript off as it
   is anywhere else.

   Every value below is a token this sheet already declares. */
.tt-readout, .tt-limits {
  list-style: none; padding-left: 0; margin: var(--s4) 0 var(--s6);
}
.tt-readout li, .tt-limits li {
  padding: var(--s2) 0; border-top: 1px solid var(--hairline);
  font-size: 15px; line-height: 1.7;
}
.tt-readout li strong { font-family: var(--font-mono); font-weight: 500; }

.tt-spine { margin: var(--s4) 0 var(--s6); padding-left: 0; list-style: none; }
.tt-spine-item {
  position: relative; padding: var(--s2) 0 var(--s2) var(--s5);
  border-left: 2px solid var(--rule); margin-left: var(--s2);
}
.tt-spine-item::before {
  content: ""; position: absolute; left: -5px; top: 18px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent-text);
}

.tt-layer { margin: var(--s6) 0; }
.tt-layer-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s3);
  margin: 0 0 var(--s3); padding-bottom: var(--s2);
  border-bottom: 2px solid var(--ink-800);
  font: 600 18px var(--font-display); color: var(--ink-800);
}
.tt-layer-meta {
  font: 400 11px var(--font-mono); letter-spacing: .06em;
  text-transform: uppercase; color: var(--label);
}

.tt-nodes { list-style: none; padding-left: 0; margin: 0; }
.tt-node {
  display: block; padding: var(--s3) 0;
  border-top: 1px solid var(--hairline);
}
.tt-node:first-child { border-top: 0; }
.tt-code {
  display: inline-block; margin-right: var(--s2);
  font: 500 11px var(--font-mono); letter-spacing: .06em; color: var(--label);
}
.tt-title { font-weight: 500; }
.tt-cat {
  display: inline-block; margin-left: var(--s2);
  font: 400 11px var(--font-mono); letter-spacing: .04em; color: var(--ink-faint);
}
.tt-deps, .tt-dependents {
  display: block; margin-top: var(--s1);
  font-size: 13px; line-height: 1.6; color: var(--ink-600);
}
.tt-dependents { color: var(--ink-faint); font-family: var(--font-mono); font-size: 11px;
  letter-spacing: .04em; }
