/* ================================================================
   COMPONENT: COURSE STEPPER
   Stepped sequence (the carousel's hold → switch motion DNA):
   panels advance on a timed hold with a progress bar on the active
   step; click any step to take control (autoplay stops). Panels
   enter with the unfold grammar (slight rotateX hinge). No
   scroll-jacking — Lenis stays untouched.
   Driven by js/modules/course-stepper.js. HOLD_MS = 2600 must match
   the --cs-hold custom property below.
   ================================================================ */

.course-stepper {
  --cs-hold: 2600ms;   /* keep in sync with HOLD_MS in course-stepper.js */
  /* The plate's inset, as a variable because the rail's corner is DERIVED from
     it (see `--rail` below). Overridden at ≤768, where only this token moves
     and the padding follows — never write a second `padding` for the plate. */
  --cs-pad: var(--sp-6);
  max-width: 880px;
  margin: var(--sp-8) 0;   /* left-aligned — one reading axis with the grid */
  background: var(--surface-alt);
  border-radius: var(--radius-card);
  corner-shape: squircle;
  padding: var(--cs-pad);
}

/* THE RAIL'S PLATE WRAPS, SO IT DERIVES. Its corner is the card's plus its own
   inset — 16 + 24 = 40, and 16 + 20 = 36 on mobile — so all three arcs share a
   centre and the plate reads as holding the cards rather than sitting behind
   them. With the picture now flush to the card's edge, that is one concentric
   run through all three layers: plate 40 → card 16 → picture 16 at inset 0.

   This is the FRAME half of DESIGN.md → Two plate families. A plate you read
   (`.case-lede`) keeps `--radius-card`; a plate that wraps something derives,
   the way `.case-figure__mat` does. It holds for a prose rail too — the card
   is what the plate wraps, and a prose card wears the same 16 as a picture
   card. `corner-shape` already matches on all three — without that, a squircle
   inside a circular corner never hugs however the radii are set. */
.course-stepper--rail {
  border-radius: calc(var(--case-shot-radius, var(--radius-lg)) + var(--cs-pad));
}

.course-stepper__steps {
  position: relative;
  display: grid;
  list-style: none;
  padding: 0;
  margin: 0 0 var(--sp-6);
}

/* All panels share the same grid cell; the container sizes to the
   tallest, so switching never reflows the page. */
.course-stepper__panel {
  grid-area: 1 / 1;
  opacity: 0;
  transform: perspective(1200px) rotateX(-10deg) translateY(14px);
  transform-origin: 50% 0;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.course-stepper__panel.is-active {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* ── WHEN THE STEP CARRIES ITS OWN SCREEN ────────────────────────
   The intake loop was being told TWICE: once as text panels here, and
   again as three standalone figures (a buyer pair, a merchant pair, an
   error shot) sitting above and below this block. That cost ~2,800px of
   scroll and, worse, split every step in two — the reader met a screen,
   then met the sentence explaining it several hundred pixels later, and
   had to hold one in memory to use the other.

   Merged, a panel is [screen | explanation] and the sequence is told
   once. The block takes the CANVAS (948) rather than the reading column
   because a portrait screen plus a readable measure does not fit in 680:
   the extra 268 is exactly the image column, so the prose keeps its
   measure instead of being squeezed to make room.

   THE CANVAS IS FOR SCREENS, so `:has()` asks whether this rail actually
   carries any. A prose rail (japan, vision-next) has no image column to make
   room for, and widening it to 948 would push its steps out of the reading
   column for nothing — it stays in the 680 the rest of the page reads in. */
@media (min-width: 1025px) {
  .case-page .course-stepper--rail:has(.course-stepper__shot) {
    margin-left: 0;
    max-width: var(--case-canvas);
  }
}

/* ── THE RAIL IS CARDS, NOT ONE SWAPPING PANEL ──────────────────────
   Designed in Figma and implemented from it. The panel model showed one
   step at a time in a shared grid cell, which forced two compromises: the
   card was always as tall as the tallest step, and at any moment five
   sixths of the sequence was invisible. A rail fixes both — several steps
   are legible at once, the next one peeks to say the row continues, and
   the height is whatever a single card needs.

   IT IS ALSO WHY THIS ADAPTS. A fixed-basis flex rail with scroll snap
   needs no breakpoint to decide how many cards fit: the card width is a
   clamp, the container shows as many as it can, and the overflow scrolls.
   Three at 948, two on a tablet, one plus a peek on a phone — same rule.

   IT IS NOW ALL THREE CASE PAGES. Japan and Vision Next ran the swapping
   panel because their steps are prose with no screens to put in a rail —
   but the rail's argument never depended on the pictures. The objection
   that mattered was losing the nav's labels; it does not hold, because in
   this model each card carries its own `<h3>`, which is exactly why the
   rail dropped the labelled segments in the first place. A prose card
   simply has no `__shot`, and every picture rule keys off that element. */
.course-stepper--rail .course-stepper__steps {
  display: flex;
  gap: var(--sp-6);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* NO `scroll-behavior: smooth`. Measured on this page, a programmatic jump
     of three cards took over 1.8s to start moving — the browser's smooth
     scroll competing with Lenis's rAF loop for the same frames. The module
     tweens `scrollLeft` itself instead (course-stepper.js), which is one
     easing under one clock and lands in 0.48s. Putting this back re-opens
     the stall. */
  /* Bleed to the card's edge so a slide passes under the corner instead of
     stopping short of it, while the first card keeps its 24px inset. */
  margin-inline: calc(var(--sp-6) * -1);
  padding-inline: var(--sp-6);
  /* WITHOUT THIS THE SNAP AND THE SCRIPT DISAGREE. `scroll-snap-align: start`
     aligns a card to the SNAPPORT, which is the scrollport inset by
     scroll-padding — not by padding. Leave it unset and the snapport ignores
     the 24px inset, so the resting position of card 1 is `scrollLeft: 24`
     while the script computes 0, and every programmatic scroll gets nudged
     24px by the snap afterwards. Matching it to the padding makes the two
     agree and card 1 rest at a true zero. */
  scroll-padding-inline: var(--sp-6);
  scrollbar-width: none;
  overscroll-behavior-x: contain;   /* don't hand a horizontal fling to the page */
}

.course-stepper--rail .course-stepper__steps::-webkit-scrollbar { display: none; }

/* Only while a mouse is actually dragging (course-stepper.js). No `cursor`
   change — the custom cursor owns that — just the selection suppression,
   without which a drag across a caption highlights the text instead. */
.course-stepper--rail .course-stepper__steps.is-dragging { user-select: none; }

/* THE CARD. `--paper` on the `--surface-alt` plate: a raised sheet on a
   recessed one, which is a real distinction rather than a second skin —
   DESIGN.md → Two plate families.

   IT WEARS THE PICTURE'S CORNER, and that is the whole reason for the token.
   Three boxes nest here — plate, card, picture — and they used to read
   14 / 8 / 16: the innermost box was the ROUNDEST and the one between them the
   hardest, so the picture looked softer than the card holding it. `--radius-sm`
   belongs to small chrome; this card is a mount around a screen, so it takes
   the screen's own corner and the two agree at every breakpoint. `--radius-card`
   would NOT: it steps 16/14/12 while `--case-shot-radius` stays 16, which puts
   the inversion straight back at tablet and mobile.

   No `calc()` here on purpose — see the note above `__shot` for when to derive. */
.course-stepper--rail .course-stepper__panel {
  flex: 0 0 clamp(256px, 76vw, 388px);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  background: var(--surface-page);
  border-radius: var(--case-shot-radius, var(--radius-lg));
  corner-shape: squircle;
  padding: var(--sp-6);
  /* Every panel is visible now, so none of the swapping model applies: no
     shared grid cell, no opacity, no hinge. */
  grid-area: auto;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* A SQUARE WELL, AND THE SCREEN STANDS IN IT.
   The panel model needed a tall screen beside a paragraph; a card wants a lid
   over one. The lid is a dark ground with the phone standing on it, not a
   screenshot bled to the edges — designed in Figma ("course-stepper — YOUR
   EDITS", node 18:80) and measured off that render.

   THE LID REACHES THE CARD'S EDGES. The card's own 24px padding is cancelled
   on three sides, so the picture runs to the left, right and top borders and
   the text below keeps the inset. `width: auto` is what does the stretching —
   `100%` would resolve against the CONTENT box and stop 24px short on each
   side, negative margins or not.

   The inset is now ZERO, which is the one case where the concentric test and
   the ladder test give the same answer: outer = inner + 0, so the picture's
   top corners are the card's corners exactly — same 16, same squircle, one
   arc drawn twice. The bottom two go square because the picture does not end
   there, the card carries on into the copy.

   THE GROUND IS `--ink-900`, NOT THE FIGMA'S `#2d2d2d`. That value is this
   site's RETIRED neutral ink — `tokens.css` still carries the note, `--ink-900
   #2c2620 (was #2d2d2d)`. The whole ramp went warm because neutral greys read
   cold against warm paper, and a cold rectangle is exactly what a 388px lid
   would be. The two are 1.3:1 apart in contrast and indistinguishable side by
   side; only one of them belongs on this page. This is also NOT the mount
   experiment that was rejected on `.case-figure__mat` — that put ink around a
   picture as a frame; this is the ground a picture stands on, which is the one
   job a dark surface does well here. */
.course-stepper--rail .course-stepper__shot {
  aspect-ratio: 1;
  width: auto;
  margin: calc(var(--sp-6) * -1) calc(var(--sp-6) * -1) 0;
  border-radius: var(--case-shot-radius, var(--radius-lg))
                 var(--case-shot-radius, var(--radius-lg)) 0 0;
  background: var(--ink-900);
  position: relative;
}

/* THE PHONE IS A WINDOW, NOT THE WHOLE DEVICE — and that is a legibility
   decision, not a stylistic one. The Figma stands the entire screen in the
   well at 40.6% of its width. On the real 388px lid that renders a 1179px-wide
   capture at 12.5%, against 30.8% when the screenshot bled to the edges — and
   `case-viz.css` records what that cost used to buy: the URL bar reading
   `admin-test.pindo.info` on the merchant screens was the proof of the
   chapter's argument, that the warehouse tool is a web page on a phone the
   merchant already owns rather than a ¥10,000 scanner terminal. At 12.5% that
   domain is a blur and a demonstrated claim goes back to being an asserted
   one. At 62% it is 19% — still readable, still a phone on a dark ground.

   THE 2026-08-18 RE-EXPORT REMOVED THE CHROME, so there is no domain left to
   protect. **The 62% is not up for grabs on that account** — it is now held by
   the interface text on the same screens (the OCR chips, the keypad, the
   toggle pairs), which is the same legibility argument with a different
   subject. If you widen or narrow this, read those at 1x before shipping.

   `object-fit: cover` + `object-position` replaced the old `margin-top: -N%`
   slice hack. Same crop, but stated as a fraction of the SOURCE rather than of
   the image's rendered width, so it holds at every breakpoint with no media
   query and no re-tuning when the well resizes. Per-step values live in
   `case-viz.css`, with the page that owns the captures.

   IT IS POSITIONED, AND IT STATES BOTH LENGTHS. Two failures got it here, both
   worth knowing. As a centred GRID item, `height: calc(100% - 40px)` silently
   fell back to the image's intrinsic height — a percentage height needs a
   DEFINITE containing block, and a grid area inside an `aspect-ratio` box is
   not definite while its own items are being sized; it measured 481px in a
   388px well. Absolute positioning fixes that (the containing block becomes
   the well's padding box, which is definite) — but `inset` alone does NOT,
   because for an absolutely positioned REPLACED element `width: auto` resolves
   to the INTRINSIC width and the opposite offset is simply dropped. It went
   back to full width. So the box states both lengths explicitly and lets
   `left` place it; `19%` is the same 62% width expressed as what is left over,
   which also avoids a `translateX(-50%)` centring transform on a child of a
   rounded, clipping parent — the pattern that degrades the corner on mobile
   (`DESIGN.md → Radius`). */
.course-stepper--rail .course-stepper__shot img {
  position: absolute;
  top: var(--sp-5);
  left: 50%;
  transform: translateX(-50%);
  height: calc(100% - var(--sp-10));
  width: auto;
  border-radius: 13% / 6%;
}

/* The screen. Ratio lives here (one for every step); each step's own
   slice offset lives with the page that owns the captures. */
/* Same picture corner as every other screen on a case page. The fallback
   keeps the component usable off a case page, where the variable is unset.

   WHY THIS IS NOT `calc(inner + inset)` LIKE `.case-figure__mat` IS.
   Concentric corners only matter where the two curves OVERLAP — a parent's
   radius R only bends the outer R of its corner, so a child inset by more
   than R begins past the bend and the arcs never share a corner region. The
   mat derives because its inset (20) is smaller than its radius (36). Here
   every inset is larger than its parent's radius (24 inside 14, 24 inside 16),
   so the curves never meet and deriving would only inflate the boxes: strict
   concentricity wants a 40px card inside a 64px plate, which is a different
   component. The rule that applies at this spacing is the LADDER — corners
   must not invert going inward — and the fix belongs on the card, above.

   If a padding here ever drops below its parent's radius, that flips: derive
   then, the way the mat does.

   THE `--rail` VARIANT OVERRIDES BOTH VALUES BELOW. It takes a 1:1 well on
   `--ink-900` and stands the whole phone in it, so neither this ratio nor this
   stock is what you see on a case page today. They are what the ORIGINAL
   swapping-panel stepper used, and Japan still runs that model. */
.course-stepper__shot {
  aspect-ratio: 1260 / 2100;
  overflow: hidden;
  border-radius: var(--case-shot-radius, var(--radius-sm));
  corner-shape: squircle;
  background: var(--paper-tint-2);
}

.course-stepper__shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* The card IS the measure now. */
.course-stepper--rail .course-stepper__panel p { max-width: none; }

/* A DRAWN DEVICE FRAME — TRIED TWICE, STILL NOT HERE, AND NOW UNNECESSARY.
   The two passes were a bezel drawn over the square well, and a full-body
   phone at true proportions driving the card's height. The second took the
   card 490 → 755 on mobile; the first put a black device BODY — a material
   this warm-paper system does not otherwise own — around six screens that
   already read as screens.

   What ships now gets the same reading for neither cost: the whole capture
   stands inside the square well on `--ink-900`, so the phone's own rounded
   screen IS the device silhouette. No drawn bezel, no extra height — the well
   stays 1:1 and the phone is sized to fit it.

   The findings that survived, so none of it is re-derived:
   - EVERY capture carries a real iOS status bar, the merchant Safari screens
     included. A drawn dynamic island landed straight on the Safari URL bar,
     which is the one piece of evidence the chapter rests on.
   - A single percentage radius on a phone-shaped (2.16x) box gives ELLIPTICAL
     corners; it needs the two-value form — and `13% / 6%` is exactly the pair
     that resolves CIRCULAR at this ratio, which is what the shipped rule uses.
   - Standing the phone whole costs SCALE, not height: the screen renders at
     41% of its design size instead of filling the well. That is the trade the
     lightbox exists to cover. */

/* ── DOTS, for the rail only ─────────────────────────────────────────
   The rail shows several self-labelled cards at once — each carries its own
   `04. Weigh and classify` — so a second set of labels underneath would be
   the same information twice. Position is all that is left to report, and a
   dot reports position. The base variant keeps its labelled segments for the
   opposite reason: one panel visible, so the nav is the only place the whole
   sequence exists. */
/* Readout on the left, control on the right — the split the reference uses,
   and the one that stops a reader hunting for where to click. */
.course-stepper--rail .course-stepper__nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
}

.course-stepper--rail .course-stepper__dots {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* A DOT IS A READOUT, NOT A CONTROL. It is a `<span>` on this variant, which
   is why there is no hover here and why the row keeps its 18px pitch at every
   width — an indicator has no touch target to satisfy, so the phone no longer
   needs the 44px boxes that spread the row out. */
.course-stepper--rail .course-stepper__dot {
  flex: none;
  width: 10px;
  height: 10px;
  padding: 0;
  border-radius: 50%;
  background: var(--paper-tint-4);
  transition: background-color var(--dur-normal) var(--ease-out);
}

/* KILL THE BASE SEGMENT. `.course-stepper__dot::after` is the 2px progress
   track of the OTHER variant, pinned `left/right/bottom: 0`. Left alive under
   a 10px round dot it draws a flat bar across the dot's foot, and the pair
   reads as a dome, not a dot. Losing this one line is exactly what shipped
   that. If a dot ever changes shape again, check this override first. */
.course-stepper--rail .course-stepper__dot::after { content: none; }

/* ── ARROWS ─────────────────────────────────────────────────────────
   Chrome, so they FILL on hover — the same ink sweep as `.btn-pill` and
   `.navbar__link` (DESIGN.md → Two hover families). This is the variant that
   can carry it: unlike the old step pills, an arrow really is a button with a
   shape, so there is something to fill. */
.course-stepper--rail .course-stepper__arrows {
  display: flex;
  gap: var(--sp-2);
}

.course-stepper--rail .course-stepper__arrow {
  appearance: none;
  border: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface-page);
  color: var(--text-primary);
  display: grid;
  place-items: center;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transition: color var(--dur-slow) var(--ease-out);
}

.course-stepper--rail .course-stepper__arrow svg { width: 16px; height: 16px; }

.course-stepper--rail .course-stepper__arrow::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-color: var(--text-primary);
  /* THE HOUSE SWEEP, VERBATIM: a circle opening from the BOTTOM-RIGHT corner,
     always to 200%, always on `--ease-out` (DESIGN.md → Two hover families).
     This was `circle(0% → 75% at 50% 50%)`, which grew from the middle — a
     different gesture from every other fill on the site, and on a 44px button
     the difference reads immediately because the whole sweep is in view.
     200% rather than a fitted value is what keeps the leading edge moving at
     speed instead of decelerating into the far corner. */
  clip-path: circle(0% at 100% 100%);
  transition: clip-path var(--dur-slow) var(--ease-out);
  z-index: -1;
}

.course-stepper--rail .course-stepper__arrow:hover::before,
.course-stepper--rail .course-stepper__arrow:focus-visible::before {
  clip-path: circle(200% at 100% 100%);
}

.course-stepper--rail .course-stepper__arrow:hover,
.course-stepper--rail .course-stepper__arrow:focus-visible {
  color: var(--fill-contrast);
}

/* The label and index still exist for screen readers and for the base
   variant's nav; on a dot they are the accessible name, not decoration. */
.course-stepper--rail .course-stepper__dot-num,
.course-stepper--rail .course-stepper__dot-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* TWO STATES, NOT THREE. `is-past` used to take `--text-tertiary`, so the row
   ran dark / dark / teal / pale / pale / pale — three inks to say one thing.
   On the panel variant the inked-behind reading is the point (the segments
   are a rule filling left to right); on a LOOPING rail it is not, because the
   run wraps and "behind" resets every six cards. Where you are is the only
   fact left, so only the active dot carries ink and everything else is the
   same pale mark. `is-past` is still set in the JS — the panel variant needs
   it — it simply has no ink here. */
.course-stepper--rail .course-stepper__dot.is-active { background: var(--text-accent-ink); }

/* The autoplay fill has no room on a 10px dot, and the rail reports its own
   position by what is on screen. */
.course-stepper--rail .course-stepper__bar { display: none; }

/* WHOSE SCREEN IS THIS. Six steps cross between two apps — buyer, warehouse,
   warehouse, warehouse, buyer, buyer — and without the marker the reader has
   to infer it from a URL bar. It earns the mono register on the one test that
   matters (`DESIGN.md → The label register`): it names the ARTIFACT, not the
   sheet's own structure, the way chrome quoted inside a device mock does. It
   is also not a restated heading — "Warehouse" over *Photograph the label* is
   information the heading does not carry, which is the line the eighteen
   deleted eyebrows failed.

   Tertiary ink, not accent: `.case-cover__kicker` is the page's only accent
   label and this must not become a second one. Two values, ever. */
.course-stepper__side {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: var(--sp-2);
}

.course-stepper__panel h3 {
  font-family: var(--font-display);
  font-weight: var(--weight-regular);
  font-size: var(--type-case-component);
  /* Same tier, same fix as `.fold-compare__title`: `body`'s absolute 24px
     leading inherits verbatim and set this 24px title solid. */
  line-height: var(--leading-snug);
  color: var(--text-primary);
  margin-bottom: var(--sp-3);
}

.course-stepper__panel p {
  font-size: var(--type-body1-size);
  font-weight: var(--weight-book);
  line-height: 1.65;
  color: var(--text-body);
  max-width: 560px;
}

/* ── STEP NAV — labels on a track, not buttons in boxes ──────────────
   The pills carried `1px solid var(--border-mid)`, and `--border-mid` is
   `--gray-200`: a COOL grey on warm paper. That is the exact thing DESIGN.md
   removed from `.fold-compare__rows` (eight of them) and from `.case-meta`,
   so six more of them under the busiest component on the page were the last
   holdout of a rule the rest of the site already follows. Dropping the box
   also drops a second radius, a second fill and a second border colour from a
   card that is already carrying six screens.

   What replaces them is the language the page is built in: **a drawn line**.
   Every label sits on a warm hairline track; the active one INKS IN across
   its own segment, left to right, for exactly as long as that step holds. The
   progress indicator and the state indicator became the same object.

   This deliberately leaves the ink-sweep hover family (DESIGN.md → Two hover
   families: chrome fills, paper lifts). A fill needs a shape to fill, and
   there is no longer a shape — the control is text on a rule now, so its
   hover is the label taking ink. Do not put the pill back to justify the
   sweep; the sweep was a consequence of the box, not a reason for it. */
/* ONE RULE, DIVIDED IN SIX — the nav is a measuring rule, not a row of tabs.
   The first pass hung a hairline under each label, so the six tracks were six
   different widths with gaps between them: ragged, and it read as decoration
   under text rather than as a instrument. An EQUAL-COLUMN grid makes the
   track one continuous rule divided into six equal segments — which is what
   finally lets it report anything.

   Three states across that rule, and together they are the progress:
     past    inked, and it STAYS inked   — how far you have come
     active  inking in over its own dwell — where you are, and how long is left
     ahead   warm track                   — what is left

   Before this, only the active segment ever carried ink, so there was no
   reading of position in a six-step sequence — the complaint was exactly
   right. Cumulative ink is the fix, and it costs one class from the JS. */
.course-stepper__nav {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--sp-4) var(--sp-3);
}

.course-stepper__dot {
  appearance: none;
  border: 0;
  background: none;
  border-radius: 0;
  padding: 0 0 10px;      /* the only inset left: room for the segment */
  position: relative;
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--type-body2-size);
  font-weight: var(--weight-book);
  line-height: 1.35;
  color: var(--text-secondary);
  transition: color var(--dur-normal) var(--ease-out);
}

/* The segment. Warm (`--paper-tint-4`), never `--border-mid`.

   `z-index: 0` IS LOAD-BEARING, and its absence is what hid the running fill.
   `::after` is generated content, which the box tree treats as the element's
   LAST child, while `.course-stepper__bar` is written by the JS as its FIRST.
   Both are positioned, so with no z-index they paint in tree order and the
   warm track landed on top of the ink filling underneath it — the active step
   looked identical to an untouched one. Order them explicitly: track 0, fill
   1. Do not remove either value. */
.course-stepper__dot::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--paper-tint-4);
  z-index: 0;
  transition:
    background-color var(--dur-normal) var(--ease-out),
    height           var(--dur-normal) var(--ease-out);
}

/* HOVER HAS TO CHANGE SOMETHING YOU CAN SEE, AND COLOUR ALONE DOES NOT.
   Two attempts failed on this: tertiary→primary on the label (two warm greys),
   then tint-4→tint-5 on the segment (#d8d0bf→#cdc4b2, about 4% apart). Both
   are rounding errors at 14px and 2px respectively.

   The segment now GROWS — 2px to 4px, upward from a pinned bottom edge, so
   nothing reflows — and the label and its numeral both take ink at once. A
   change of SIZE is legible where a change of tone at this scale is not, and
   it works identically over all three states: an inked past segment, a
   filling active one, and a warm one ahead all thicken under the cursor
   without needing three separate hover colours. */
.course-stepper__dot:hover,
.course-stepper__dot:focus-visible {
  color: var(--text-primary);
}

.course-stepper__dot:hover::after,
.course-stepper__dot:focus-visible::after,
.course-stepper__dot:hover .course-stepper__bar,
.course-stepper__dot:focus-visible .course-stepper__bar {
  height: 4px;
}

.course-stepper__dot:hover .course-stepper__dot-num,
.course-stepper__dot:focus-visible .course-stepper__dot-num {
  color: var(--text-secondary);
}

/* The one ahead-of-us segment under the cursor also darkens, which is the
   only state where tone still has room to move. */
.course-stepper__dot:not(.is-past):not(.is-active):hover::after,
.course-stepper__dot:not(.is-past):not(.is-active):focus-visible::after {
  background: var(--paper-tint-5);
}

/* PAST — stays inked. This is the half that makes the row a progress rule. */
.course-stepper__dot.is-past::after {
  background: var(--text-primary);
}

.course-stepper__dot.is-past { color: var(--text-tertiary); }

.course-stepper__dot.is-active { color: var(--text-primary); }

/* THE INDEX IS MACHINE TEXT; THE LABEL IS PROSE. They were reading as one
   thing because they differed only by 3px and a shade of the same warm grey.
   Now they differ on four counts at once: the numeral is zero-padded (`01`,
   written by the JS), mono, tracked at the label register's 0.08em, and
   tabular — while the label stays body grotesk, untracked, 14px. Mono labels
   machine text and does not label the sheet's own structure (DESIGN.md → The
   label register); a step index is exactly the former. */
.course-stepper__dot-num {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  margin-bottom: 7px;
  color: var(--text-tertiary);
  transition: color var(--dur-normal) var(--ease-out);
}

/* The accent lands on the step you are on, and nowhere else. Teal on six
   labels at once was decoration; on one it is a position. */
.course-stepper__dot.is-active .course-stepper__dot-num {
  color: var(--text-accent-ink);
}

/* Autoplay progress — the track inking in. Duration is written by the JS when
   a picked step holds longer than the default, so the line always reports the
   real dwell rather than a fixed guess. */
.course-stepper__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--text-primary);
  transform: scaleX(0);
  transform-origin: 0% 50%;
  z-index: 1;            /* above the track — see the note on `::after` */
  pointer-events: none;
  /* Matches the track so the pair thicken together under the cursor. Height
     only — never transition `transform` here, the keyframes own it. */
  transition: height var(--dur-normal) var(--ease-out);
}

.course-stepper__dot.is-active .course-stepper__bar.is-running {
  animation: cs-progress var(--cs-hold) linear 1 forwards;
}

@keyframes cs-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* Reduced motion: no autoplay (JS), no hinge — simple fade */
@media (prefers-reduced-motion: reduce) {
  .course-stepper__panel {
    transform: none;
    transition: opacity var(--dur-normal) linear;
  }
  .course-stepper__bar { display: none; }
}

/* -- Responsive ---------------------------------------------------- */
@media (max-width: 1024px) {
  /* The rail has collapsed, so the canvas IS the reading column and the
     image column no longer has anywhere to come from. 300px keeps the
     screen legible while leaving the prose a workable measure. */
  /* Nothing for the rail here any more: it is a column flex box, so the
     `grid-template-columns` this rule carried never applied, and its 24px gap
     was `--sp-6` restated. */
  /* Six segments across ~630px gives each label ~90px, which wraps
     "Weigh and classify" to three lines. Two rows of three keeps the rule
     readable as a rule. */
  .course-stepper__nav { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 768px) {
  /* Only the token moves — `padding` reads it, and so does the rail's derived
     corner, which drops to 16 + 20 = 36 in step with it. */
  .course-stepper { margin: 40px auto; --cs-pad: 20px; }
  /* TOUCH TARGET, GROWN UPWARD. Losing the pill took the dot to 41px — under
     the 44px WCAG floor this site holds itself to. The padding goes on TOP so
     the extra area is invisible (there is no background to give it away) and
     the track stays tight under its label instead of drifting 14px below it. */
  .course-stepper__dot { padding: 12px 0 9px; }
  /* Two columns, three rows: at 343px even three segments would put every
     label on two lines and the rule would stop reading as one instrument. */
  .course-stepper__nav { grid-template-columns: repeat(2, 1fr); gap: 12px; }

  /* The dots need no mobile rule at all now — they are 10px indicators, not
     touch targets, so the row keeps its designed pitch here too. The arrows
     are the controls and they are already 44. */
  /* Side by side, the screen would be ~150px — an illustration of a screen
     rather than a screen. Stacked, it gets the full 303px measure, and the
     step reads in its natural order: look, then read. */
  /* `grid-template-columns` was dropped from this rule: the card is a column
     FLEX box, so it never did anything here. The gap did — keep it. */
  .course-stepper--rail .course-stepper__panel { gap: 16px; }

  /* `.course-stepper__shot { max-width: 260px }` was deleted here. It sized a
     screen stacked above a paragraph in the full-width panel, and the two
     remaining panel steppers (japan, vision-next) carry no shot at all — so
     its only live subject was the rail, where it capped a box already at most
     340px wide. From ~416px up it bound before the card did and left as much
     as 80px of bare paper down the right of every image: the card filled, the
     picture did not. On the rail the CARD is the frame. */
}
