/* intro.css — section 2: black, and the "Intro" line sidebar. Spec: 156.

   ⚠️ REWRITTEN FROM SCRATCH 2026-09-03 (156 §1.1, §2, Xinlu's decision). The
   434-line sheet that stood here described the character video, the glow, the
   three copy panels, the travelling wash and the unfold/tilt composition. All
   of it went with js/sections/intro.js and both assets/dist/intro-portrait-*.
   Nothing was carried across: 000 §5's rule is remove, do not stub, and a
   half-kept sheet describing a section that no longer exists is exactly the
   stale-comment failure 000 §8 catalogues.

   Same path deliberately — index.html's <link> is unchanged, so the deletion
   and the replacement do not churn the head.

   ── THE DIVISION, WHICH IS THE SITE'S AND NOT THIS FILE'S ──────────────────
   JS feeds numbers, CSS owns appearance. rail.js writes exactly three custom
   properties — --effect per item, --rail-shift once, --rail-glow — and every
   colour, length and transform below is derived from them here. The one place
   that division is load-bearing rather than tidy is --rail-shift: see §6.2 and
   the block above .rail__num.

   ── THE RIGHT-HAND HALF IS NOT BUILT (156 §12, Xinlu) ──────────────────────
   Desktop puts the rail on the left and leaves the right empty; there is no
   markup for the cards and none is anticipated here beyond the split itself. */

#intro {
  background: #000;

  /* One source of truth for the mobile gutter: the padding below AND the
     divisor in .rail__item's width both read it, so the five ticks cannot
     drift out of the space the section actually leaves them. */
  --rail-pad: clamp(16px, 5vw, 40px);

  /* .section is a centred flex row (layout.css:10). Desktop puts the rail hard
     left, mobile puts it at the bottom — 156 §1.2, "left ≡ bottom". Both are
     overrides of that one rule rather than a second layout system. */
  justify-content: flex-start;

  /* 158 §3 — the left inset is a DIAL now (?railx=), because it is the thing
     Xinlu wanted changed and 156 gave him no way to change it. Unitless, times
     1vw, so the number in the URL reads as "% of the viewport width" and does
     not have to be re-derived from a clamp(). The old rule was
     `clamp(24px, 7vw, 128px)`, which measured 100.8 px = 7.0 % at 1440 and
     pinned to 128 px on anything wider — so it stopped tracking the viewport
     exactly where a wide screen most needs it to. */
  /* ⚠️ 6, NOT 10, AND IT SUPERSEDES 168 §4 (187 §1). Xinlu, 2026-09-06, having
     tested ?railx=6 on s12-cards-c1: "After applying it, the distance between
     the linebar and the cards, as well as the overall page layout, look good —
     we can adopt it." Approved the way this project's values are approved, by
     looking at the built thing on his own hardware. DO NOT RE-PROPOSE 10.

     ⚠️ WHAT IT ACTUALLY DOES IS NOT OBVIOUS. The composition is capped at the
     reference frame and centred above it (184 §3), so --rail-x sets the
     proportions INSIDE that frame, not the page margins. At the 1440 frame it
     gives 86px margins and a 199px gap; centred at his 2063px window that reads
     as 19.3% page margins, against the reference site's measured 19% (185 §1).
     The same derivation at 10 predicts 22.08% and 185 §1 measured 22.10% — the
     arithmetic control that makes the prediction for 6 trustworthy.

     ⚠️ RE-CHECK AFTER THE COPY SESSION. The rail is 382.9px wide because of
     `PLACEHOLDER ITEM THREE`, 22 characters against the reference's 17. Shorter
     real titles widen the gap beyond 199px on their own and 6 may then be too
     generous. 185 §1.2 is the write-up; it is a note for that session, not a
     task for this one.

     ⚠️ THE DEFAULT LIVES IN TWO PLACES ON PURPOSE — here for the frame before
     rail.js runs and for the ?rail=0 path, and in rail.js's DEFAULTS.x. They
     must be changed together; C2 is what catches them disagreeing. */
  --rail-x: 6;

  /* ⚠️ ONE EXPRESSION, THREE READERS (184 §3). #intro's padding, and the `right`
     of .intro__cards and .intro__glow in intro-cards.css, all read this. They
     have to agree exactly or the block's right edge stops sitting on the rail's
     inset mirrored — which is C2, and C2 is what catches them drifting apart.

     ⚠️ THE max() IS THE "CAP AND CENTRE" HALF OF 184 §3, and it is the half
     that scaling alone does not fix. Xinlu: "when I widen the browser, the
     distance between the cards and the linebar gets larger and larger."
     Capping ?cardmaxscale= stops the composition GROWING; it does not stop the
     viewport growing around it, so above the reference width the inset takes
     over and centres the composition in a --intro-max box instead.

       W  = 1920  ->  (1920 - 1440)/2 = 240  against 10vw = 192  -> 240, centred
       W  = 1440  ->  (1440 - 1440)/2 = 0    against 10vw = 144  -> 144
       W  = 1280  ->  negative                against 10vw = 128  -> 128

     Below the reference the max() resolves to --rail-x and nothing changes, so
     ?railx= still moves both sides together exactly as it did. */
  --intro-inset: max(calc(var(--rail-x) * 1vw),
                     calc((100% - var(--intro-max, 1440px)) / 2));
  padding: 0 var(--intro-inset);
}

.intro__rail {
  text-align: left;
  max-width: 100%;

  /* ⚠️ POSITIONED AND RAISED, AND THAT IS REQUIRED BY 181 §3's GLOW MOVE, not
     a style choice. .intro__glow is now an absolutely positioned sibling
     spanning the whole section with no clip, so it paints ABOVE static content
     unless the rail is lifted out of its way. Without this the accent field
     washes over the rail's own titles. C13's hit test would not catch it —
     .intro__glow is pointer-events: none, so the rail stays clickable while
     looking wrong. Only a pixel check sees it, and that is C19b in 182. */
  position: relative;
  z-index: 1;

  /* ── THE MOBILE SLIDE (158 §4.3) ─────────────────────────────────────────
     The rail translates in from the left AS ONE RIGID BLOCK — every part
     moving together, never a progressive wipe. So the transform is here, on
     the wrapper, and nothing inside it knows the slide exists: the push, the
     proximity colour and the marker's scale all stay in the item's own frame
     and compose with this by being nested inside it.

     ⚠️⚠️ --rail-slide DEFAULTS TO 1 — THE FINAL POSITION — FOR THE SAME REASON
     --rail-glow DEFAULTS TO 1 (158 §4.4, 156 §6.4, 036 §7.5). Under
     prefers-reduced-motion scroll.js creates no ScrollTrigger at all, so
     nothing writes this. A default of 0 would park the rail permanently off
     the left edge: a reduced-motion visitor would get an empty black section
     and no error anywhere. 1 is a stated value, not a resting accident, and
     rail.js ALSO writes it explicitly on that path.

     --rail-travel is DERIVED, never typed: rail.js measures the wrapper's own
     right edge and writes exactly enough to clear the viewport. Typing a vw
     literal here would be right at one width and wrong at every other, which
     is 156 §6.2's mistake in a different costume. The 100vw fallback is only
     for the frame before init runs and is deliberately over-generous. */
  --rail-slide: 1;
  --rail-travel: 100vw;

  /* ⚠️ THE RAIL SCALES WITH THE COMPOSITION, AND 184 §2 SAYS TO SAY SO PLAINLY.
     168 §4's approved values — pitch 64, the type sizes, the marker geometry —
     are now the values AT ?cardref= (1440) and are multiplied everywhere else.
     They are not being re-proposed and not being overruled; they are being made
     proportional, which is what Xinlu asked for when he chose "everything
     shrinks together". 183 §1 is why it has to include the rail: the gap is the
     only elastic part of a row that also holds two rigid ones, and scaling the
     cards alone moves the break-even from 1336 to ~1183 and no further.

     ⚠️ transform-origin: left center IS LOAD-BEARING. The rail's left edge sits
     on #intro's content-box left edge and 170 §1.5 derives the cards' right
     inset from exactly that — scaling about the centre would walk it inward and
     C2 would go red.

     ⚠️ ONE transform, SLIDE THEN SCALE. The mobile slide was already here; the
     scale composes with it rather than replacing it. Rightmost applied first,
     so the rail is scaled about its left edge and then translated. */
  transform-origin: left center;
  transform:
    translateX(calc(var(--rail-travel) * (var(--rail-slide) - 1)))
    scale(var(--intro-scale, 1));
}

/* ⚠️ THE HEADING IS WHITE AND STATIC, AND THAT IS A DECISION (156 §1.14).
   It must not read --rail-accent, must not read --effect, and must not sit
   inside any selector that does. It is the one element in this section whose
   colour never moves — C-R9 asserts rgb(255, 255, 255) on all three accent
   flags, which is the arm that would catch it being folded into the rail's
   colour rule by a later tidy-up.

   Blinker per Xinlu's standing rule of 2026-09-03 (156 §1.12): Blinker for
   titles, Poppins for body, everywhere on this site unless he says otherwise.
   'Blinker Subset' is declared in css/fonts.css, which is linked first — see
   that file's header for why it is not declared in a section sheet. */
.intro__title {
  font-family: 'Blinker Subset', system-ui, -apple-system, sans-serif;
  font-weight: 400;
  /* 158 §3, ?railtitle=. Was clamp(1.5rem, 3.2vw, 2.5rem), which measured
     2.5rem / 40 px at 1440 and 1.5rem / 24 px at 390 — the clamp's two ends,
     never the vw term, on either device anyone actually uses. So it was two
     literals wearing a formula, and replacing it with one dial per platform
     loses nothing and gains a number Xinlu can move. Defaults are those two
     values +20 %: 3rem / 48 px and 1.8rem / 28.8 px. */
  font-size: var(--rail-title, 3rem);
  line-height: 1;
  letter-spacing: 0.01em;
  color: #fff;
  margin: 0 0 clamp(18px, 3vh, 40px);
}

/* ── THE RAIL ──────────────────────────────────────────────────────────────
   Every one of these is a dial in 156 §10 and rail.js writes the ones a flag
   moves. They live on .rail rather than on :root so ?rail=0 removing the
   element takes them with it, and so nothing else on the page can read them
   by accident — the trap intro.css's old header recorded about --scan-edge
   colliding with the hero's band. */
.rail {
  --rail-pitch: 64px;        /* centre-to-centre, was 88 (158 §3); ?railpitch= */

  /* ⚠️ THE TAP TARGET, AND IT IS NOT THE PITCH (160 §1, C-3). 160 §1 reasons
     that "the item's own text is ~16px tall, so the pitch IS the hit height."
     Measured, it is not: .rail__item is the element the pitch sizes, and
     .rail__btn inside it is only as tall as its content — 15.6 px, at pitch 64
     AND at pitch 48 AND at every other pitch. The spacing between targets was
     changing; the targets themselves never did.

     So the floor is given a mechanism instead of resting on that reasoning.
     min-height 44 makes the button itself the 44 px target, and 44 then
     becomes exactly the pitch at which two adjacent targets stop overlapping:
     at 48 they clear by 4 px, at 44 they touch, below 44 they overlap and a
     tap near a boundary opens the wrong card. rail.js clamps ?railpitch= there
     on the mobile branch and says why.

     ⚠️ It moves no pixel anyone can see, on either platform. .rail__item is
     `align-items: center`, so a taller button stays centred and the marker,
     number and label render exactly where they did — C-9 asserts the desktop
     geometry is byte-identical to 159's. */
  --rail-hit: 44px;
  --rail-marker: 60px;       /* marker length at scale 1; ?railmarker= */
  --rail-grow: 0.5;          /* scale span: rest 0.7 -> hover 1.2 */

  /* ⚠️ DERIVED, NOT TYPED (156 §6.2, §9). rail.js overwrites this at init with
     --rail-marker x --rail-grow and re-derives it on resize. The literal here
     is the value that pairing gives at the shipped defaults (60 x 0.5 = 30px)
     and exists ONLY so the sheet renders correctly for the one frame before
     init runs and on the ?rail=0 path. If you ever find yourself editing this
     number to change the push, you are editing the wrong thing: change
     --rail-marker and the shift follows. C-R3's second arm exists to catch
     exactly that mistake. */
  --rail-shift: 30px;

  --rail-accent: var(--accent);   /* 156 §1A — #6E7FB8, the bar's dots */
  --rail-target: var(--rail-accent);
  --rail-lift: 0.35;              /* ?railhover=lift — toward white */
  --rail-rest: #c4c4c4;           /* the reference's resting near-white */
  /* ⚠️ 0, NOT 0.35, AND IT IS A CHANGE OF KIND (158 §2.1). The rail now starts
     FULLY INVISIBLE rather than dim. That is stronger than the "starting
     dimness should not be too dark" Xinlu described on 2026-09-03, and he has
     approved the stronger version by looking at it on hardware — recorded as a
     deliberate replacement of the earlier description, not as a number that
     drifted.

     ⚠️ AND IT MAKES THE FAIL-SAFE STRONGER, NOT WEAKER (158 §2.2). opacity is
     dim + (1 - dim) x glow, so at dim 0 it is just glow — and --rail-glow's
     literal below is 1. A build whose writer dies still renders a BRIGHT rail,
     never an invisible one. That is the direction this has to fail in, and
     C-T3 asserts it by killing the writer. */
  --rail-dim: 0;                  /* ramp floor; ?raildim= */

  /* ⚠️⚠️ DEFAULTS TO FULL, AND THE DEFAULT IS THE WHOLE POINT (156 §6.4).
     Under prefers-reduced-motion: reduce, scroll.js creates NO ScrollTrigger
     at all — MEDIA has no reduce branch — so nothing writes this property. A
     ramp that never runs would leave the sidebar at --rail-dim permanently,
     which is a bar at 35% opacity forever and looks exactly like a bar that
     rendered correctly.

     This is the cue's bug, one file family over. --cue-opacity had to be
     written explicitly on the reduce path AND defaulted in hero.css, "stated
     in both places on purpose (036 §7.5)"; the pair was deleted together on
     2026-09-03 when the fade went (145 §5.1), not because it stopped being
     the right shape. So: the literal is here, AND rail.js writes 1 explicitly
     on the reduce path and on mobile. C-R5 asserts the computed value. */
  --rail-glow: 1;

  /* ⚠️ BRIGHTNESS AND HUE ARE INDEPENDENT AXES (156 §6.4). The ramp is
     opacity on the whole nav; the proximity effect is colour per item. They
     compose because they touch different properties — put the ramp on colour
     and hovering would fight it. */
  opacity: calc(var(--rail-dim) + (1 - var(--rail-dim)) * var(--rail-glow));

  /* ⚠️ pan-y, NOT none (156 §7). A vertical scroll that starts on the rail
     must scroll the page; taps still register. Third time this site has made
     the choice and the rule that decides it is what the object is FOR: the
     badge takes the gesture because dragging it IS the interaction; the strip
     and this bar are things you scroll past. C-R7 tests both arms. */
  touch-action: pan-y;
}

/* ?railhover=lift (156 §6.5, §1A.2). The mix target becomes a tint of the
   accent, so an item gains hue AND brightness instead of hue alone. Expressed
   as a colour-mix of the accent rather than a second hex, so it follows
   ?railaccent= instead of silently disagreeing with it. Measured at the
   shipped accent: #6E7FB8 lifted 35% is #a1acd1, 9.337:1 on black against the
   accent's own 5.391:1 and the resting 12.040:1 — better, still not brighter
   than rest. 156 §1A.2 is explicit that no tint of this hue can be. */
.rail.is-lift {
  --rail-target: color-mix(in srgb, #fff calc(var(--rail-lift) * 100%), var(--rail-accent));
}

.rail__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Pitch is the item's height, so centre-to-centre IS --rail-pitch by
   construction. rail.js still MEASURES it from the rendered list rather than
   reading this value (156 §6.1) — same move as the strip's unit width: a
   measured number survives a font swap, a zoom and a media query that a read
   CSS value does not. */
.rail__item {
  height: var(--rail-pitch);
  display: flex;
  align-items: center;
}

/* ⚠️ THE SIZE LIVES ON THE BUTTON, NOT ON .rail__text, AND THAT IS WHAT MAKES
   "numbers stay at 0.85em of the label" TRUE (158 §3). .rail__num and
   .rail__text are SIBLINGS, so an em on the number resolves against their
   shared parent — put the dial on .rail__text and 0.85em would silently mean
   0.85 of whatever .rail__btn inherited instead. One font-size here, 1em and
   0.85em below, and the ratio is structural.

   ⚠️ The previous ratio was NOT constant across viewports: 12/15 = 0.80 on
   desktop and 12/13 = 0.923 on a phone, because the label was a clamp() and
   the number a literal. 0.85 on both is a small deliberate change to each. */
.rail__btn {
  display: flex;
  align-items: center;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  /* ⚠️ ORDER MATTERS AND IT COST A ROUND TRIP. `font` is a SHORTHAND and resets
     font-size to the inherited value, so the dial has to come after it or the
     label silently renders at 16 px whatever ?railsize= says. Measured: 16 px
     with the two lines the other way round, 21.6 px with them this way. */
  font: inherit;
  font-size: var(--rail-size, 1.35rem);
  color: inherit;
  min-height: var(--rail-hit, 44px);   /* the tap target — see .rail above */
  cursor: pointer;
  text-align: left;
  position: relative;
}

/* ── THE TAP FLASH (164 §1) ────────────────────────────────────────────────
   Tapping an item flashed a filled rectangle over it on Xinlu's phone. He
   measured #0c5166 off the screenshot — a translucent cyan over black.

   ⚠️ IT IS CHROME'S OWN DEFAULT, WHICH 164 §1 RULED OUT ON A WRONG PREMISE.
   §1 says Chrome Android's default tap highlight "is a dark grey and would be
   invisible on this background", so a cyan had to be a Samsung system accent
   or a text selection. Measured on 163's build, the computed value of
   -webkit-tap-highlight-color on this element is:

       rgba(51, 181, 229, 0.4)

   which is #33b5e5 at 40 % alpha — the Android platform blue, and a cyan.
   Over #000 it composites to #14485c against Xinlu's #0c5166: green and blue
   normalise to 0.783/1.000 against 0.794/1.000, red to 0.217 against 0.118.
   Same hue family, same alpha, a redder screenshot — a themed accent or the
   phone's colour pipeline shifting one channel. The mechanism is the default.

   ⚠️ SELECTION IS RULED OUT, SEPARATELY AND BY MEASUREMENT. A trusted tap and
   a 900 ms long press on the button both leave window.getSelection() with
   rangeCount 0 and an empty string, on 163's build. `user-select: none` is
   here because 164 §1.2 asks for both guards and because every other
   interactive element on the site carries it — bar.css:189, strip.css:54,
   badge.css:62 — not because it is what stopped the flash. It is not.

   ⚠️ AND THE ATTRIBUTION IS BY THE PROPERTY, NOT BY WATCHING IT STOP. Headless
   Chrome COMPUTES the highlight and does not PAINT it: six screenshots of the
   button while a trusted touch was held are byte-identical to the resting
   frame. C-1a's pixel arm therefore cannot fail on 163 either, which 164 §3
   correctly says would make it worthless — so it is reported as unusable and
   the computed value is the arm that discriminates. 165 §1.

   ⚠️ 164 §1.1's TABLE IS WRONG ABOUT THE STRIP: it has user-select
   (strip.css:54-55) and NO tap-highlight-color. And it omits the badge, which
   has both plus -webkit-touch-callout. The headline claim survives — the rail
   was the only interactive element missing BOTH — but the strip is missing one.

   164 §1.3: this leaves a tap with no visual acknowledgement until the cards
   exist. That is deliberate. Do not invent a substitute. */
.rail__btn {
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
}

.rail__btn:focus-visible {
  outline: 2px solid var(--rail-target);
  outline-offset: 6px;
}

/* ⚠️ COLOUR IS THE SIGNAL, AND IT IS ONE RULE FOR ALL THREE PARTS (156 §5).
   The marker paints with currentColor so it moves with the text rather than
   needing its own copy of the mix. */
.rail__marker,
.rail__num,
.rail__text {
  color: color-mix(in srgb, var(--rail-target) calc(var(--effect, 0) * 100%), var(--rail-rest));
}

/* ⚠️ transform-origin: left center IS LOAD-BEARING, NOT A STYLE CHOICE.
   156 §6.2's gap identity assumes the marker grows from its anchored end:
       gap(e) = markerGap + 0.3 x markerLength + e x (maxShift - grow x markerLength)
   Scale it about its centre and the tip advances at half the rate, the label
   outruns it, and the push stops reading as a push. Measured before it was
   written: at markerLength 60 / shift 30 the gap holds at 18.000 px across
   e = 0, 0.25, 0.5, 0.75, 1; at markerLength 80 with the shift TYPED as 30 it
   walks 24 -> 14, a spread of exactly grow x L - shift = 10 px. */
.rail__marker {
  flex: none;
  width: var(--rail-marker);
  height: 1px;
  background: currentColor;
  transform-origin: left center;
  transform: scaleX(calc(0.7 + var(--rail-grow) * var(--effect, 0)));
}

/* ⚠️ BOTH SLIDE, BY THE SAME DERIVED AMOUNT, AND THAT IS WHY ?railnum=0 IS
   SAFE. 156 §6.2 talks about "the label", which in this markup is two
   siblings. Translating them together means the gap the check measures — from
   the marker's right edge to whichever of them comes first — is governed by
   one number whether the indices are shown or hidden. Translate only .rail__text
   and hiding the index would silently change which element forms the gap. */
.rail__num,
.rail__text {
  transform: translateX(calc(var(--rail-shift) * var(--effect, 0)));
}

/* 156 §5, and stated as a decision rather than a default per 000 §8: a system
   monospace stack, PAYLOAD ZERO. The face differs slightly between a Mac, a PC
   and a phone; for two digits at this size that is a few pixels, and it buys
   not adding a third self-hosted family to a site whose whole font budget is
   18,624 bytes for three faces (fonts.css). */
.rail__num {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;          /* of .rail__btn — see the block above */
  letter-spacing: 0.04em;
  margin-left: 18px;
  flex: none;
}

/* Poppins for body, Xinlu's standing rule of 2026-09-03 (156 §1.12). */
.rail__text {
  font-family: 'Poppins Subset', system-ui, -apple-system, sans-serif;
  font-weight: 300;
  font-size: 1em;             /* of .rail__btn — see the block above */
  letter-spacing: 0.02em;
  line-height: 1.2;
  margin-left: 12px;
  white-space: nowrap;
}

.rail.is-nonum .rail__num { display: none; }

/* ── MOBILE: THE SAME RAIL, PLACED LOWER (158 §4.2) ────────────────────────
   767px matches strip.css and scroll.js's BREAKPOINT_PX - 1.

   ⚠️ 156 §7's ROTATED LAYOUT IS GONE, AND THIS IS WHAT REPLACES IT. There is
   no second layout any more: the rail is vertical on both platforms, with the
   same markup, the same push, the same couplings and the same type dial. Only
   its position in the section differs — lower half instead of centre-left, and
   "Intro" is already above it in source order on both. One layout in two
   positions is less code than 157 shipped, not more, and there is no rotated
   frame for a check to have to project into.

   157 §5 is why it went: at 45 degrees on a 390px phone a title fitted SIX
   characters, and 22 only at 70 degrees, which is nearly vertical. The layout
   was never going to survive real copy. That reading is preserved in 159 for
   the copy session even though the layout it measured no longer exists. */
@media (max-width: 767px) {
  #intro {
    align-items: flex-end;
    /* The lower half, left side. The bottom padding is what places it; the
       left inset is ?railx= exactly as on desktop, so one dial moves both. */
    padding-bottom: clamp(32px, 6vh, 72px);

    /* ⚠️ 10, NOT 6, AND IT MUST MATCH rail.js's DEFAULTS.x.mobile (196 §7.1).
       6vw is 86px at 1440 and 22px at 360 — a margin on one screen and a
       rounding error on the other. C2 compares this literal against the JS
       default on BOTH platforms now, so the pair above and the pair here drift
       together or not at all. ?railx= still overrides both: rail.js writes
       --rail-x onto #intro at :300 and an inline custom property beats this. */
    --rail-x: 10;
  }
}

/* ── REDUCED MOTION (156 §5, §6.3) ─────────────────────────────────────────
   ⚠️ TRANSFORMS ONLY, NEVER THE COLOUR. The hue change is not motion; the
   slide and the marker's growth are. The colour rule above is deliberately
   outside this block, and C-R5 asserts that proximity still moves the
   computed colour under `reduce`.

   ⚠️ THIS IS NOT 156 §5's SNIPPET AND THE DIFFERENCE IS DELIBERATE — REPORTED
   IN 157. That snippet is `.rail__btn, .rail__marker { transform: none }`,
   and applied here it does two wrong things:

     1. ⚠️ GONE, AND KEPT ONLY AS A RECORD (169 §2.2, 171 §1). This read
        ".rail__btn CARRIES THE MOBILE ROTATION (§7)" — five 20-character
        labels laid flat on one rule. There is no rotation: 156 §7's rotated
        layout went at 158 §4.2, said at :367 above, and .rail__btn carries no
        transform at all (the sheet's only three are :75, :324 and :335). So
        this reason no longer holds and the decision does not rest on it.
     2. `transform: none` on .rail__marker snaps the tick from its resting
        scaleX(0.7) to 1 — the marker gets LONGER under `reduce` than at rest
        under no-preference. A correctly-suppressed effect should leave the
        resting frame untouched; this one redraws it.

     ⚠️ REASON 2 CARRIES THE DECISION ON ITS OWN and the code is unchanged.
     Only the justification above it was stale.

   Zeroing the two DIALS instead suppresses exactly the two animated
   quantities, leaves the resting geometry identical between the two media,
   keeps 0.7 in one place, and needs no branch in rail.js: the shift is
   DERIVED as --rail-marker x --rail-grow, so a grow of 0 makes the derivation
   itself produce 0 px. 156 §6.3 asks for a value rather than a behaviour —
   the values are --rail-grow: 0 and --rail-shift: 0px, and C-R5 prints the
   computed transform of both elements under both media. */
@media (prefers-reduced-motion: reduce) {
  .rail {
    --rail-grow: 0;      /* the tick rests at scaleX(0.7) and never grows */
    --rail-shift: 0px;   /* ...so the derivation yields 0 and nothing slides */
  }
  /* 158 §4.4 — the rail sits at its FINAL position. Stated as a value
     (translateX(0)) rather than left to whatever the first frame produced,
     036 §7.5. --rail-slide: 1 is already the default above, so this is the
     second of the two places that fact is written; rail.js writes it a third
     time on the reduce path, and C-T7 reads the computed transform rather
     than any of the three declarations. */
  .intro__rail { --rail-slide: 1; }
}

