/* ═══════════════════════════════════════════════════════════════════════
   project.css — theme for the project detail pages (project.html).
   Loads AFTER drafting.css and inherits its tokens (ink palette, fonts,
   radii, .set-tabs header, .dm-toggle, crosshair, night mode). This file
   removes the sheet chrome and lays content directly on a calmed backdrop:

   · #backdrop  — same swirl images as home, tamed by a translucent wash
   · .p-head    — back link, meta row, display title, lede, spec strip
   · .frame     — hairline media frame: corner brackets + HUD overlay
   · .toc       — sticky numbered left rail with scroll-spy (01–10)
   · .split     — text | figure two-column rhythm, alternating sides
   · .result    — big-number callout for headline measurements
   · .code      — dark editor-style card (traffic lights + filename)
   · .faq       — numbered accordion with animated grid-rows expand

   Perf: no live backdrop-filter, no canvas. The .set-tabs fake-frost from
   drafting.css is re-declared here with the wash added so the bar keeps
   matching what is actually behind it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Signature gold — section wayfinding ─────────────────────────────────
   Project pages carry a lot of same-weight headings ("Introduction",
   "Overview", "Machining"…) that were hard to tell apart from body copy at
   a glance. Gold is reserved for ONE job: "where am I in this document" —
   the section kicker + title, the version tick, and the active TOC / tab
   states. Nothing else gets it, so it stays a signal instead of decoration.

   --gold      solid, for small text (needs the 4.5:1 small-text ratio)
   --gold-grad metallic sheen for display type: dark → bright band → dark,
               the same background-clip:text technique as .p-title. The
               highlight band is deliberately held back from full brightness
               so even the lightest part of a glyph keeps ~3:1 on paper.
   Only loaded on project pages (project.css), so the homepage is untouched. */
:root {
  --gold: #7E6019;
  --gold-line: rgba(126, 96, 25, 0.55);
  --gold-grad: linear-gradient(180deg, #6B4E12 0%, #8F6D1E 42%, #AE8630 60%, #7A5A17 88%, #5E4410 100%);
}
body.dark-mode {
  --gold: #E3C078;
  --gold-line: rgba(227, 192, 120, 0.5);
  --gold-grad: linear-gradient(180deg, #FFF1CB 10%, #EBCB84 48%, #C79B45 78%, #A97F30 105%);
}

/* ── Backdrop — home's swirl, calmed with a two-stop wash ───────────────── */
#backdrop {
  background:
    linear-gradient(180deg, rgba(244, 246, 249, 0.88), rgba(238, 240, 243, 0.76)),
    url("media/backgrounds/bright-swirl.webp") center 30% / cover no-repeat;
}
/* Night mode sat darker than the homepage's because this wash is both heavy
   AND mixed from rgba(6,8,14) — below --desk (#0A0D14) and --paper-solid
   (#10131C), so the page read darker than the panels resting on it. Lifting
   the wash COLOR into the token range (rather than just dropping alpha)
   brightens it without letting the swirl's contrast back through the text. */
body.dark-mode #backdrop {
  background:
    linear-gradient(180deg, rgba(14, 18, 28, 0.82), rgba(14, 18, 28, 0.70)),
    url("media/backgrounds/dark-swirl.webp") center 35% / cover no-repeat;
}
/* Mobile: the swirl is a PORTRAIT image (1707x2560, ~0.67) held at `cover`.
   On a tall narrow phone, cover is height-driven, so essentially the whole
   image height maps to one viewport — the swirl's features shrink to around
   the size of the type itself and the two compete. Desktop, by contrast,
   is width-driven and only shows ~42% of the image height, so its features
   stay large and stay out of the way.

   Two fixes below: push the wash close to opaque, AND zoom the image so its
   features land in a coarser size range than the text (see background-size).
   Project pages only — the homepage loads drafting.css, never this file. */
@media (max-width: 860px) {
  /* Light mode needs the heavier hand of the two: the bright swirl's
     highlights land close to the page's own paper tone, so its darker
     sweeps are what the body copy actually fights. Nearly opaque here. */
  #backdrop {
    background:
      linear-gradient(180deg, rgba(245, 247, 250, 0.975), rgba(241, 243, 246, 0.955)),
      url("media/backgrounds/bright-swirl.webp") center 30% / cover no-repeat;
    background-size: auto max(180vh, 150vw);
  }
  body.dark-mode #backdrop {
    background:
      linear-gradient(180deg, rgba(14, 18, 28, 0.90), rgba(14, 18, 28, 0.86)),
      url("media/backgrounds/dark-swirl.webp") center 35% / cover no-repeat;
    background-size: auto max(180vh, 150vw);
  }
}
/* keep the header's pre-blurred fake frost in sync with the washed backdrop */
.set-tabs::before {
  background:
    linear-gradient(rgba(252, 253, 254, 0.28), rgba(252, 253, 254, 0.28)),
    linear-gradient(180deg, rgba(244, 246, 249, 0.86), rgba(238, 240, 243, 0.68)),
    url("media/backgrounds/bright-swirl.webp") center 30% / cover no-repeat;
  background-attachment: fixed;
}
body.dark-mode .set-tabs::before {
  background:
    linear-gradient(rgba(12, 16, 26, 0.16), rgba(12, 16, 26, 0.16)),
    linear-gradient(180deg, rgba(14, 18, 28, 0.80), rgba(14, 18, 28, 0.64)),
    url("media/backgrounds/dark-swirl.webp") center 35% / cover no-repeat;
  background-attachment: fixed;
}
/* ...including at the mobile wash level above, or the bar reads as a lighter
   panel floating over a much flatter page. */
@media (max-width: 860px) {
  /* Same zoom as #backdrop — this bar's frost is a fake (no live
     backdrop-filter), so its image must be sized identically or the swirl
     visibly jumps at the header's bottom edge. background-attachment:fixed
     means both resolve against the viewport, so the vh/vw math matches. */
  .set-tabs::before {
    background:
      linear-gradient(rgba(252, 253, 254, 0.28), rgba(252, 253, 254, 0.28)),
      linear-gradient(180deg, rgba(245, 247, 250, 0.955), rgba(241, 243, 246, 0.91)),
      url("media/backgrounds/bright-swirl.webp") center 30% / cover no-repeat;
    background-size: auto, auto, auto max(180vh, 150vw);
    background-attachment: fixed;
  }
  body.dark-mode .set-tabs::before {
    background:
      linear-gradient(rgba(12, 16, 26, 0.16), rgba(12, 16, 26, 0.16)),
      linear-gradient(180deg, rgba(14, 18, 28, 0.88), rgba(14, 18, 28, 0.80)),
      url("media/backgrounds/dark-swirl.webp") center 35% / cover no-repeat;
    background-size: auto, auto, auto max(180vh, 150vw);
    background-attachment: fixed;
  }
}

/* Top-bar active tab picks up the same gold as the TOC — one wayfinding
   colour whether the reader is reading the header or the rail. Scoped by
   virtue of living in project.css (the homepage never loads this file). */
.set-tabs nav a.active::after { background: var(--gold); }

/* ── Page column ────────────────────────────────────────────────────────── */
.proj { max-width: 1140px; margin: 0 auto; }

.backlink {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink-faint); text-decoration: none;
  margin-bottom: clamp(22px, 4vh, 40px);
}
.backlink:hover { color: var(--ink); }

/* ── Project head ───────────────────────────────────────────────────────── */
.p-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.p-meta .pno { color: var(--ink); font-weight: 600; letter-spacing: 0.2em; }
.p-meta .sep { opacity: 0.4; }
.pill {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 9px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  border: 1px solid currentColor; border-radius: 999px; padding: 5px 12px;
}
.pill::before { content: "●"; font-size: 7px; }
.pill.amber { color: var(--stamp); }
.pill.ok { color: var(--ok); }

.p-title {
  font-family: "Big Shoulders Display", var(--cond), sans-serif;
  font-weight: 800; text-transform: uppercase;
  font-size: clamp(52px, 7vw, 108px); line-height: 0.94; letter-spacing: 0.005em;
  max-width: 16ch; margin: 20px 0 24px;
  background: linear-gradient(180deg, #101114 10%, #4A4E57 115%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
body.dark-mode .p-title {
  background: linear-gradient(180deg, #FFFFFF 20%, #C7D6F2 65%, #8FA6D6 105%);
  -webkit-background-clip: text; background-clip: text;
}
.lede { font-size: clamp(15.5px, 1.45vw, 19px); line-height: 1.64; color: var(--ink-dim); max-width: 56ch; }
.lede strong { color: var(--ink); font-weight: 600; }

/* Opt-in per-project header treatment (META.centerTitle): title + lede both
   center-align and share the same full container width, so a wide two-line
   title and the paragraph below it read as one matched block. (Tried
   shrink-wrapping the block to the title's own intrinsic width via
   `width: fit-content` first — spec-correct in theory, but its shrink-to-fit
   pass also measures the lede's un-wrapped one-line width as a max-content
   candidate, which blew the container way past the viewport on narrow
   screens. Since the title's first line already spans ~98% of the
   container at desktop widths anyway, matching both to 100% reads the
   same but has zero shrink-to-fit risk.) */
.p-head.centered .p-title-group { text-align: center; }
.p-head.centered .p-title { max-width: none; }
.p-head.centered .lede { max-width: 100%; margin-inline: auto; }

/* ── Spec strip — hairline-ruled row, no boxes inside ───────────────────── */
.specstrip {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--paper); overflow: hidden;
  margin: clamp(26px, 4.5vh, 44px) 0;
}
.spec { padding: 16px 20px 14px; min-width: 0; }
.spec + .spec { border-left: 1px solid var(--line-soft); }
.spec .k {
  display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: 8px;
}
.spec .v { font-family: var(--cond); font-weight: 600; font-size: clamp(19px, 1.8vw, 25px); line-height: 1.1; color: var(--ink); }
.spec .v.ok { color: var(--ok); }
.spec .v.amber { color: var(--stamp); }
.spec .n { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); margin-left: 8px; }
@media (max-width: 720px) {
  .specstrip { grid-auto-flow: row; grid-template-columns: 1fr 1fr; }
  .spec + .spec { border-left: 0; }
  .spec:nth-child(even) { border-left: 1px solid var(--line-soft); }
  .spec:nth-child(n+3) { border-top: 1px solid var(--line-soft); }
  .spec .v { white-space: normal; }
}

/* ── Frames — hairline border, corner brackets, HUD overlay ─────────────── */
.frame {
  position: relative; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--paper);
  box-shadow: 0 18px 44px -30px rgba(10, 14, 20, 0.4);
}
body.dark-mode .frame { box-shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.7); }
.frame > img, .frame > video { display: block; width: 100%; height: auto; }
.frame > iframe { display: block; width: 100%; height: 100%; border: 0; position: absolute; inset: 0; }
.frame.embed { aspect-ratio: 16 / 9; }
/* fixed-ratio variant (hero + gallery rhythm): media fills and crops */
.frame.cover { aspect-ratio: 16 / 9; }
.frame.cover > img, .frame.cover > video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
/* corner brackets — blend-difference keeps them visible on any image */
.frame .brk {
  position: absolute; width: 12px; height: 12px; pointer-events: none;
  border: 0 solid rgba(255, 255, 255, 0.9); mix-blend-mode: difference;
}
.frame .brk.tl { top: 9px; left: 9px; border-top-width: 1.5px; border-left-width: 1.5px; }
.frame .brk.tr { top: 9px; right: 9px; border-top-width: 1.5px; border-right-width: 1.5px; }
.frame .brk.bl { bottom: 9px; left: 9px; border-bottom-width: 1.5px; border-left-width: 1.5px; }
.frame .brk.br { bottom: 9px; right: 9px; border-bottom-width: 1.5px; border-right-width: 1.5px; }
/* HUD labels — instrument-panel captions overlaid on the media */
.frame .hud {
  position: absolute; top: 12px; pointer-events: none;
  font-family: var(--mono); font-size: 8.5px; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.88); text-shadow: 0 1px 8px rgba(0, 0, 0, 0.55);
  max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.frame .hud.l { left: 28px; }
.frame .hud.r { right: 28px; }
/* placeholder for media not shot yet */
.slot-ph {
  display: grid; place-items: center; padding: 20px; text-align: center;
  min-height: 180px; aspect-ratio: 16 / 9;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink-ghost);
  background: repeating-linear-gradient(45deg, transparent 0 14px, var(--line-soft) 14px 15px);
}
.frame.cover .slot-ph { position: absolute; inset: 0; aspect-ratio: auto; }

.figcap {
  display: flex; justify-content: space-between; gap: 14px; margin-top: 10px;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.figcap b { font-weight: 600; color: var(--ink-dim); }
.figcap .nts { color: var(--ink-ghost); flex: none; }
/* The label span must be allowed to shrink and break, or a long caption
   pushes the flex:none "NTS" tag clean off the right edge of a phone (ASX-6
   FIG.9 did exactly that: an 18px document-wide bleed). A flex item's default
   min-width is auto = min-content, so a caption with long tokens refuses to
   narrow. `anywhere` rather than `break-word` because only `anywhere` also
   lowers the item's min-content contribution, which is what actually frees
   the flex line to fit. */
.figcap > span:not(.nts) { min-width: 0; overflow-wrap: anywhere; }
figure.fig { margin: 0; min-width: 0; }

/* Emphasis caption (slot.emphasis) — a big gradient headline instead of the
   usual small mono annotation, for a figure that IS the point (e.g. "this
   is the final result"), same gradient-clip technique as .p-title. */
.figcap.emphasis {
  display: block; margin-top: 12px;
}
.figcap.emphasis .figno-tag {
  display: block; margin-bottom: 5px;
  font-family: var(--mono); font-weight: 600; font-size: 9.5px;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint);
}
.figcap.emphasis .headline {
  display: block;
  font-family: "Big Shoulders Display", var(--cond), sans-serif;
  font-weight: 800; text-transform: uppercase;
  font-size: clamp(19px, 2vw, 30px); line-height: 1.08; letter-spacing: 0.005em;
  background: linear-gradient(180deg, #101114 10%, #4A4E57 115%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
body.dark-mode .figcap.emphasis .headline {
  background: linear-gradient(180deg, #FFFFFF 20%, #C7D6F2 65%, #8FA6D6 105%);
  -webkit-background-clip: text; background-clip: text;
}

/* Caption plate under a mated pair (META.bandA/bandB) — a thin frosted
   plate that replaces the individual figcaptions. No live backdrop-filter
   (this theme deliberately avoids it — see the homepage's blur-flicker
   history): a solid translucent fill reads as "glass" without repainting
   on every cursor move under the coord-readout crosshair. */
.pair-band {
  margin-top: 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--paper);
  box-shadow: 0 14px 34px -26px rgba(10, 14, 20, 0.35);
}
body.dark-mode .pair-band { box-shadow: 0 14px 34px -22px rgba(0, 0, 0, 0.55); }

/* single — one shared label, centered, both frames are "the same kind of thing" */
.pair-band:not(.split) { padding: 15px 22px; text-align: center; }
.band-label {
  font-family: var(--mono); font-weight: 600; font-size: 11px;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-dim);
}

/* split — each frame keeps its own text, centered in its own half, divided
   exactly where the frames above meet (same 65/35 proportion). */
.pair-band.split { display: flex; align-items: stretch; overflow: hidden; }
.band-half { padding: 14px 18px; text-align: center; min-width: 0; }
.band-half:not(.fill) { flex: 0 0 65%; }
.band-half.fill { flex: 1 1 auto; }
.band-half:not(:last-child) { border-right: 1px solid var(--line); }
.band-figno {
  display: block; margin-bottom: 4px;
  font-family: var(--mono); font-weight: 600; font-size: 9px;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint);
}
.band-headline {
  display: block;
  font-family: "Big Shoulders Display", var(--cond), sans-serif;
  font-weight: 800; text-transform: uppercase;
  font-size: clamp(15.5px, 1.5vw, 22px); line-height: 1.1; letter-spacing: 0.005em;
  background: linear-gradient(180deg, #101114 10%, #4A4E57 115%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
body.dark-mode .band-headline {
  background: linear-gradient(180deg, #FFFFFF 20%, #C7D6F2 65%, #8FA6D6 105%);
  -webkit-background-clip: text; background-clip: text;
}
/* Below 860px the frames themselves fall back to a plain stack (see the
   .mated rule inside the min-width:860px block above) — the band must
   match that same breakpoint, or a row-split band would sit under
   vertically-stacked frames and no longer line up with anything. */
@media (max-width: 859px) {
  .pair-band.split { flex-direction: column; }
  .band-half:not(.fill), .band-half.fill { flex: 1 1 auto; }
  .band-half:not(:last-child) { border-right: 0; border-bottom: 1px solid var(--line); }
}

/* head figure (FIG. 1 + optional film) */
.p-fig { margin-top: clamp(10px, 2vh, 18px); }
.p-fig + .p-fig { margin-top: clamp(24px, 4vh, 40px); }

/* Film pair — FIG.1 pinned top-left, FIG.2 pinned top-right, both at 0.65
   scale. Mobile: plain stack (above). Desktop: both absolutely positioned
   from their own corner inside a padding-bottom aspect box, so FIG.2 reads
   as staggered below-and-right of FIG.1 rather than stacked beneath it —
   the overlap in reserved height is what shrinks the block's total height. */
@media (min-width: 860px) {
  /* Plain block flow, not absolute positioning — far more robust than
     trying to derive corner-pinned percentages (top% resolves against
     the container's height, width% against its width; mixing the two to
     hand-place overlapping boxes turned out fragile across breakpoints).
     fig-a simply narrows to 65% — block flow already pins it top-left.
     fig-b narrows to 65% and pins its right edge via margin-left:auto,
     with a plain top gap. Both scale together, staggered left/right, no
     overlap to manage, and the height win comes from 0.65 scale alone. */
  .film-stack .fig-a { width: 65%; }
  .film-stack .fig-b { width: 65%; margin-left: auto; margin-top: clamp(18px, 2.4vh, 28px); }

  /* Mated pair (META.mateA/mateB present): the row spans the full stack
     width; the primary film frame keeps its 0.65 proportion and the mate
     fills what's left, edges flush against it — zero gap, seam corners
     squared (below), same joined-piece technique as the homepage's paired
     project cards. The mate frame has no aspect-ratio of its own; being a
     flex child of a column-flex figure, it stretches to fill whatever
     height is left after its own caption, which lands it flush with the
     main frame's top and (approximately, caption-height permitting) bottom. */
  .film-stack .fig-a.mated, .film-stack .fig-b.mated {
    width: 100%; margin-left: 0;
    display: flex; align-items: stretch; gap: 0;
  }
  .film-stack .mated > .fig + .fig { margin-top: 0; }
  .film-stack .mated > .fig:not(.fill) { flex: 0 0 65%; }
  .film-stack .mated > .fig.fill {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-direction: column;
  }
  .film-stack .mated > .fig.fill .frame { flex: 1 1 auto; aspect-ratio: auto; }
  .film-stack .mated > .fig.fill .figcap { flex: 0 0 auto; }
}
.mated > .fig + .fig { margin-top: 12px; }  /* mobile fallback: simple stacked gap */

/* Seam edges — squared + brackets hidden on the touching side. Frames sit
   truly flush (zero gap), but the two figcaptions below them would run
   into each other at that same seam, so they alone get a little breathing
   room back — the joined look is in the media, not the captions. */
.frame.seam-r { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.frame.seam-l { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.frame.seam-r .brk.tr, .frame.seam-r .brk.br { display: none; }
.frame.seam-l .brk.tl, .frame.seam-l .brk.bl { display: none; }
.frame.seam-r + .figcap { padding-right: 14px; }
.frame.seam-l + .figcap { padding-left: 14px; }
/* Below 860px a mated row falls back to a plain vertical stack, so the
   horizontal seam no longer exists — give the seam frames their rounded
   corners, brackets, and caption alignment back (same un-mating treatment
   the repeatability gallery already applies at this width). */
@media (max-width: 859px) {
  .frame.seam-r, .frame.seam-l { border-radius: var(--r-lg); }
  .frame.seam-r .brk.tr, .frame.seam-r .brk.br,
  .frame.seam-l .brk.tl, .frame.seam-l .brk.bl { display: block; }
  .frame.seam-r + .figcap { padding-right: 0; }
  .frame.seam-l + .figcap { padding-left: 0; }
  /* Stacked full-width, the "FINAL RESULT" mate (a 3:4 portrait photo) loses
     most of its subject to the default 16:9 landscape crop — match its
     native ratio instead. Scoped to this page AND to fig-b: fig-a's mate is
     a landscape clip, and other projects put different media in this slot
     (ASX-6's fig-b mate is a video). */
  .p-wave-drive .film-stack .fig-b .fig.fill .frame.cover { aspect-ratio: 3 / 4; }
}
#film { scroll-margin-top: 80px; }

/* ── Body grid — sticky TOC rail + content ──────────────────────────────── */
.body-grid {
  display: grid; grid-template-columns: 200px minmax(0, 1fr);
  gap: clamp(32px, 4.5vw, 68px);
  margin-top: clamp(44px, 7vh, 80px);
}
.toc { position: sticky; top: 76px; align-self: start; }
.toc-head {
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--ink-ghost); margin-bottom: 14px;
}
.toc a {
  display: flex; gap: 12px; align-items: baseline;
  padding: 6px 0 6px 14px; border-left: 2px solid transparent;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-faint); text-decoration: none;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.toc a .n { color: var(--ink-ghost); flex: none; }
.toc a:hover { color: var(--ink); }
.toc a.active { color: var(--ink); font-weight: 600; border-left-color: var(--gold); }
.toc a.active .n { color: var(--gold); }

@media (max-width: 1000px) {
  /* minmax(0,1fr), NOT bare 1fr: a 1fr track's automatic minimum is the
     column's min-content, and the math section's code card contributes its
     longest white-space:pre line (~760px) — which silently blew this track,
     and with it every section below the TOC, to ~2x the phone's viewport
     (the classic "page pans sideways, bottom wider than top" failure).
     Clamped to the container, the code card's own overflow-x:auto takes
     over and scrolls internally. Desktop's two-column template above
     already guards its content track the same way. */
  .body-grid { grid-template-columns: minmax(0, 1fr); gap: 0; }
  /* Sticky ribbon. Two details here are load-bearing for SMOOTHNESS, not
     looks — without them the bar drifts a few px with the page and then
     snaps back on every scroll frame (reads as vibration):

     1. `will-change: transform` promotes the bar to its own compositor
        layer. Sticky is normally repositioned on the compositor thread and
        stays glued during a scroll; when it gets demoted to the main
        thread its offset is only corrected once the main thread catches
        up, so it visibly lags the scroll and then jumps back into place.
        Its own `overflow-x: auto` (it IS a horizontal scroller) is what
        makes it a demotion candidate here, so the hint is explicit.
     2. An OPAQUE background. At 0.92 alpha the bar had to re-blend with
        whatever content was sliding underneath it every single frame,
        which forces a main-thread repaint of the bar on each scroll tick.
        Opaque at these values is visually near-identical and repaint-free.
     See also the removed `main { overflow-x: clip }` — an ancestor clip is
     the other classic sticky-demotion trigger, and it's no longer needed. */
  .toc {
    position: sticky; top: 60px; z-index: 40;
    display: flex; gap: 4px; align-items: center;
    overflow-x: auto; scrollbar-width: none;
    scroll-behavior: smooth;  /* for the auto-follow in project.html */
    margin: 0 calc(-1 * clamp(10px, 2.5vw, 40px));
    padding: 10px clamp(10px, 2.5vw, 40px);
    background: #F1F3F6;
    border-bottom: 1px solid var(--line-soft);
    will-change: transform;
  }
  body.dark-mode .toc { background: #0A0D14; }
  .toc::-webkit-scrollbar { display: none; }
  .toc-head { display: none; }
  .toc a { border-left: 0; border-bottom: 2px solid transparent; padding: 6px 10px; white-space: nowrap; }
  .toc a.active { border-bottom-color: var(--gold); }
}

/* ── Sections ───────────────────────────────────────────────────────────── */
.sec { padding: clamp(40px, 7vh, 76px) 0; border-top: 1px solid var(--line-soft); scroll-margin-top: 80px; }
.content > .sec:first-child { border-top: 0; padding-top: clamp(6px, 1vh, 12px); }
@media (max-width: 1000px) { .sec { scroll-margin-top: 118px; } }
.sec-kicker {
  display: flex; align-items: center; gap: 12px;
  font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--gold);
}
.sec-kicker::before { content: ""; width: 26px; height: 1px; background: var(--gold-line); }
.sec-title {
  font-family: var(--cond); font-weight: 600; text-transform: uppercase;
  font-size: clamp(30px, 3.4vw, 46px); line-height: 0.98; letter-spacing: 0.01em;
  margin: 12px 0 clamp(20px, 3vh, 30px);
  background: var(--gold-grad);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
/* background-clip:text needs a transparent fill on the heading, and BOTH
   `color` and `-webkit-text-fill-color` inherit — so any child that isn't
   meant to be gold-clipped renders invisible unless its fill is restored.
   The two that exist: the status pill (keeps its own ok/amber colour, which
   its border reads via currentColor) and the subnote. */
.sec-title .pill { vertical-align: 6px; margin-left: 14px; -webkit-text-fill-color: currentColor; }
.sec-title .sec-subnote {
  display: block;
  font-family: var(--mono); font-size: clamp(11px, 1.05vw, 13px); font-weight: 600;
  letter-spacing: 0.05em; text-transform: none; color: var(--ink-dim);
  -webkit-text-fill-color: currentColor;
  line-height: 1.55; margin-top: 10px;
}

.split {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 38%);
  gap: clamp(28px, 4vw, 60px); align-items: start;
}
.split.flip { grid-template-columns: minmax(260px, 38%) minmax(0, 1fr); }
.split.flip > .s-txt { order: 2; }
@media (max-width: 860px) {
  /* minmax(0,1fr) — same auto-minimum guard as .body-grid's mobile track. */
  .split, .split.flip { grid-template-columns: minmax(0, 1fr); }
  .split.flip > .s-txt { order: 0; }
}

/* Body copy runs 1px under the old 16.5px. Line-height nudged UP as the
   size came down: at a smaller size the same 1.78 ratio yields a tighter
   absolute leading, and these are long full-width paragraphs on mobile
   where the measure is what makes them readable. */
.s-txt { font-size: 15.5px; line-height: 1.8; color: var(--ink-dim); max-width: 64ch; }
/* A closing paragraph dropped below a .split row (s.bodyWide) — runs the
   section's full width instead of the 64ch column cap, since by the time
   it renders the split's columns (usually the taller figure) have ended. */
.s-txt-full { max-width: none; margin-top: clamp(20px, 3vh, 32px); }
.s-txt p { margin: 0 0 15px; }
.s-txt p:last-child { margin-bottom: 0; }
.s-txt strong { color: var(--ink); font-weight: 600; }
.s-txt em { font-style: italic; }
.s-txt a { color: var(--accent); text-underline-offset: 3px; }
.s-txt code {
  font-family: var(--mono); font-size: 0.85em;
  background: var(--paper-2); border: 1px solid var(--line-soft);
  border-radius: 5px; padding: 1px 5px;
}

/* Closing note — small right-aligned addendum figure beside a short text
   column, for a minor aside that doesn't warrant a full split row. */
.sec-closer {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(150px, 220px);
  gap: clamp(20px, 3vw, 36px); align-items: start;
  margin-top: clamp(32px, 5vh, 48px);
}
.sec-closer .frame.cover { aspect-ratio: 9 / 16; }
@media (max-width: 700px) {
  .sec-closer { grid-template-columns: 1fr; }
  .sec-closer .fig { max-width: 220px; margin: 0 auto; }
}

/* Second figure stacked below a .split — a standalone demo clip that
   doesn't need its own text column. */
.s-extra { margin-top: clamp(24px, 4vh, 40px); }

/* check lists (ASX-6 joints / queue) */
.check-list { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
.check-list li { display: flex; gap: 12px; font-size: 13.5px; line-height: 1.68; color: var(--ink-dim); }
.check-list .tick { font-family: var(--mono); color: var(--ok); font-weight: 600; flex: none; }
.check-list.queue .tick { color: var(--ink-faint); }
.check-list strong { color: var(--ink); font-weight: 600; }

/* ── Galleries ──────────────────────────────────────────────────────────── */
.gal { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(14px, 1.8vw, 22px); margin-top: clamp(22px, 3.5vh, 34px); align-items: start; }
.gal .fig.lead { grid-column: 1 / -1; }
.gal .frame.cover { aspect-ratio: 16 / 10; }
.gal .fig.lead .frame.cover { aspect-ratio: 21 / 10; }
.gal.wide { grid-template-columns: repeat(4, 1fr); }
.gal.wide .frame.cover { aspect-ratio: 4 / 3; }
.gal.wide .fig.lead .frame.cover { aspect-ratio: 21 / 10; }
.gal .frame .hud, .split .frame .hud { display: none; }  /* HUD on marquee frames only */
.gal .fig.lead .frame .hud { display: block; }
@media (max-width: 1000px) { .gal.wide { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px)  { .gal, .gal.wide { grid-template-columns: 1fr; } }

/* ── Version sub-blocks — iteration grids stacked inside one section ─────── */
/* Each version is a titled band: an accent-tick heading with a hairline that
   runs to the right margin, then its own media grid (crop ratio set per
   version via the --ar custom property on the grid), then a text column. */
.ver { margin-top: clamp(40px, 6vh, 64px); }
.ver-title {
  display: flex; align-items: center; gap: 14px;
  font-family: var(--cond); font-weight: 600; text-transform: uppercase;
  font-size: clamp(20px, 2.3vw, 28px); line-height: 1; letter-spacing: 0.03em;
  color: var(--ink); margin: 0 0 clamp(16px, 2.4vh, 22px);
}
/* Version blocks sit one level below a section, so they take the gold as a
   tick only — the heading text stays ink. Gold gradient on both would flatten
   the section → version hierarchy back out. */
.ver-title::before { content: ""; flex: none; width: 22px; height: 2px; background: var(--gold); }
.ver-title::after  { content: ""; flex: 1 1 auto; height: 1px; background: var(--line-soft); }
.gal.ver-grid { margin-top: 0; }
.gal.ver-grid .frame.cover { aspect-ratio: var(--ar, 16 / 10); }
/* A lead (full-width) item inside a version grid must out-rank the generic
   ".gal .fig.lead .frame.cover" 21:10 rule (more classes = higher
   specificity, wins regardless of source order) or its own --ar is
   silently ignored. Bottom-anchored crop = trims only off the top. */
.gal.ver-grid .fig.lead .frame.cover { aspect-ratio: var(--ar, 16 / 10); }
.gal.ver-grid .fig.lead .frame.cover > img { object-position: center bottom; }
.ver-txt { margin-top: clamp(16px, 2.4vh, 24px); }
/* This section's paragraphs (the summary and each version's own text) run
   the section's full content width instead of the site-wide 64ch column
   cap, since the media grids above them already run full width too.
   Font-size/line-height are now the site-wide .s-txt default — see below. */
#sec-prototype-demos .s-txt { max-width: none; }
#sec-architecture .s-txt { max-width: none; }
/* Design/build log intros — same reasoning: both are gallery-only sections
   whose grids already span the full content width, so nothing is gained by
   holding the lead-in paragraph to the 64ch measure. */
#sec-design-log .s-txt { max-width: none; }
#sec-build-log .s-txt { max-width: none; }
/* ASX-6's "What Exists Today" — same gallery-only case. Scoped to .p-asx6
   for consistency with the rest of this project's page-specific rules. */
.p-asx6 #sec-joints .s-txt { max-width: none; }
/* Base joint (J1) gearbox-internals photo (source 1500x2000, cropped into
   a 16:9 frame) has its subject sitting roughly 19-62% down the image.
   A transform: translateY() nudge was tried here first, but that <img> is
   position:absolute + inset:0 (already sized to exactly fill the frame),
   so translating it just exposes a same-size gap on the opposite edge —
   structural, not a magnitude problem. object-position instead slides the
   crop window within the oversized (post-cover-scale) image itself, which
   stays gap-free for any value 0-100%; 35% centers the window on the
   subject's actual vertical span. (3rd div sibling in the section: the
   kicker, then the intro .s-txt, then this .ver block.) */
#sec-architecture .ver:nth-of-type(3) .fig:nth-child(1) .frame.cover > img {
  object-position: center 35%;
}

/* ── Result callout — the headline number ───────────────────────────────── */
.result {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px clamp(24px, 3vw, 44px);
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--r-md); background: var(--paper);
  padding: clamp(18px, 2.6vh, 26px) clamp(20px, 2.6vw, 32px);
  margin: clamp(22px, 3.5vh, 34px) 0 0;
}
.result .big {
  font-family: var(--cond); font-weight: 700; font-size: clamp(44px, 5vw, 68px);
  line-height: 1; color: var(--ink); letter-spacing: 0.01em;
}
.result .txt { display: grid; gap: 5px; }
.result .lbl { font-family: var(--cond); font-weight: 600; text-transform: uppercase; font-size: clamp(15px, 1.4vw, 19px); color: var(--ink); }
.result .note { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint); }

/* ── Code card — dark editor style, glass like the homepage sheets ───────── */
/* Same "glass" language as the homepage .sheet (translucent fill + soft
   white edge over the swirl backdrop) but WITHOUT live backdrop-filter —
   this codebase's project pages run a live coord-readout crosshair
   (drafting.js) that re-rasterizes on every cursor move, and a real blur
   here would reproduce the flicker bug already fixed elsewhere (see
   .pair-band, which uses this identical translucent-fill-only technique). */
.code {
  margin-top: 22px; max-width: 860px;
  border-radius: var(--r-md); overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(17, 20, 27, 0.74); color: #D6DDE7;
  box-shadow: 0 18px 44px -30px rgba(10, 14, 20, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.s-txt .code { max-width: none; }

/* FIG.5 (Introduction) — source render is a square with generous margin
   around the subject, so a taller crop (object-fit:cover, same column
   width) just trims background, not the gearbox itself. */
#sec-introduction .frame {
  aspect-ratio: 4 / 5;
}
#sec-introduction .frame > img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}

/* FIG.6 (Working Principle) — tall 4:6 crop so the looping clip reads as
   roughly paragraph-height beside the text column, center-cropped. Scoped
   to .split (not .s-extra below it), which holds only this one frame. */
#sec-principle .split .frame {
  aspect-ratio: 4 / 6;
}
#sec-principle .split .frame > video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center;
}

/* Repeatability gallery (FIG.23-27) — the test video is actually 9:16 (a
   YouTube Short); shrinking it to a narrow column upsampled it and showed
   aliasing, so it now runs full width at its real (tall) ratio instead.
   The four rig photos sit below it in two mated pairs (flush zero-gap
   seam, matching the mating treatment used on the hero film pairs above),
   rather than a cramped 4-across row. */
/* Math section (FIG.29) — the handwritten-notes source image is extremely
   tall (517x2000, ~1:3.9) and dense with detail, so it earns a much wider
   figure column than the site default .split cap; text narrows to match.
   Guarded to >860px so the normal single-column mobile collapse still wins. */
@media (min-width: 861px) {
  #sec-math .split { grid-template-columns: minmax(0, 1fr) minmax(300px, 50%); }
}

#sec-repeatability .s-txt { max-width: none; }
#sec-repeatability .gal.wide {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto auto;
  column-gap: 0; row-gap: clamp(14px, 1.8vw, 22px);
}
#sec-repeatability .gal.wide .fig:nth-child(1) { grid-column: 1 / -1; grid-row: 1; }
#sec-repeatability .gal.wide .fig:nth-child(1) .frame.embed { aspect-ratio: 9 / 16; }
#sec-repeatability .gal.wide .fig:nth-child(2) { grid-column: 1; grid-row: 2; }
#sec-repeatability .gal.wide .fig:nth-child(3) { grid-column: 2; grid-row: 2; }
#sec-repeatability .gal.wide .fig:nth-child(4) { grid-column: 1; grid-row: 3; }
#sec-repeatability .gal.wide .fig:nth-child(5) { grid-column: 2; grid-row: 3; }
#sec-repeatability .gal.wide .fig:nth-child(2) .frame,
#sec-repeatability .gal.wide .fig:nth-child(4) .frame { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#sec-repeatability .gal.wide .fig:nth-child(3) .frame,
#sec-repeatability .gal.wide .fig:nth-child(5) .frame { border-top-left-radius: 0; border-bottom-left-radius: 0; }
#sec-repeatability .gal.wide .fig:nth-child(2) .brk.tr, #sec-repeatability .gal.wide .fig:nth-child(2) .brk.br,
#sec-repeatability .gal.wide .fig:nth-child(4) .brk.tr, #sec-repeatability .gal.wide .fig:nth-child(4) .brk.br { display: none; }
#sec-repeatability .gal.wide .fig:nth-child(3) .brk.tl, #sec-repeatability .gal.wide .fig:nth-child(3) .brk.bl,
#sec-repeatability .gal.wide .fig:nth-child(5) .brk.tl, #sec-repeatability .gal.wide .fig:nth-child(5) .brk.bl { display: none; }
#sec-repeatability .gal.wide .fig:nth-child(2) .figcap, #sec-repeatability .gal.wide .fig:nth-child(4) .figcap { padding-right: 14px; }
#sec-repeatability .gal.wide .fig:nth-child(3) .figcap, #sec-repeatability .gal.wide .fig:nth-child(5) .figcap { padding-left: 14px; }
@media (max-width: 860px) {
  #sec-repeatability .gal.wide { grid-template-columns: 1fr; row-gap: clamp(14px, 1.8vw, 22px); }
  #sec-repeatability .gal.wide .fig:nth-child(1),
  #sec-repeatability .gal.wide .fig:nth-child(2),
  #sec-repeatability .gal.wide .fig:nth-child(3),
  #sec-repeatability .gal.wide .fig:nth-child(4),
  #sec-repeatability .gal.wide .fig:nth-child(5) { grid-column: 1; grid-row: auto; }
  #sec-repeatability .gal.wide .fig:nth-child(2) .frame, #sec-repeatability .gal.wide .fig:nth-child(3) .frame,
  #sec-repeatability .gal.wide .fig:nth-child(4) .frame, #sec-repeatability .gal.wide .fig:nth-child(5) .frame { border-radius: var(--r-lg); }
  #sec-repeatability .gal.wide .fig .brk { display: block; }
  #sec-repeatability .gal.wide .fig .figcap { padding-left: 0; padding-right: 0; }
}

/* SolidWorks gallery (FIG.30-34) — FIG.30/31 mate as a flush pair on their
   own row, FIG.32-34 mate as a flush trio below (taller 5/6 crop so the two
   rows read as comparable weight, not the pair towering over a squat trio).
   6-column grid = LCM of 2 and 3: pair each span 3 tracks, trio each span 2.
   The load-bearing detail is `column-gap: 0` — with zero column gap, equal
   track SPANS are genuinely equal WIDTH and sit flush, so a shared
   aspect-ratio produces genuinely equal HEIGHT (bottom edges line up). An
   earlier version tried to keep a non-zero gap and close only the pair's
   seam with `margin-left: -gap`; that's a TRAP — a negative margin only
   SHIFTS an item to visually close the seam, it does NOT change the item's
   width, so the pulled item stayed one-gap WIDER (hence one-gap TALLER,
   ~14-18px) than its neighbour. Since every image here is now mated, zeroing
   the column gap outright removes the need for any margin compensation and
   is the actually-equal-width solution. `align-items: start` pins tops too,
   belt-and-suspenders with the now-equal heights. Only vertical seams exist
   (within each row); the row-gap keeps the two rows visually separated so no
   horizontal seam squaring is needed between rows. */
#sec-solidworks .s-txt { max-width: none; }
#sec-solidworks .gal.wide {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: auto auto;
  column-gap: 0; row-gap: clamp(14px, 1.8vw, 22px);
  align-items: start;
}
#sec-solidworks .gal.wide .fig:nth-child(1) .frame,
#sec-solidworks .gal.wide .fig:nth-child(2) .frame { aspect-ratio: 5 / 4; }
#sec-solidworks .gal.wide .fig:nth-child(3) .frame,
#sec-solidworks .gal.wide .fig:nth-child(4) .frame,
#sec-solidworks .gal.wide .fig:nth-child(5) .frame { aspect-ratio: 5 / 6; }
#sec-solidworks .gal.wide .fig:nth-child(1) { grid-column: 1 / 4; grid-row: 1; }
#sec-solidworks .gal.wide .fig:nth-child(2) { grid-column: 4 / 7; grid-row: 1; }
#sec-solidworks .gal.wide .fig:nth-child(3) { grid-column: 1 / 3; grid-row: 2; }
#sec-solidworks .gal.wide .fig:nth-child(4) { grid-column: 3 / 5; grid-row: 2; }
#sec-solidworks .gal.wide .fig:nth-child(5) { grid-column: 5 / 7; grid-row: 2; }
/* Seam edges — square the touching corners + hide their brackets. Pair:
   30 squares its right, 31 its left. Trio: 32 right, 33 both, 34 left. */
#sec-solidworks .gal.wide .fig:nth-child(1) .frame,
#sec-solidworks .gal.wide .fig:nth-child(3) .frame { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#sec-solidworks .gal.wide .fig:nth-child(2) .frame,
#sec-solidworks .gal.wide .fig:nth-child(5) .frame { border-top-left-radius: 0; border-bottom-left-radius: 0; }
#sec-solidworks .gal.wide .fig:nth-child(4) .frame { border-radius: 0; }
#sec-solidworks .gal.wide .fig:nth-child(1) .brk.tr, #sec-solidworks .gal.wide .fig:nth-child(1) .brk.br,
#sec-solidworks .gal.wide .fig:nth-child(3) .brk.tr, #sec-solidworks .gal.wide .fig:nth-child(3) .brk.br,
#sec-solidworks .gal.wide .fig:nth-child(4) .brk.tr, #sec-solidworks .gal.wide .fig:nth-child(4) .brk.br { display: none; }
#sec-solidworks .gal.wide .fig:nth-child(2) .brk.tl, #sec-solidworks .gal.wide .fig:nth-child(2) .brk.bl,
#sec-solidworks .gal.wide .fig:nth-child(4) .brk.tl, #sec-solidworks .gal.wide .fig:nth-child(4) .brk.bl,
#sec-solidworks .gal.wide .fig:nth-child(5) .brk.tl, #sec-solidworks .gal.wide .fig:nth-child(5) .brk.bl { display: none; }
#sec-solidworks .gal.wide .fig:nth-child(1) .figcap, #sec-solidworks .gal.wide .fig:nth-child(3) .figcap { padding-right: 14px; }
#sec-solidworks .gal.wide .fig:nth-child(2) .figcap, #sec-solidworks .gal.wide .fig:nth-child(5) .figcap { padding-left: 14px; }
#sec-solidworks .gal.wide .fig:nth-child(4) .figcap { padding-left: 14px; padding-right: 14px; }
@media (max-width: 860px) {
  #sec-solidworks .gal.wide { display: block; }
  #sec-solidworks .gal.wide .fig { margin-top: clamp(14px, 1.8vw, 22px); }
  #sec-solidworks .gal.wide .fig:first-child { margin-top: 0; }
  #sec-solidworks .gal.wide .fig .frame { border-radius: var(--r-lg) !important; }
  #sec-solidworks .gal.wide .fig .brk { display: block; }
  #sec-solidworks .gal.wide .fig .figcap { padding-left: 0; padding-right: 0; }
}

/* Assembly (FIG.35/36) — full-width text, then the assembly-array photo and
   the assembly YouTube video (same clip as the top-of-page hero), each its
   own full-width row. The array photo is a collage of sub-images, so it
   keeps its exact native ratio (903x719) instead of the gallery's default
   21:10 hero crop, which would slice into the sub-photos at top/bottom. */
#sec-assembly .s-txt { max-width: none; }
#sec-assembly .gal .fig.lead .frame.cover { aspect-ratio: 903 / 719; }

/* Machining (FIG.37) — mediaFirst: the engineering drawing leads, full
   width, natural ratio (1266x866) so the title block/dimensions at the
   edges survive uncropped; the write-up runs full width below it. */
#sec-machining .s-txt { max-width: none; }
#sec-machining .gal .fig.lead .frame.cover { aspect-ratio: 1266 / 866; }

/* Conclusion — no media at all, so this is purely the full-width-text
   treatment (same pattern as prototype-demos/repeatability/solidworks/
   math/assembly/machining above). */
#sec-conclusion .s-txt { max-width: none; }

/* ASX-6 "What Exists Today" — the last two encoder-mount photos are
   portrait (1500x2000 after the 90 CW rotation), so the gallery's default
   16:10 landscape crop would slice them to a thin band. They share grid
   row 3, so matching both to their native 3:4 shows them uncropped AND
   keeps that row's two frames the same height. (Keyed to grid position,
   not FIG number — figure numbering shifts whenever a section is added
   ahead of this one.) */
#sec-joints .gal .fig:nth-child(4) .frame.cover,
#sec-joints .gal .fig:nth-child(5) .frame.cover { aspect-ratio: 3 / 4; }

.code-head {
  display: flex; align-items: center; gap: 14px; padding: 11px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: rgba(214, 221, 231, 0.5);
}
.code-head .dots { display: inline-flex; gap: 6px; }
.code-head .dots i { width: 9px; height: 9px; border-radius: 50%; }
.code-head .dots i:nth-child(1) { background: #FF5F57; }
.code-head .dots i:nth-child(2) { background: #FEBC2E; }
.code-head .dots i:nth-child(3) { background: #28C840; }
.code-head b { color: #EDF1F6; font-weight: 500; }
.code-head .lang { margin-left: auto; }
.code pre { margin: 0; padding: 14px 0; overflow-x: auto; }
.code code.block { display: block; font-family: var(--mono); font-size: 12.5px; line-height: 1.75; counter-reset: ln; }
.code code.block span.ln { display: block; padding-right: 18px; white-space: pre; }
.code code.block span.ln::before {
  counter-increment: ln; content: counter(ln, decimal-leading-zero);
  display: inline-block; width: 48px; padding-right: 14px; margin-right: 16px;
  text-align: right; color: rgba(214, 221, 231, 0.25);
  border-right: 1px solid rgba(255, 255, 255, 0.08);
}
/* Lightweight syntax highlight tokens (regex-based, see codeCard() in
   project.html) — a small VS Code "Dark+"-style palette on the glass card. */
.code .tok-k { color: #C586C0; }               /* keyword */
.code .tok-s { color: #CE9178; }               /* string */
.code .tok-n { color: #B5CEA8; }               /* number */
.code .tok-f { color: #DCDCAA; }               /* function call */
.code .tok-c { color: #7CA666; font-style: italic; } /* comment */

/* ── FAQ accordion ──────────────────────────────────────────────────────── */
.faq { border-top: 1px solid var(--line); max-width: 860px; }
.faq-item { border-bottom: 1px solid var(--line); }
.faq-q {
  display: flex; align-items: baseline; gap: 16px; width: 100%;
  padding: 17px 4px; margin: 0; border: 0; background: none; cursor: pointer;
  text-align: left; font-family: var(--sans); font-size: 14px; font-weight: 600;
  color: var(--ink); line-height: 1.5;
}
.faq-q .qno { font-family: var(--mono); font-size: 10px; font-weight: 600; color: var(--gold); letter-spacing: 0.1em; flex: none; }
.faq-q .tog {
  margin-left: auto; flex: none;
  font-family: var(--mono); font-size: 14px; color: var(--ink-faint);
  transition: transform 0.25s ease;
}
.faq-item.open .faq-q .tog { transform: rotate(45deg); }
.faq-a {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}
.faq-item.open .faq-a { grid-template-rows: 1fr; }
.faq-a > div { overflow: hidden; }
.faq-a .ans { padding: 2px 4px 20px 47px; font-size: 13.5px; line-height: 1.72; color: var(--ink-dim); max-width: 62ch; }
.faq-a .ans em { font-style: italic; }
.faq-a .ans a { color: var(--accent); }
.faq-a .ans code { font-family: var(--mono); font-size: 0.85em; }
@media (max-width: 560px) { .faq-a .ans { padding-left: 4px; } }

/* ── Desktop typography — body copy scale ────────────────────────────────
   Every font-size above is the PHONE size (this file is mobile-first for
   type). Phones needed body copy a point smaller: the measure runs the full
   width of the screen there, so a smaller size fits more words per line and
   reads better. On a wide screen the same text sits in a 64ch column beside
   a figure, where that reduction just looked undersized — so the desktop
   sizes are restored here rather than being carried down from the phone.

   861px because that is where this page stops being a phone stack and the
   .split rows become two columns (same threshold #sec-math already uses).

   THIS IS THE ONE PLACE TO TUNE DESKTOP BODY TYPE. Each line pairs a size
   with its line-height; keep them together, since leading is a ratio of the
   size and the two drift apart if only one is edited. */
@media (min-width: 861px) {
  .s-txt         { font-size: 16.5px; line-height: 1.78; }
  .lede          { font-size: clamp(16.5px, 1.5vw, 20px); line-height: 1.62; }
  .check-list li { font-size: 14.5px; line-height: 1.65; }
  .faq-q         { font-size: 15px;   line-height: 1.45; }
  .faq-a .ans    { font-size: 14.5px; line-height: 1.7; }
  /* prose too, so it moves with the body scale rather than the heading */
  .sec-title .sec-subnote { font-size: clamp(12px, 1.1vw, 14px); line-height: 1.5; }
}

/* ── Prev / next ────────────────────────────────────────────────────────── */
.pn-band { border-top: 1px solid var(--line-soft); padding-top: clamp(36px, 6vh, 60px); }
.pn { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(14px, 2vw, 22px); }
.pn a {
  position: relative; isolation: isolate; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 5px;
  min-height: clamp(130px, 18vh, 180px);
  padding: 18px 20px;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  color: #F2F5F9; text-decoration: none; background: #14181F;
}
.pn .bg {
  position: absolute; inset: 0; z-index: -2;
  background-size: cover; background-position: center;
  filter: brightness(0.8);
  transition: transform 0.45s ease;
}
.pn a::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to top, rgba(8, 11, 16, 0.85), rgba(8, 11, 16, 0.3) 55%, rgba(8, 11, 16, 0.08));
}
.pn a:hover .bg { transform: scale(1.04); }
.pn .k { font-family: var(--mono); font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase; opacity: 0.75; }
.pn .t { font-family: var(--cond); font-size: clamp(18px, 1.9vw, 26px); font-weight: 600; text-transform: uppercase; line-height: 1; }
.pn a.next { align-items: flex-end; text-align: right; }
@media (max-width: 560px) { .pn { grid-template-columns: 1fr; } }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.foot { border-top: 1px solid var(--line-soft); margin-top: clamp(40px, 6vh, 64px); padding: 22px 0 10px; }
.foot .row {
  display: flex; justify-content: space-between; gap: 10px 22px; flex-wrap: wrap;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-faint);
}
.foot a { color: inherit; text-underline-offset: 3px; }

/* NOTE — deliberately NO `main { overflow-x: clip }` here, unlike the
   homepage's mobile pass. It was tried as a belt-and-braces bleed guard and
   removed: an ancestor clip is one of the triggers that demotes a sticky
   descendant off the compositor thread, and this page has a sticky TOC
   ribbon (plus sticky expand-chips) that then judder during scroll. It is
   also redundant here — drafting.css already sets `body { overflow-x:
   hidden }`, which propagates to the viewport site-wide, and this page's
   measured document width is exactly the viewport width once the grid
   tracks are clamped (see .body-grid above). If a future bleed appears,
   clip the offending block itself rather than the whole of main. */

/* ── Reveals ────────────────────────────────────────────────────────────── */
.rv { opacity: 0; transform: translateY(14px); transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.22, 0.9, 0.32, 1); }
.rv.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .rv { opacity: 1; transform: none; transition: none; }
  .faq-a, .faq-q .tog, .pn .bg { transition: none; }
  /* the TOC ribbon's auto-follow still tracks the active section, it just
     jumps there instead of sliding */
  .toc { scroll-behavior: auto; }
}

/* ── Zoomable frames — sticky "click to view full size" hint ─────────────── */
/* slot.zoomable frames get a pointer cursor and a chip that rides the
   viewport edge — the exact mechanic as the homepage's .open-chip:
   position:sticky;bottom:18px for the pin itself (this alone holds the
   chip near the viewport bottom for the whole time the frame spans a
   meaningful chunk of the viewport, not just once you're near the frame's
   own end — the frame's rect, not the chip's exact spot inside it, is what
   bounds the sticky range), PLUS a scroll-driven .show class (JS, see
   project.html) that fades it in only once the frame is a decent way into
   view and fades it out once scrolled past — same thresholds as the
   homepage (rect.top < 30vh, rect.bottom > 50vh). The chip's own box is
   zero-height so it never adds space below the media — the pill is pulled
   up into view with a transform instead. */
.frame.zoomable { cursor: zoom-in; }
.expand-chip {
  position: sticky; bottom: 18px; z-index: 3;
  height: 0; overflow: visible;
  display: flex; justify-content: center;
  pointer-events: none;
  opacity: 0; transition: opacity 0.35s ease;
}
.expand-chip.show { opacity: 1; }
.expand-chip span {
  transform: translateY(-18px);
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--accent); color: #FFFFFF;
  border-radius: 999px;
  font-family: var(--mono); font-size: 9.5px; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  padding: 9px 16px;
  box-shadow: 0 1px 2px rgba(23, 24, 27, 0.25), 0 10px 26px -8px rgba(23, 24, 27, 0.5);
}

/* ── Lightbox — full-size click-through viewer ────────────────────────────
   The scrim's blur is a deliberate, scoped exception to this codebase's
   "no live backdrop-filter" rule (see the .code card note above): it only
   exists in an active state while the lightbox is open, and opening it adds
   body.hide-crosshair — the same layer class the homepage Tweaks panel uses
   — which removes the one thing (the live coord-readout crosshair) that
   makes a persistent blur expensive on these pages. With the crosshair
   suppressed for the lightbox's lifetime, nothing behind the scrim keeps
   repainting on its own, so this blur has nothing to re-rasterize. */
.lightbox {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(24px, 5vh, 64px) clamp(16px, 4vw, 48px);
  opacity: 0; visibility: hidden;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}
.lightbox.open { opacity: 1; visibility: visible; transition: opacity 0.25s ease; }
.lightbox-scrim {
  position: absolute; inset: 0;
  background: rgba(10, 12, 16, 0.6);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}
body.dark-mode .lightbox-scrim { background: rgba(4, 5, 8, 0.72); }
.lightbox-panel {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; gap: 10px;
  width: min(720px, 92vw); max-height: 100%;
}
.lightbox-close {
  align-self: flex-end; flex: none;
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  padding: 9px 16px; cursor: pointer;
  box-shadow: 0 10px 26px -10px rgba(0, 0, 0, 0.5);
}
.lightbox-close:hover { background: var(--accent); color: #FFFFFF; border-color: var(--accent); }
.lightbox-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, 0.6);
  background: #0B0D11;
}
.lightbox-scroll img { display: block; width: 100%; height: auto; }
.lightbox-cap {
  flex: none;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.75); text-align: center;
}
body.lightbox-open { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .lightbox { transition: none; }
}
