/* ═══════════════════════════════════════════════════════════════════════════
   OYT LANDING v3 — THE STAGE
   Run 04. The dark middle of the page: five acts on one continuous ground,
   between the cream hero above and the cream pricing below.

   THE PIN IS CSS, NOT SCRIPT. `position: sticky` on the act frame is what
   holds the stage still while the page scrolls past it; ScrollTrigger's job
   here is only to SCRUB — to turn scroll position into timeline progress.
   That split is deliberate and it is what makes the run's two hardest gates
   reachable at all:

     * CLS 0.0000. ScrollTrigger's own `pin` wraps the pinned element in a
       generated .pin-spacer at script time, which is after first paint on a
       deferred script. A wrapper appearing under painted content is a layout
       shift by definition. A sticky element authored in the stylesheet has
       already laid out by the time anything paints, and the document height
       is the same byte-for-byte whether or not a line of JS ever runs.
     * The reduced-motion and mobile paths. Both are `@media` conditions here,
       so the stacking, the sticky and the track heights simply do not exist
       for them. Nothing has to be undone by script, which means nothing can
       be left half-undone.

   THE PIN LENGTH IS ONE NUMBER. --stage-screens below is the whole stage in
   viewport-heights; every track length is a fraction of it, and oyt-stage.js
   reads the same custom property off :root rather than keeping its own copy.
   Tune the stage by changing that one value.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── THE ONE CONSTANT ──────────────────────────────────────────────────
     Total viewport-heights of DOCUMENT the stage occupies, end to end, from
     the top of act 1 to the bottom of the cream handoff. The four fractions
     below sum to exactly 1 and are the only place the acts' relative weight
     is stated. Change --stage-screens and everything follows, including
     oyt-stage.js, which reads this property rather than keeping a copy.
     WHY 36 AND NOT THE 26 RUN 04c ASKED FOR — and this time the gap is large
     enough that it needs the arithmetic written out rather than a footnote.

     A STICKY PIN COSTS ONE VIEWPORT-HEIGHT. A track N screens tall holds its
     frame still for only N-1 screens of scroll: the last screen is spent
     letting the frame's own bottom reach the viewport's. There are two pinned
     tracks, so two screens of any total are gone before an act has moved.

     Run 04c names a total (26) AND a floor under every beat, and the two do
     not fit each other. The floors, in wheel notches of 100px at a 900px
     viewport, are what the run is actually about:

       act 1   five stations, each >= 8 notches, >= 4 of hold between
                 -> 4*(8+4) + 8 = 56, + a hold + the hand-over  = 64 notches
       act 2   arrival >= 12, hold-at-full-grid >= 12, sort >= 20, labels >= 12
                 -> 12+12+20+12 = 56 before any stagger or any hold, and act 2
                    must also stay the LARGER of the two              = 80
       acts 4+5  ELEVEN steps, strictly sequential, >= 8 notches each
                 -> 11*8 = 88 of motion alone, before a single hold   = 117
       act 3   4 screens, as asked                                    = 36
       handoff 1 screen, as asked                                     =  9

     RUN 11 DELETES THE HANDOFF AND HANDS ITS SCREENS BACK TO THE READER.
     The cream fade is gone -- there is no dawn, no veil and no ramp, and
     what follows the stage is the same dark ground the stage itself ends on.
     A blend needs a length; a hard cut does not. So the 1.5 screens the
     handoff held are not redistributed, they are RECLAIMED: 18 + 1.5 + 14 =
     33.5, and the distance from act 5's last beat to pricing's first row is
     zero by construction. See the fractions below and item 2 of the run-11
     report for the measured before/after.

     RUN 06 REDISTRIBUTED WITHOUT CHANGING THE TOTAL, and its note is kept
     because it is why act 3 is 1.5 screens and act 2 is the larger of the
     pinned pair: act 3 gave up two of its four, one went to track A so the
     act 1 -> act 2 hand-over could be fourteen notches rather than six, and
     one went to the cream handoff so it could blend rather than cut.

     That is 306 notches = 34 screens of SCRUB, so 36 of document. At 26 there
     are 216 notches of scrub to spend, and track A alone wants 144 of them:
     act 2 would get 44 where its own floors need 56, and would end up SMALLER
     than act 1. 26 cannot hold this run's step 2. It is one number to go back
     to any shorter total; what each one costs is in the report.

     (The run's "acts 4+5 ~ 6 screens" is run 04b's own note quoted back, and
     that note was 6 screens of ALLOCATION for eleven strictly sequential
     FOUR-notch steps. Doubling the window to 8 doubles the track: 13 screens
     of scrub, 14 of allocation.)

     THE FRACTIONS ARE SIXTY-SEVENTHS — 36/67, 3/67, 28/67 — which sum to
     exactly 1 and put every track's allocation on a whole (or half) screen at
     any viewport height. They were thirty-fifths while the handoff held the
     last 1.5 screens; taking those screens out halves the denominator's
     parity, so the exact fractions are now sixty-sevenths and are written to
     EIGHT places rather than six. That is not decoration: at six places
     0.537313 x 33.5 is 18.0000855 screens, which is 0.077px of track A at a
     900px viewport, where the old six-place value was out by 0.009px. Eight
     places put every track back inside a ten-thousandth of a pixel. */
  --stage-screens: 33.5;
  --stage-f-a: 0.53731343; /* 36/67  acts 1-2, pinned -> 18.00 screens (17.00 of
                            scrub, 15300px = 153.0 wheel notches at a 900px
                            viewport). RUN 06 takes a screen from act 3 and
                            spends it on the hand-over: act 1 -> act 2 was six
                            notches, which is one gesture, and it is fourteen
                            now. Act 1 keeps its 60; act 2 gets 79 */
  --stage-f-m: 0.04477612; /* 3/67  act 3, UNPINNED -> 1.50 screens. RUN 07
                            takes another half-screen off it, and off the
                            stage's total: after the copy cuts act 3's content
                            is 1007px -> the number in the run-07 report at
                            1440x900, inside a 1800px box, which is 44% empty
                            ground. The box is 1350px now. RUN 06's note, kept
                            because it is the reason the floor exists at all: */
  /*                       RUN 06, and
                            the file's own note said which way to go: at 4
                            screens against 1.28 screens of copy this act was
                            2.7 screens of empty ground, split either side of
                            the argument, and both halves read as a fault. Two
                            screens leaves 0.36 of a screen of clean ground
                            above and below -- the breath the pin break is for
                            and no more. The screen it gives up goes to track A
                            (the hand-over) and to the handoff (the blend).
                            Act 3's height is still its CONTENT where the
                            content is taller: at 1024x768 the copy is 1585px
                            against this floor's 1536, so the stage is 36.06
                            screens there rather than 36.00 */
  --stage-f-b: 0.41791045; /* 28/67  acts 4-5, pinned -> 14.00 screens (13.00
                            of scrub, 11700px = 117.0 notches). Eleven steps at
                            8 notches each with 2.5 of hold, strictly
                            sequential.
                            THE THREE FRACTIONS NOW SUM TO 1 WITH NOTHING LEFT
                            OVER, which is the whole of what run 11 changed
                            here: --stage-f-h is deleted, and with it the only
                            term that ever stood between the last act and the
                            page below. The pinned track's release and the
                            stage's own bottom edge are now the SAME document
                            row: track B ends at 33.5 screens, its frame stops
                            sticking one screen earlier at 32.5, and 32.5 is
                            also the master trigger's last scroll position. Act
                            5's last beat and pricing crossing the fold are one
                            event. */
  /* ── THE GROUND ────────────────────────────────────────────────────────
     One new colour on the page: the stage floor. Everything else is a token
     that already exists. #0D0C0B is a step below --v3-stage-deep (#15110e)
     and still carries the brand's brown rather than going to pure black.

     Contrast, computed (WCAG 2.1, sRGB) against that floor:
       #F1EADB  cream ink      16.32:1
       #f5c542  gold           12.05:1   -- on the DARK ground gold may be
                                            type. On the cream it is 1.40:1
                                            and never is. Same gold, inverted
                                            ground, opposite rule.
       #A79C8A  dim ink         7.23:1   -- run 06: this file said 7.36.
                                            The same routine reproduces
                                            the cream and the gold above
                                            exactly, so the arithmetic is
                                            this file's, not a reviewer's.
     The glow below is the only thing that lifts the floor, and its ceiling
     is set so those numbers stay true at its brightest pixel. */
  --v3-ground: #0D0C0B;
  /* RUN 11 DELETED FOUR TOKENS THAT ONLY THE HANDOFF READ, and they are
     named here rather than simply removed, because three of them were the
     page's only statement of what lay under the stage:
       --v3-below-rgb   20 18 16    the ground below, as a channel triple
       --v3-cream-rgb  237 229 213  the cream, as a channel triple
       --dawn-a         0.85        the light's strength
       --handoff-rx     128% / 260% the veil's horizontal radius
     WHAT REPLACES THEM IS NOT A TOKEN. The sections below the stage sit on
     --v3-ground-gb now -- the stage's own last colour -- so "the colour under
     the stage" is not a second value that has to be kept in step with a
     first. It is the same value. index.html's .v3-below reads it directly.  */

  /* -- PER-ACT GROUND TEMPERATURE ----------------------------------------
     RUN 06. The floor shifts by act and the lamp brightens as the reader
     descends. Every step is a few units per channel; the largest single
     channel delta from the base anywhere below is +6.

     token   rgb          d from base   R-B   hue     what it is
     ---------------------------------------------------------------------
     ground  (13,12,11)   ( 0, 0, 0)    +2     30deg  the brand brown, shipped
     g2      (11,12,16)   (-2, 0,+5)    -5    228deg  act 1's warm spread mirrored
     g3      (18,16,10)   (+5,+4,-1)    +8     45deg  the GOLD's own hue (43.9deg)
     g4      (11,13,17)   (-2,+1,+6)    -6    220deg  act 2's cold, a half-step cyaner
     g5      (16,15,13)   (+3,+3,+2)    +3     40deg  the CREAM's own hue (40.0deg)
     gb      (18,17,15)   (+5,+5,+4)    +3     40deg  the same hue, two units lifted

     THE ARC MOVES ON TWO AXES. Warmth (R-B) runs +2, -5, +8, -6, +3, +3 --
     ALTERNATING, on the same beat the layout alternates on. Hue runs 30,
     228, 45, 220, 40, 40: it leaves the brand brown for THE GOLD'S OWN HUE
     at the money and lands on THE CREAM'S OWN HUE for the last two, which is
     "cream bleeding in" stated as a hue rather than as a brightness. The two
     colds are different -- g2 is blue-violet at 228deg, g4 is blue-cyan at
     220 and one unit lighter -- so the arc is not a palindrome.

     IT IS NOT MONOTONE IN LIGHTNESS and does not pretend to be: relative
     luminance runs .00373 / .00372 / .00492 / .00399 / .00445 / .00537, and
     act 3 is a local peak because act 3 is the money. THE MONOTONE AXIS IS
     THE LAMP, and only the lamp -- see .stage-glow and paintGround.

     ACT 1 STAYS EXACTLY #0D0C0B, and that is load-bearing rather than
     conservative. Three rules FILL A SHAPE with var(--v3-ground) and expect
     it to vanish into the floor: .path-step::before here -- the 7px hollow
     node punched out of act 1's gold rule -- and the same node's fill and
     its lit ring in oyt-stage-mobile.css. All three are act 1 and nowhere
     else, so pinning act 1 to the base means no punch-out on this stage can
     ever mismatch the floor behind it, and the hard top edge against the
     cream hero stays byte-identical.

     WHERE THEY ARE APPLIED: one authored linear-gradient on .stage itself,
     three times -- an unqualified base map (reduced motion, and any browser
     no query reaches), a desktop scrub map inside the 901px query, and a
     phone map in oyt-stage-mobile.css. .stage has no padding and
     `isolation: isolate`, so its own background paints beneath every
     descendant and a gradient stop is simply a document row. COST PER
     FRAME: ZERO. Nothing writes a colour. */
  --v3-ground-g2: #0B0C10;  /* act 2  cool     */
  --v3-ground-g3: #12100A;  /* act 3  warmest  */
  --v3-ground-g4: #0B0D11;  /* act 4  coldest  */
  --v3-ground-g5: #100F0D;  /* act 5  cream in */
  --v3-ground-gb: #12110F;  /* act 5's tail. The handoff is an ALPHA ramp over
                               the live ground since run 06, so its first row
                               IS this colour by construction rather than by a
                               stop that has to be kept in step with it. */
  --v3-ground-ink: #F1EADB;
  --v3-ground-dim: #A79C8A;
  --v3-ground-rule: rgba(255, 255, 255, 0.12);
  --v3-ground-grid: rgba(255, 255, 255, 0.04);

  /* The lamp's centre and its alpha. oyt-stage.js and oyt-stage-mobile.js
     write these from scroll progress and nothing else does. The values here
     are the AUTHORED REST POSITION: what a reduced-motion visitor sees, what
     a visitor with no JavaScript sees, and frame 0 of the animated path.
     Run 06: they are written on .stage-glow, which is a LEAF, and never
     on .stage -- see the note above that element. */
  --stage-glow-x: 50%;
  --stage-glow-y: 42%;
  --stage-glow-a: 0.06;

}

/* ═══ SCROLL-BEHAVIOR IS auto ON THIS PAGE ════════════════════════════════
   RUN 06, and it is the SECOND half of the scroll-back freeze — a different
   mechanism from the style-recalc one, and the one that produces a lock
   rather than jank.

   index.html:326 sets `html { scroll-behavior: smooth }`, overridden to
   `auto` only under prefers-reduced-motion (index.html:1480) — that is, for
   exactly the readers who never see this stage. Chrome applies
   scroll-behavior to anchor navigation, to keyboard scrolling (Home, End,
   PageUp), to scrollbar clicks, and to window.scrollTo in both its
   one-argument and TWO-argument forms. It does NOT apply it to the wheel or
   to touch, which is why every existing gate in this harness is blind to it:
   they all move the page with behavior:'instant' or with a real wheel.

   Over a 36-screen scrub that turns any such scroll into an animated
   traversal of the WHOLE STAGE. Measured at 1440x900, unthrottled,
   harness/smoothprobe.js:

     one click on the nav's #pricing link
       43 rendered frames over 1492ms, ALL 43 inside the stage,
       jumping a median of 304px and a MAXIMUM OF 4644px — 5.2 viewports —
       between two consecutive rendered frames
     window.scrollTo(0, y), the 2-argument form ScrollTrigger 3.13 itself
     uses to restore scroll position
       64 frames over 1509ms, 32 of them inside the stage, median jump 749px
       = 0.83 viewports per frame, max 3455px
     the same with scroll-behavior: auto
       1 frame, 70ms

   Forty-three full scrub paints for a navigation the reader meant as a jump,
   and between two of them the stage moves five screens — which is what "the
   animation does not replay" describes. At the pre-run-06 frame cost of 76ms
   those forty-three frames are three seconds of grinding.

   There is nothing to preserve on the other side of the trade, and less than
   there used to be. This page has exactly three in-page anchors —
   #act-the-math, #pricing and #faq. (The four this note used to name were
   #problem, #features, #compare and #pricing; run 04 deleted the first three
   with the sections they named, and the set has been rebuilt since.)

   Two of the three, #pricing and #faq, are BELOW the stage, so every use of
   either crosses the whole of --stage-screens. The third is the case this
   rule is really for: #act-the-math targets a beat INSIDE the stage, so a
   smooth jump to it does not cross the pinned timeline, it SCRUBS it — every
   intermediate scroll position is a scroll position ScrollTrigger reads, and
   the reader who pressed a link to skip ahead is shown the animation they
   were skipping, one painted frame at a time, and set down in the middle of
   it. Smooth buys this page nothing and costs it a forced tour.

   Declared HERE rather than edited at index.html:326 for two reasons: this
   stylesheet is linked by index.html and by nothing else in the repo, so the
   other twelve pages keep their smooth anchors; and the reason lives next to
   the thing that caused it. oyt-stage.css loads after the inline <style>, so
   at equal specificity this wins. ══════════════════════════════════════ */
html { scroll-behavior: auto; }

/* ═══ THE SHELL ═══════════════════════════════════════════════════════════
   Everything dark lives inside this one element, so the ground is painted
   once and the boundary with the cream above and below is a single edge.
   It is in normal flow inside .content-wrap; only its interior is sticky. */
.stage {
  position: relative;
  background-color: var(--v3-ground);
  /* THE BASE MAP -- reduced motion, and any browser neither the desktop
     scrub query nor the phone sheet reaches. Proportional sixths with wide
     ramps, because under `reduce` the tracks have no height and the stage is
     six to seven and a half screens rather than thirty-six: the ramps are
     wide enough to bracket every boundary across that whole range, and a
     ramp that moves under one 8-bit step per screen cannot be seen to be
     misregistered.

     INTERPOLATION MUST STAY sRGB. Do not add `in oklab`, `in oklch` or any
     other `in <space>` keyword to any of the three gradients: every contrast
     number in this file's comments is an 8-bit sRGB lerp, and a colour-space
     keyword changes the mid-ramp values without changing the endpoints -- so
     nothing would look broken and the whole table would silently be wrong.
     Not `in srgb` either: it fails to parse on older Firefox and takes the
     entire declaration with it. */
  background-image: linear-gradient(180deg,
    var(--v3-ground)     0%,     var(--v3-ground)    14.5%,
    var(--v3-ground-g2) 25.5%,   var(--v3-ground-g2) 31.5%,
    var(--v3-ground-g3) 42.5%,   var(--v3-ground-g3) 52.5%,
    var(--v3-ground-g4) 63.5%,   var(--v3-ground-g4) 71.0%,
    var(--v3-ground-g5) 82.0%,   var(--v3-ground-g5) 86.5%,
    var(--v3-ground-gb) 92.5%,   var(--v3-ground-gb) 100%);
  background-repeat: no-repeat;
  color: var(--v3-ground-ink);
  /* The hero's own trick, for the same reason: the ground layers below are
     sticky rather than fixed, but the grain is a repeating tile and paint
     containment keeps the compositor from promoting the whole eight-screen
     shell to one enormous layer. */
  contain: paint;
  isolation: isolate;
}

/* The edges. The cream hero above and the cream pricing below meet the stage
   at a hard line by default; the handoff at the bottom softens the lower one
   into a full crossfade. The top edge stays hard on purpose -- the stage
   OPENS, it does not seep in. */
.stage::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(245, 197, 66, 0.38), transparent);
  z-index: 3;
}

/* ═══ THE GROUND LAYERS ═══════════════════════════════════════════════════
   Four layers, all sticky rather than fixed. Sticky means "follows the
   viewport for exactly as long as its own container is on screen" -- which
   is what a stage ground wants and what `fixed` cannot do without either a
   containing-block trick or a script. Each takes zero flow height via the
   negative margin, so the shell's height is the acts' height and nothing
   else. */
.stage-ground {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  margin-bottom: -100vh;
  margin-bottom: -100svh;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

/* 1. THE GRID. 1px lines on a 64px cell, radially masked so it fades out
      before the edges instead of stopping at them. */
.stage-grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--v3-ground-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--v3-ground-grid) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(ellipse 78% 68% at 50% 46%, #000 12%, transparent 100%);
  mask-image: radial-gradient(ellipse 78% 68% at 50% 46%, #000 12%, transparent 100%);
}

/* 2. THE GLOW. ONE lamp, gold, very low alpha, and its position is the only
      thing on the ground that scroll moves. The alpha ceiling is 0.10: at
      that value the brightest pixel it can make of the floor is still dark
      enough for every contrast number in the token block above to hold.

      RUN 06 -- THE LAMP IS MEASURED, AND IT STAYS A GRADIENT.
      The scroll-back freeze made every per-frame cost on this stage worth
      attributing, and this one was attributed twice.

      Its three numbers used to be written on .stage, which is the inherited
      root of 303 elements -- 45ms of an 88ms frame at 4x CPU. They are
      written HERE now, on a leaf. That is the fix, and it is in
      oyt-stage.js.

      A COMPOSITED ALTERNATIVE WAS BUILT, MEASURED AND REJECTED: a fixed
      circle of 116vw with `will-change: transform, opacity`, translated in
      whole pixels. It moves the cost from paint to composite, which is the
      right trade on a GPU and the wrong one in software raster -- at 4x CPU
      it took the frame from a 26.3ms median to 40.5ms, and it made the page
      more expensive WITH THE ENGINE KILLED (20.8ms -> 41.7ms), because a
      1670x1670 layer over a sticky full-viewport ground is re-composited on
      every frame whether or not anything wrote to it.

      The gradient's own per-frame rebuild costs 7ms at 4x by the same
      instrument: 26.3ms with it, 19.3ms with the lamp's numbers frozen,
      18.0ms with the lamp gone. Seven is the cheaper number and it is paid
      on every machine rather than only on the ones without a GPU.

      The box is `inset: 0` and not the `-20%` it was: .stage-ground clips at
      inset 0, so the overhang was painted and then thrown away. */
.stage-glow {
  position: absolute;
  inset: 0;
  /* THE LAMP WIDENS AS IT BRIGHTENS, for zero new properties and zero new
     writes: the two stop positions ride the alpha the engine already
     writes. At the authored rest value 0.06 they resolve to 22.0% and
     57.8% -- the shipped 22% and 58%, to a tenth of a point. The three
     fallbacks turn a malformed write into a lamp at rest rather than an
     invalid declaration and no lamp at all. */
  background: radial-gradient(
    circle at var(--stage-glow-x) var(--stage-glow-y),
    rgba(245, 197, 66, var(--stage-glow-a, 0.06)) 0%,
    rgba(245, 197, 66, calc(var(--stage-glow-a, 0.06) * 0.45))
                       calc(19% + var(--stage-glow-a, 0.06) * 50%),
    transparent        calc(50% + var(--stage-glow-a, 0.06) * 130%)
  );
}

/* 3. THE GRAIN. The hero's own tile -- same feTurbulence, same 200px repeat --
      at a lower opacity and screened rather than multiplied, because over a
      near-black floor a multiply is a no-op. */
.stage-grain {
  position: absolute;
  inset: 0;
  opacity: 0.05;
  mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 200px 200px;
}

/* 4. THE VIGNETTE. Keeps the grid's fade from reading as a rectangle. */
.stage-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 90% 80% at 50% 45%, transparent 40%, rgba(0, 0, 0, 0.55) 100%);
}

/* ═══ HAIRLINE PANELS ═════════════════════════════════════════════════════
   A 1px gradient border that is bright at the top-left and gone by the
   bottom-right -- the same rim-light logic the hero's plate uses, done with
   a mask so it stays 1px at every radius. */
.hair {
  position: relative;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.018);
}
.hair::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(140deg, var(--v3-ground-rule), rgba(255, 255, 255, 0.02) 46%, transparent 78%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* ═══ TRACKS AND FRAMES ═══════════════════════════════════════════════════
   DEFAULT (mobile, reduced motion, no JS): a track is nothing. It is a plain
   wrapper, its frame is a plain block, and each act is a full-height section
   in normal document flow. That is the accessibility path and it is also the
   fallback, which is the point -- there is one layout to get right and the
   animated path is a decoration on top of it. */
.pin-track { position: relative; }
.pin-frame { position: relative; }

/* ═══ THE ACTS ════════════════════════════════════════════════════════════ */
.act {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding: var(--v3-8) var(--v3-5);
  /* THE BODY TYPE IS SET HERE AND INHERITED, not stated as `.act p`. A
     descendant selector like `.act p` beats every single-class rule below it
     — .math-table .keep, .cluster-label, .tile-h — so a rule meant as a default
     silently became a ceiling and the largest numbers on the page rendered at
     body size. Inheritance has no specificity, so a class on the element
     itself always wins. */
  font-size: var(--v3-fs-body);
  line-height: var(--v3-lh-body);
  color: var(--v3-ground-ink);
}
.act-inner {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
}

/* ── THE PHONE'S WRAPPERS ARE NOT BOXES HERE ──────────────────────────────
   Run M1 groups each act's own children into a head and two to four panels,
   so that a phone can hand one panel of an argument to the next inside a
   sticky frame. That markup is SHARED — index.html is one document — so the
   wrappers have to be invisible to every context except the phone's.
   `display: contents` removes the box entirely: the children lay out exactly
   as they did when they were direct children, in flow, in a grid, in a flex
   box, anywhere. /oyt-stage-mobile.css turns them back into boxes at 900px
   and below, and nowhere else.

   This is the ONLY rule run M1 adds to this file, and the desktop stage is
   frozen: 105 frames — 21 seeks across 1440/1280/1024/1100/901 plus the
   reduced-motion render at 1440 — must hash identically before and after. */
.m-head,
.m-panel { display: contents; }
/* No `.act p` rule at all, for the same reason: every paragraph that wants a
   measure cap states its own, on its own class. */

.act-eyebrow {
  font-family: var(--mono);
  font-size: var(--v3-fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--v3-track-eyebrow);
  text-transform: uppercase;
  color: var(--v3-gold);
  margin-bottom: var(--v3-4);
}
/* RUN 13 — THE ACT HEAD IS SENTENCE CASE AND ONE FAMILY WITH THE SECTION
   HEADS BELOW THE STAGE. Jake, on the caps: "a guy screaming on a lot of
   caffeine". The transform is off, so each h2 renders the casing its markup
   already carries (no copy was retyped), and the three type values are the
   ones index.html's .sec-title reads -- clamp(28px, 8vw, 36px), 0.01em,
   1.15 -- stated here in full rather than through a shared token, because
   --v3-fs-display / --v3-lh-display / --v3-track-display also dress the
   FROZEN hero h1, which stays in caps above this test's scope. If the family
   moves, it moves in both files. The desktop rule under @media (min-width:
   901px) and the phone sheet's own .act h2 carry the same three numbers. */
.act h2 {
  font-family: var(--v3-display);
  font-size: clamp(28px, 8vw, 36px);
  line-height: 1.15;
  letter-spacing: 0.01em;
  font-weight: 400;
  text-transform: none;
  color: var(--v3-ground-ink);
  margin-bottom: var(--v3-5);
  text-wrap: balance;
}
.act .dim { color: var(--v3-ground-dim); }
.act .lit { color: var(--v3-gold); }

/* ═══ THE STAGE'S ONE CTA ══════════════════════════════════════════════════
   Gold fill, ink label -- 10.65:1, the same pair the hero's primary uses, so
   the two buttons on this page are one button.

   RUN 06 MAKES THEM THE SAME BUTTON IN FULL. The hero's `.hero .btn-p` is
   the standard on this page and this pill only half-followed it: a flatter
   shadow, a 1px lift instead of 2, no active state of its own, no arrow
   motion and no sweep. Every one of those is now the hero's own value,
   scaled up one step because the stage is bigger than it was:

     box       52px min-height / 14px 28px pad / 15px  ->  58 / 17px 34px / 16px
     shadow    the hero's exact pair, at the larger radius the larger pill
               wants: `0 1px 0 rgba(0,0,0,0.10)` for the seated edge and a
               spread gold pool beneath it
     hover     --v3-gold-lit, translateY(-2px) -- the hero's 2, not this
               pill's 1 -- and the arrow walks 4px, the hero's own tell
     active    --v3-gold-deep at translateY(-2px) scale(0.98) in 90ms, which
               this pill simply did not have
     the sweep one shimmer per hover-ENTER, reusing the hero's own
               @keyframes v3-cta-shimmer. That keyframe is declared inside
               index.html's `@media (prefers-reduced-motion: no-preference)`,
               so under `reduce` the NAME DOES NOT EXIST and the animation
               silently does not apply -- which is exactly the behaviour
               wanted, with nothing to remember to switch off.

   THE TRANSITIONS ARE SAFE ON THIS STAGE and that is worth writing down,
   because nothing else here is allowed one. The determinism contract is
   about SCROLL: a transition on a scroll-driven value makes the frame at a
   given position depend on when you arrived. These fire on :hover and
   :active only, the harness never hovers, and the resting frame -- the one
   every gate photographs -- carries none of them.

   `overflow: hidden` clips the sweep to the pill. The hero's own primary is
   `inline-block` for a ::first-letter it needs and this one does not, so
   inline-flex stays: the label and its arrow are a flex row here. */
.stage-cta {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-height: 58px;
  padding: 17px 34px;
  border-radius: var(--v3-r-pill);
  background: var(--v3-gold);
  color: var(--v3-gold-ink);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10), 0 12px 34px -10px rgba(245, 197, 66, 0.62);
  transition: background 180ms var(--ease-settle), transform 180ms var(--ease-settle),
              box-shadow 180ms var(--ease-settle);
}
.stage-cta svg { transition: transform 180ms var(--ease-settle); }
.stage-cta:hover {
  background: var(--v3-gold-lit);
  transform: translateY(-2px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10), 0 18px 44px -10px rgba(245, 197, 66, 0.78);
}
.stage-cta:hover svg { transform: translateX(4px); }
.stage-cta:active {
  background: var(--v3-gold-deep);
  transform: translateY(-2px) scale(0.98);
  transition-duration: 90ms;
}
/* The sweep rests off the left edge and is only ever moved by the animation,
   so a pointer that never arrives never sees it -- the hero's own note, and
   the reason this contributes nothing to any resting frame. */
.stage-cta::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.45) 50%, transparent 60%);
  transform: translateX(-120%);
}
.stage-cta:hover::after { animation: v3-cta-shimmer 700ms ease-out 1; }

/* ——— THE PROGRESS INSTRUMENT IS DELETED ———————————————————————————————————————
   RUN 07. Run 05 had an act-name rail; the brief for run 06 said kill it,
   and run 06 replaced it with THE TRAVEL — a 735px machinist's scale in
   the left lane with a sight, 35 graduations, six registration ticks and a
   dimension bracket, carrying no glyph anywhere in its subtree. Every word
   of that is true and it is still a progress bar: it says where you are and
   how much is left, which is the thing the brief did not want on the page.

   SO IT IS GONE, and not hidden: the markup, this block, the phone's
   overrides, both engines' paint and measure functions, the six IND_*
   score constants in each, the teardown entries, the crossing's clear
   lists, and the harness probes that named it. What is left in the left
   lane is nothing at all.

   WHAT THE ACTS GET BACK. The lane was 64px of act padding — `padding-inline:
   max(--act-pad, 64px)` — a floor under the gutter that existed only so a
   half-opacity glyph on act 4's 22px leftward exit could not reach the
   graduations. With no graduations there is no floor: the acts take the
   symmetric --act-pad they were always sized in, which is 32px at 901 and
   80px at 1920. At 1101 that is 32 against 64 — 64px of band back, and it
   is the width band that had the least of it.
   ——————————————————————————————————————————————————————————————————————— */

/* ═══ #stickyBar IS NOT PART OF THIS STAGE ═════════════════════════════════
   RUN 06, and it closes two things at once.

   THE GATE. index.html's own bottom bar is `position: fixed` at z-index 99
   with `backdrop-filter: blur(20px)`, and its "Get Started" duplicates the
   stage's one gold CTA. Run 04's contrast harness measured that CTA at
   1.48:1 THROUGH it. The run's own gate says nothing may be occluded by any
   fixed chrome, and a translucent blurred panel over the act is occlusion
   whatever it is doing there.

   THE COST. A full-width backdrop-filter over a ground that moves under it is
   re-blurred on every frame. Ablated at 4x CPU while scrolling the stage:
   removing it took the frame from a 40.5ms median to 29.8ms and the
   over-33ms count from 50 of 57 to 18.

   Run M1 already did exactly this on the phone, for the first reason and at
   172px of a 844px screen. The mechanism is that file's; only the class name
   is new, because oyt-stage-mobile.css is linked at (max-width: 900px) and so
   its rule cannot reach a desktop.

   THE !important IS LOAD-BEARING. #stickyBar carries its whole style as an
   inline attribute, `transform: translateY(100%)` included, and index.html's
   scroll handler writes stickyBar.style.transform on every scroll event. An
   inline declaration beats any author rule on specificity alone, so a
   stylesheet can only win with !important — and winning here rather than
   fighting that handler from script is what keeps one property from having
   two writers. Its 500ms transition goes with it: a transition is a clock,
   and with it the frame at a given scroll position depended on when the stage
   came on screen. ═══════════════════════════════════════════════════════ */
html.stage-over #stickyBar {
  transform: translateY(100%) !important;
  transition: none !important;
  /* AND VISIBILITY, NOT ONLY THE TRANSFORM. translateY(100%) puts the bar's
     top on the viewport's bottom edge -- and the occlusion hit test caught it
     there: at 1440x900 with act 3's cost table running past the fold,
     document.elementsFromPoint at y=899 returned #stickyBar ABOVE three table
     cells, because the bar's box lands at 899.x after fractional layout and
     Chrome's hit testing is sub-pixel. One row of pixels, over rows that are
     themselves half below the fold, and still: an element that is supposed to
     be gone should not be hit-testable. `visibility` is the property that
     means gone -- it takes the bar out of the hit test AND out of the
     accessibility tree for as long as the stage owns the screen, and it stops
     applying the moment the class does. The same rule the instrument uses for
     the same reason. */
  visibility: hidden !important;
}

/* ═══ THE HANDOFF IS DELETED — RUN 11 ═════════════════════════════════════
   `.stage-handoff` and its two pseudo-element layers are gone: the veil (a
   radial ellipse of --v3-below-rgb), the dawn (run 08's vertical cream fade,
   `alpha(p) = 1 - smoothstep(|p - 0.58| / 0.38)` scaled by --dawn-a) and the
   crossing grain. Roughly 300 lines, four custom properties, one element and
   1.5 screens of document.

   WHAT THE ELEMENT WAS FOR, so that deleting it is a decision and not a loss.
   It solved ONE problem: the stage's near-black ground met a section that
   painted #141210, and run 04 wanted the reader carried from one to the other
   rather than dropped. Every layer in it existed to make that crossing
   invisible -- and the last measurement run 08 took of it said so plainly:
   the seam was 1.002:1 BEFORE the light was ever switched on, because the
   two grounds are seven code levels apart.

   RUN 11 REMOVES THE PROBLEM INSTEAD OF THE EDGE. The sections below the
   stage are the stage's own ground now (index.html, `.v3-below`), so there is
   nothing to cross: the last row of the stage and the first row of pricing
   are the same surface, carrying the same grid, the same grain and the same
   vignette from the same three rules. A crossfade between a colour and
   itself is 1.5 screens of scroll spent on nothing.

   THE ONE THING THAT DID NOT COME BACK is the light. Run 08's dawn made the
   stage's last screen bright -- L* 78.90 at its peak at 1440x900 -- and that
   is deliberately gone; the bottom of the page is dark now, end to end.

   THE TOP EDGE IS UNCHANGED AND STAYS HARD. .stage::before is still the gold
   hairline above act 1: the stage OPENS, it does not seep in, and now it does
   not seep out either.  ══════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   THE SCRUB LAYOUT
   Desktop, and only where motion has not been refused. Inside this query the
   tracks get their length, the frames stick, and the acts of a pinned track
   stack on top of each other so one can become the next. Outside it none of
   that exists and the acts are five ordinary sections.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {

  /* -- THE DESKTOP GROUND MAP ---------------------------------------------
     A gradient stop is a DOCUMENT ROW, not a progress. At scroll s the
     viewport shows [s, s+1] screens and the reader's eye is at s + 0.5, and
     the master trigger's travel is 35 screens on a 36-screen box, so

         stop%(s) = (s + 0.5) / --stage-screens * 100

     Omit the +0.5 and every plateau lands half a screen -- 1.493 points at
     33.5 -- early. THE LAST STOP IS THE ONE EXCEPTION: 33.5/33.5 = 100% is
     the STAGE'S OWN BOTTOM EDGE, a real document row, and takes no +0.5 term.
     It was 95.714% and the handoff's top edge; run 11 deletes the handoff, so
     the same document row is now the last row of the stage and the first row
     of the page below it.

     RUN 11 CHANGES ONLY THE DENOMINATOR. Every stop below is at the same
     DOCUMENT ROW it was at in run 08 -- 8.222, 9.222, 18.000, 19.000, 19.500,
     20.500, 26.333, 27.333, 32.500, 33.500 -- because the acts have not moved
     and their tracks are the same absolute heights. 35 became 33.5, so each
     percentage is the same row over a shorter box. That is what makes acts 1,
     2, 4 and 5 render byte-identically to run 08 at every document row.

     The boundaries are derived, never chosen. S = --stage-screens:
       b(1|2) = (S*f_a - 1) * A2_IN             = 17 * 0.483660 =  8.2222 sc
       b(2|3) = S * f_a                         =                 18.0000
       b(3|4) = S * (f_a + f_m)                 =                 19.5000
       b(4|5) = S*(f_a+f_m) + (S*f_b-1)*B5_IN   = 19.5 + 13*0.525641
                                                =                 26.3333
       seam   = S * (f_a + f_m + f_b)           =                 33.5000
     THE SCREEN POSITIONS ABOVE DID NOT MOVE WHEN --stage-f-h WENT TO ZERO:
     every boundary is measured from the top of the stage and the handoff was
     after all of them. Only the PERCENTAGES changed, because the denominator
     did. `seam` is now S exactly -- f_a + f_m + f_b sum to 1 -- and it is the
     stage's own bottom edge.
     RUN 07 CORRECTS THIS TABLE'S ARITHMETIC AS WELL AS ITS NUMBERS: it read
     "16 * 0.458333" for b(1|2) and 21.0 for b(3|4), neither of which the
     engine ever computed. A2_IN is 74/153 = 0.483660 and S*f_a - 1 is 17.
     The STOPS below were always right; only this comment was not.
     RAMP HALF-WIDTH 0.5 SCREENS, and it is act 3 that sets it, again. At two
     screens the half-ramp was 0.75 and act 3 kept half a screen of plateau;
     at 1.5 screens a 0.75 half-ramp meets itself exactly at 18.75 and the act
     never reaches its own colour for a single row. 0.5 gives it 18.500 to
     19.000 of plateau with its copy centred at 18.750, dead inside it. The
     tail ramp into the handoff is still 1.5 screens.
     THE ENGINE'S OWN R MUST MATCH: oyt-stage.js measureGround().

     ACT 3'S WARMEST FRAME IS THE FRAME ITS COPY IS CENTRED, and that falls
     out of the geometry rather than being tuned to it: act 3's copy is
     1223px inside a 4-screen box, centred, so its dead centre is document
     19.000 screens and the viewport is centred on it at 52.778% -- exactly
     where the g3 plateau opens. */
  .stage {
    background-image: linear-gradient(180deg,
      var(--v3-ground)     0%,        /*  0.000 sc  act 1 opens on the shipped floor */
      var(--v3-ground)    24.543%,    /*  7.722 sc  plateau ends       (doc  8.222) */
      var(--v3-ground-g2) 27.528%,    /*  8.722 sc  ramp centred on b(1|2) = 8.222 */
      var(--v3-ground-g2) 53.731%,    /* 17.500 sc                     (doc 18.000) */
      var(--v3-ground-g3) 56.716%,    /* 18.500 sc  ramp centred on b(2|3) = 18.000 */
      var(--v3-ground-g3) 58.209%,    /* 19.000 sc  act 3's copy, dead centre 18.750 */
      var(--v3-ground-g4) 61.194%,    /* 20.000 sc  ramp centred on b(3|4) = 19.500 */
      var(--v3-ground-g4) 78.607%,    /* 25.833 sc                     (doc 26.333) */
      var(--v3-ground-g5) 81.591%,    /* 26.833 sc  ramp centred on b(4|5) = 26.333 */
      var(--v3-ground-g5) 97.015%,    /* 32.000 sc                     (doc 32.500) */
      var(--v3-ground-gb) 100%);      /* doc 33.500 == THE STAGE'S OWN BOTTOM EDGE,
                                         and the colour the page below it carries */
  }

  .pin-track[data-pin='a'] { height: calc(var(--stage-screens) * var(--stage-f-a) * 100vh); }
  .pin-track[data-pin='b'] { height: calc(var(--stage-screens) * var(--stage-f-b) * 100vh); }

  .pin-frame {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
  }

  /* Stacked. Every act of a pinned track occupies the whole frame; which one
     is legible is a matter of opacity and transform, both of which are pure
     functions of scroll progress. */
  .pin-frame .act {
    position: absolute;
    inset: 0;
    min-height: 0;
  }

  /* THE AUTHORED TRANSFORM, and why it is not a no-op.
     Every element the engine moves is given translate3d(0,0,0) HERE, in the
     stylesheet, so that its resting state is the same kind of thing as its
     painted state. A transform promotes an element to its own compositing
     layer, and a composited layer renders text with greyscale antialiasing
     instead of subpixel — so an element with no transform and the same
     element at translate3d(0,0,0) are not the same pixels. The determinism
     sweep saw exactly that: act 4 approached from below (never painted, no
     transform) against act 4 approached from above (painted at progress 0,
     transform applied) differed across 9,352 pixels of glyph edges with
     nothing moved. Authoring the identity transform makes the two states one
     state, and has the side benefit that the type does not change weight the
     instant the scrub touches it. */
  .stage .act,
  .stage [data-step],
  .stage [data-m],
  .stage [data-tile],
  .stage [data-cluster-label],
  .stage [data-rest],
  .stage [data-frame] {
    transform: translate3d(0, 0, 0);
    /* AND THE LAYER IS HELD, not merely created. The identity transform above
       promotes these to their own compositing layers, but Chrome DEMOTES a
       layer again once it has been still for a while — and a demoted layer
       renders text with subpixel antialiasing where a live one renders it
       greyscale. That demotion is a timing heuristic, not a style: the
       determinism sweep found act 4 differing across 9,352 glyph-edge pixels
       between two frames whose computed opacity, transform and visibility
       were character-for-character identical. will-change keeps the layer for
       as long as the rule applies, so the compositing state stops depending
       on how long ago something moved. */
    will-change: transform, opacity;
  }

  /* THE AT-REST STACK, and why it is authored rather than set by script.
     If oyt-stage.js never loads -- blocked, 404, a parse error -- this is
     what the visitor gets: act 1 lit, the acts behind it dimmed back but
     still in the document. Nothing is display:none and nothing is
     visibility:hidden, so every word of every act is still read by a crawler
     and still reachable by a screen reader, which is the whole reason the
     copy is authored in the DOM in the first place. */
  .pin-frame .act + .act { opacity: 0; }
  .pin-frame .act + .act[data-act-rest='lit'] { opacity: 1; }

  /* ACT 3 IS THE PIN BREAK and it is not inside a pin-track, so its scrub
     distance simply IS its height - nothing releases a viewport-height out of
     it. Its floor lives here, in the scrub query, rather than on .act-math
     unconditionally, and the scoping is the point: under run 04 the floor was
     shorter than act 3's own copy at every width, so it did nothing and where
     it was declared did not matter. At 2.00 screens it is now TALLER than the
     copy - which is what buys act 3 the two screens the run asks for, and what
     makes the stage the same length at 1024 as at 1440. On a phone and under
     reduced motion there is no scrub for that height to pay for, and a floor
     above the copy would be several hundred pixels of nothing to swipe past.
     Measured, not assumed: at 768 the copy is 1710px and this floor would be
     2048px. */
  .act-math { min-height: calc(var(--stage-screens) * var(--stage-f-m) * 100vh); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — the accessibility path, not a degraded one.
   No pin, no scrub, no reveal: five ordinary sections each showing its
   FINISHED frame with every word in it, scrolled past normally. The scrub
   query above already excludes this case, so all that is left is to make
   sure nothing else on the page can hide a word from it.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .stage [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT 1 — THE MONEY PATH
   Five stations and what leaks at each. The rule they hang from is drawn
   left to right by --draw; at rest it is fully drawn, which is the finished
   frame a phone and a reduced-motion visitor get.
   ═══════════════════════════════════════════════════════════════════════════ */
.act-lede { color: var(--v3-ground-dim); max-width: 62ch; }
/* .act-note IS GONE, and so are its seven rules. RUN 08 deletes act 3's
   maintenance-cap line, which was the last element in the document carrying
   this class -- run 06 had already emptied act 1's and run 07 act 5's. One of
   the seven had been addressing nothing since run 06: `.stage .act-money
   .act-note` placed a note act 1 does not have on the bench grid's row 5.
   That is the reason to delete a ruleset with its element rather than leave it
   for later: a rule with no element is not dormant, it is unfalsifiable. */

.path {
  list-style: none;
  margin: var(--v3-8) 0 0;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--v3-5);
  position: relative;
  padding-top: var(--v3-6);
}
/* .path::before and .path-step::before ARE NOT DECLARED AT BASE since run 06.
   The desktop's chain declares both inside @media (min-width: 901px) at the
   end of this file; oyt-stage-mobile.css declares both again, content: ''
   included, for the phone's vertical path. Neither can reach the other. */
.path-step { position: relative; }
.path-n {
  display: block;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--v3-ground-ink);
  margin-bottom: var(--v3-3);
}
.path-leak {
  font-size: 14px;
  line-height: 1.5;
  color: var(--v3-ground-dim);
  max-width: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT 2 — THE TOOLWALL
   The AUTHORED layout is the finished one: three labelled clusters, four
   named tiles each. Every tile is the same size, which is what lets
   oyt-stage.js derive the undifferentiated grid the tiles arrive in from
   this layout rather than from a second set of numbers that could drift.
   ═══════════════════════════════════════════════════════════════════════════ */
.wall {
  margin-top: var(--v3-7);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--v3-6);
  align-items: start;
}
.cluster { position: relative; }
.cluster-label {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--v3-gold);
  margin-bottom: var(--v3-4);
}
.cluster-rule {
  display: block;
  width: 22px;
  height: 1px;
  background: var(--v3-gold);
  opacity: var(--cluster-lit, 1);
  transform: scaleX(var(--cluster-lit, 1));
  transform-origin: 0 50%;
}
.cluster-tiles {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
.tile {
  min-height: 98px;
  padding: 14px 14px 12px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 10px;
  color: var(--v3-ground-ink);
}
.tile-mark { width: 22px; height: 22px; color: var(--v3-gold); flex: none; }
.tile-name {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.28;
  letter-spacing: 0.005em;
  color: var(--v3-ground-ink);
}
.wall-rest {
  margin-top: var(--v3-6);
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--v3-ground-dim);
  max-width: 86ch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT 3 — THE MATH. The pin break: full width, unpinned, and the stage's one
   gold CTA at the end of it.
   ═══════════════════════════════════════════════════════════════════════════ */
/* Act 3 is the pin break. Its floor is stated in the scrub query above, where
   the scroll distance it buys is the only thing that distance is for; here it
   is an ordinary section as tall as its copy.

   THE COPY IS CENTRED IN THE BOX, NOT PINNED TO ITS TOP, and that is a
   consequence of run 04c's length rather than a taste. Act 3 is the one act
   nothing pins, so its scroll distance IS its height: giving it four screens
   gives it 2.4 screens more than its copy needs. Aligned to the top, all of
   that arrives as one uninterrupted 2.4-screen fall of empty ground between
   the cost argument and act 4. Centred, it is a screen of clean ground before
   the argument and a screen after it -- the breath the pin break is for, and
   the shape a reader can tell was meant. Below 901px this is a no-op: the act
   has no floor there and its copy is taller than the viewport anyway
   (1926px at 390, 1710px at 768), so the mobile heights do not move. */
.act-math {
  align-items: center;
  padding-top: var(--v3-9);
  padding-bottom: var(--v3-9);
}
.act-inner-wide { max-width: 1180px; }

.vh {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* RUN 07 — THE TABLE IS A PLATE, and it is padded like one.
   THE DEFECT: at every width on the bench the first column sat ON the panel's
   own hairline. `.math-grid .math-table` inside the 1101 query carried
   `padding: 6px 0 14px` — run 06's fix for D3, a horizontal overflow of up to
   151px caused by 24px of dead padding against the table's own
   `min-width: 560px` — and zeroing the padding is not what D3 needed. Measured
   at 1440 before this run: every first-column cell's left edge 0px from the
   container, under a 1px gradient border drawn at that exact row.

   THE FIX IS THE OTHER HALF OF D3. --math-pad is a real gutter at every width
   AND the min-width comes down to 460 so the gutter fits inside the seven
   columns act 3's meridian gives the table. 460 is measured, not chosen: the
   widest unwrappable cell is "Housecall Pro MAX" at 15px/600 (nowrap) plus the
   two money columns at their natural minimum.

     width  7-col track  --math-pad  inner width  min-width  overflow
      1101      586.2       18.7        548.8        460         0
      1280      693.3       21.8        649.7        460         0
      1440      770.0       24.5        721.0        460         0

   And the plate is now on the same radius and the same air as the acts' own
   cards — 14px and 22/24/20 — so act 3 reads as one of the five rather than
   as a spreadsheet somebody pasted in. */
:root { --math-pad: clamp(18px, 1.7vw, 26px); }
.math-table {
  margin-top: var(--v3-7);
  border-radius: 14px;
  padding: 14px var(--math-pad) 8px;
  overflow-x: auto;
}
.math-table table { width: 100%; border-collapse: collapse; min-width: 460px; }
/* ── THE TABLE'S TYPE, RUN 08 ──────────────────────────────────────────────
   The owner's reading: too light to read at a glance. Both halves of "light"
   are real here -- 14/15px at weight 400 for the values and 11/12px mono at
   400 for the column headings, on a near-black plate.

   THE WEIGHTS ARE CHOSEN OFF WHAT THE PAGE ACTUALLY HAS A FACE FOR, which is
   not what the numbers suggest. Measured by rendering the same run at 64px on
   white and counting covered pixels:
     JetBrains Mono   400 -> 500  +16.2% ink   A REAL STEP
                      500 -> 600  +33.9% ink   Chromium's SYNTHETIC BOLD
                      600 -> 700   no change   the same synthetic render
     Hanken Grotesk   400 -> 500  +13.2%   500 -> 600 +12.5%   600 -> 700 +10.0%
                      every step real -- it is a 300..800 variable face
   The page declares JetBrains Mono at 400 and 500 only, so any mono weight at
   or above 600 is a smear rather than a weight. The column headings go to 500
   and stop; the values and the row headings are Hanken and can go where they
   like. Advance cannot tell these apart on a monospaced face -- the advance is
   identical at every weight by definition -- which is why the probe counts
   ink.
   The COLOURS ARE UNTOUCHED, so every contrast reading this table already had
   still holds: dim values for the competitors, ink for ours, gold for the
   name and the total. Raising a weight cannot lower a ratio, and none of
   these sizes crosses the 18.66px/700 or 24px line where the AA bar would
   drop to 3.0 and the gate would get EASIER. */
.math-table th, .math-table td {
  text-align: left;
  padding: 15px 24px 15px 0;
  /* the last column ends on the plate's gutter, not on its border */
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  font-size: 15px;
  font-weight: 500;
  color: var(--v3-ground-dim);
}
/* ── THE FOURTH COLUMN, RUN 11 ────────────────────────────────────────────
   The act's conclusion, moved inside the object that earns it. It was a 72px
   Anton payoff on a plate of its own — YOU KEEP $20,650 — making the case
   against ONE of the three competitors in the table beside it. As a column it
   makes the case against all three, and every figure in it is that row's own
   5-Year Total less the $290 licence: 20,940 - 290, 23,940 - 290, 36,000 -
   290. The <caption> states the derivation for a reader who cannot see the
   arithmetic laid out in the row.

   IT IS THE ONLY COLUMN THAT IS GOLD, and the gold is the reader's rather
   than the product's: --v3-gold marks .is-ours everywhere else on this stage,
   and here it marks what the reader keeps. The Own Your Tools row's own cell
   is an em dash in the dim ink, because you do not save against yourself.

   18px AND NOT 20. 18.66px at weight 700 is where WCAG's large-text bar drops
   from 4.5 to 3.0, and run 08's note is explicit that no size in this table
   may cross it: a type step that makes the contrast gate EASIER is not a type
   step. Measured gold on this plate: 9.72:1 against a 4.5 bar. */
.math-table .keep {
  color: var(--v3-gold);
  font-weight: 700;
  white-space: nowrap;
  text-align: right;
}
/* THREE CLASSES, and the count is the point: .math-table .is-ours td is
   (0,2,1) and sets the ink for every cell on that row, so a two-class rule for
   the dash lost to it in silence -- measured, the dash rendered #F1EADB at
   14.16:1 when it was authored to be the dim ink. */
.math-table .is-ours .keep-none { color: var(--v3-ground-dim); font-weight: 500; }
.math-table thead th:last-child { text-align: right; }
.math-table thead th {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-top: 16px;
}
.math-table tbody th { color: var(--v3-ground-ink); font-weight: 700; white-space: nowrap; }
.math-table tbody tr:last-child th, .math-table tbody tr:last-child td { border-bottom: none; }
.math-table .is-ours th { color: var(--v3-gold); }
.math-table .is-ours td { color: var(--v3-ground-ink); }
.math-table s { color: var(--v3-ground-dim); text-decoration-thickness: 2px; }
.math-table strong { color: var(--v3-gold); font-weight: 700; }


/* ── ACT 3'S RIGHT-HAND PLATE IS DELETED — RUN 11 ─────────────────────────
   .math-panel, .paths, .path-col, .path-kicker, .path-big, .path-tail,
   .math-payoff and .math-payoff-sub are gone, with every override of them in
   the >=1101px query, in /oyt-stage-mobile.css and in its narrow-phone tier.
   Run 08 made those three loose elements into one plate; run 11 finds that
   the plate and the table were two objects making one argument, and the table
   is the one with the evidence in it. See .math-table .keep above.
   NOTE FOR ANY LATER RUN THAT GREPS FOR THESE NAMES: act 1 has .path,
   .path-lead, .path-step and .path-rail, and --path-tail is act 1's frame
   air. They are a different family and none of them moved. */

/* THE COST-GAP LINE BELONGS TO THE TABLE, and 32px of air was not saying so.
   RUN 07 moved it out of the act grid's row 4 and into the table's own column;
   RUN 08 closes the distance. Measured at 1440x900 before: the table's plate
   ended at doc 17948 and this line began at 17980 -- 32px -- with 120px of
   nothing under it before the next line in the column. It read as a caption
   for the empty ground rather than for the numbers above it. 16px is the
   plate's own bottom air (10px) plus a hair, which is the distance a caption
   sits from the thing it captions everywhere else in this file. The 120px
   below it is gone too: the element that made it was .act-note. */
.math-gap { margin-top: var(--v3-4); color: var(--v3-ground-dim); max-width: 74ch; }
.math-cta { margin-top: var(--v3-7); max-width: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   ACTS 4 AND 5 — THE PLATES
   RUN 06. The four steps and the five promises were hairline-topped columns:
   a mono number, a 15px heading, a 13.5px sentence, and a 1px rule above.
   They are act 2's tile now, one size up, with a real interior.

   HOW THE IDIOM SCALES — the four things that change and the four that do not.
   CHANGES:   radius 10 -> 14px · padding 14/14/12 -> 22/24/20 · the mark
              22 -> 38px · and the type goes from ONE register (a 12.5px tool
              name) to THREE: a 13px mono gold index, a 20px cream heading, a
              14.5px dim sentence.
   UNCHANGED: THE PEN. Every mark on this page is drawn at 1.2833 rendered
              pixels — .tile-mark is stroke-width 1.4 on a 24-unit viewBox at
              22px, which is 1.2833. A 38px render of the same viewBox needs
              0.81. The tick's 1px borders, the plate fill at 0.018 white and
              the rule that gold shows only where the metal is warm are all
              act 2's, unchanged. THE DRAWING GETS BIGGER; THE LINE NEVER
              DOES. That is what drafting means here.

   THE INDEX SITS ON THE MARK'S ROW, NOT ABOVE IT, and that is a height
   decision with a measured reason: stacking a numeral above a 38px mark costs
   40px of interior and act 5's budget has 36px of slack at 1440x900. On one
   row the header becomes a RULED HEADER — a symbol and its part number under
   a rule — which is more drafting sheet than a stacked ordinal, and it keeps
   the index in the register this page already owns for it (mono, gold,
   0.14em: .path-n, .cluster-label, .act-eyebrow) rather than inventing an
   Anton numeral to compete with the heading two lines below.

   THE PROMISE COUNTERS ARE GONE with the old block, and the numbers are
   authored in the markup instead. They were CSS counters on an <ol> that
   already carried the order — decoration a screen reader got twice — and the
   plate's header row needs the index on the same line as its mark, which a
   ::before on the heading cannot be.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ══ THE HEAD IS ABOVE THE FLIGHT PATH, AND THAT IS A STACKING DECISION ══
   RUN 06. The owner asks for cards that enter from above and from all around,
   and an act's own eyebrow, headline and lede sit directly in that path: the
   card-clearance hit test found act 4's card 01 and act 5's card 02 crossing
   their act's head at full opacity — inevitable, because a plate that drops
   from 360px above its seat passes through everything between.

   Shortening the throw kills the entrance the owner asked for; clipping the
   flight to the row hides most of it; fading the card in late turns a solid
   object arriving into a ghost resolving. So the plates pass BEHIND the head
   instead. The head is the act's fixed frame and the plates are what moves
   through it, which is the better reading as well as the provable one — and
   it costs nothing: two z-index declarations, no motion change, no new
   element and nothing written per frame.

   The head elements are grid items, which honour z-index without position,
   and the lists are their siblings — so one comparison decides the whole act.
   Contrast is unaffected: a plate is 0.018 white and the text is ON TOP of
   it, which is the case the contrast harness measures at 15.82:1 cream. */
.stage .act-eyebrow,
.stage .act h2,
.stage .act-lede,
.stage .act-close { position: relative; z-index: 2; }
.stage .wall,
.stage .moves,
.stage .promise-stack { position: relative; z-index: 1; }

.card {
  --card-heat: 0;
  position: relative;
  border-radius: 14px;
  padding: 22px 24px 20px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  color: var(--v3-ground-ink);
  min-height: 216px;
}
/* ACT 5 IS THE TALLEST ACT ON THE STAGE AND HAS THE LEAST TO GIVE: five
   plates plus a heading, a lede and a closing disclosure, inside one viewport.
   Its plate is tighter than act 4's on every axis, and the act-fit gate is what
   set each of these numbers — the first build of the course needed 1000px in a
   772px frame at 1440x900 and clipped at eleven of twelve shapes. */
.promise-card {
  min-height: 168px;
  padding: 18px 22px 16px;
}
/* THE TYPE HALF OF THIS BLOCK MOVED BELOW .card's OWN RULES, because three of
   its six declarations never took effect where they stood. `.promise-card h3`
   and `.card h3` are both (0,1,1) and every plate carries BOTH classes, so
   source order decided and .card's came later: act 5's heading took act 4's
   8px margin and act 5's body took act 4's 14.5px, measured 19px/8px/14.5px
   on the running page against the 18px/6px/14px written here. The font-size
   was dead twice over -- restated as 19px 44 lines further down. The block's
   own comment says act 5's plate is "tighter than act 4's on every axis"; on
   the two type axes it was not. The geometry half below was always live. */
.promise-card .card-rule { margin: 11px 0 13px; }
.promise-card .card-mark { width: 32px; height: 32px; stroke-width: 0.96; }

.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card-mark { width: 38px; height: 38px; flex: none; stroke-width: 0.81; }
/* TWO INKS, and they are the scale-up. A 22px single-ink glyph on a 98px tile
   has room for one figure; a 38px mark on a 216px plate has room for FIGURE
   AND GROUND. That is act 2's plate/mark relationship moved inside the
   drawing, and it is the one place these cards legitimately go past act 2
   rather than only getting bigger. */
.mk-s { stroke: rgba(241, 234, 219, 0.40); }   /* STRUCTURE — 3.39:1 on the plate */
.mk-a { stroke: var(--v3-gold); }              /* ACTIVE    — 11.69:1 on the plate */
.card-n {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--v3-gold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  flex: none;
}
/* act 2's .tile-rule is white-only; a 326-443px plate can carry the heat at
   the rule's origin, which is the one place the bigger plate earns more gold */
.card-rule {
  display: block;
  height: 1px;
  margin: 14px 0 16px;
  background: linear-gradient(90deg, rgba(245, 197, 66, 0.34), rgba(255, 255, 255, 0.10) 34%, transparent 78%);
}
.card h3 {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.002em;
  color: var(--v3-ground-ink);
  margin: 0 0 8px;
  /* LOAD-BEARING: .act h2's `text-wrap: balance` must not reach a card
     heading, or a heading re-breaks between widths, which changes a card's
     height, which moves every seat in the course. */
  text-wrap: initial;
}
.card p { font-size: 14.5px; line-height: 1.5; color: var(--v3-ground-dim); max-width: none; margin: 0; }
/* ACT 5'S PLATE, AFTER .card's, WHICH IS THE ONLY PLACE IT CAN WIN. */
.promise-card h3 { font-size: 18px; margin-bottom: 6px; }
.promise-card p { font-size: 14px; }

/* THE TICK — act 4 only, and IT RECORDS THE ENTRANCE. Top-right on the odd
   cards, which drop from above; bottom-right on the even ones, which rise from
   below. 14px against the tile's 9px. Static CSS, zero frames: true with no
   JavaScript, under reduced motion, and on a phone. */
.card-tick {
  position: absolute;
  right: 14px;
  width: 14px; height: 14px;
  border-right: 1px solid rgba(245, 197, 66, 0.42);
  pointer-events: none;
}
.moves > li:nth-child(odd) .card-tick { top: 14px; border-top: 1px solid rgba(245, 197, 66, 0.42); }
.moves > li:nth-child(even) .card-tick { bottom: 14px; border-bottom: 1px solid rgba(245, 197, 66, 0.42); }

/* THE SEAL — act 5 only. A promise is stamped, not stepped. Its gold
   quarter-arc points back along the card's OWN RAY, so the seal is a compass
   of where that plate flew in from: five seals read as one object in five
   positions, and the finished frame states the entrance without a frame of
   motion. The angles are derived from --ray-x/--ray-y below, not chosen. */
.promise-seal { position: absolute; top: 16px; right: 16px; width: 24px; height: 24px; pointer-events: none; }
/* AND ACT 5'S INDEX MOVES OFF THE SEAL. Act 4's header is mark-left,
   index-right; act 5's has a stamp in that corner, and space-between put the
   number underneath it. The contrast harness read the collision before any
   eye did — gold on gold at 3.44:1 at 1440 and 2.21:1 at 320, against a 4.5
   bar. The index sits beside its mark instead, and the header reserves the
   seal's corner so the two can never meet at any width. */
.promise-card .card-head { justify-content: flex-start; padding-right: 34px; }
/* The seal's gold quarter-arc points back along the card's own ray, so the
   finished course still states where each plate came from: the top row's arcs
   point up, the bottom row's point down. Derived from --ray-y above, not
   chosen. */
.promises > li:nth-child(4) .promise-seal,
.promises > li:nth-child(5) .promise-seal { transform: rotate(180deg); }

/* ══ THE HEAT IS A RING, NOT A WASH, AND THAT IS A CONTRAST FIX ══
   Act 2's .tile::after carries `background: linear-gradient(140deg,
   rgba(245,197,66,0.16), transparent 52%)`. A tile's only text is a name in
   full cream, so that wash was never measured against DIM ink — and the lamp
   sits behind the acts, through a plate that is only 0.018 white. Composited:
   plate + lamp at its 0.080 ceiling + the wash is #45391B, and #A79C8A on
   that is 4.16:1, under the floor. There is no alpha that survives — even
   0.10 lands at 4.30:1. So the background is DELETED and only the box-shadow
   remains. A non-inset box-shadow paints EXCLUSIVELY OUTSIDE the border box,
   so nothing is ever behind a run of text but the card's own plate and the
   floor is closed by construction rather than by gradient geometry. It is
   also the better drawing: on a 443px plate the long gold EDGE is what the
   eye reads.

   AND THE GLOW TRAILS THE CARD: its offset is the NEGATIVE of the card's own
   ray, so light drags behind a plate in flight — and because it reads the
   same two numbers that drive the translate, there is no second table of
   angles to drift out of step. */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--card-heat, 0);
  box-shadow:
    0 0 0 1px rgba(245, 197, 66, 0.34),
    calc(var(--ray-x, 0) * -13px) calc(var(--ray-y, 0) * -13px) 40px rgba(245, 197, 66, 0.13);
}
/* mirrors .tile: the scrub query already holds the layer for [data-step] */
.card { will-change: auto; }

/* ── ACT 4 — THE FLUSH ROW ───────────────────────────────────────────────
   A row of four keeps DOM order (a numbered procedure cannot be read
   01, 03, 02, 04) and keeps ONE CARD PER FLIGHT CORRIDOR — the corridor and
   the grid column are the same thing, which is what makes the entrance
   provably non-occluding. "Offset the other way" comes from the HEAD, not
   from the cards: act 1's head is flush left, act 4's is hung right.

   THE INTERLOCK IS GONE, AND ONLY THE INTERLOCK. Run 06 hung the even cards
   32px below the odd ones (`margin-top: 32px`, so tops 358/390/358/390 at
   1440x900 — a staircase with the fourth plate hanging off the end); run 07
   mirrored that displacement about the row's own axis, so the ranks sat at
   -I/2 and +I/2 and the tops were 350/382/350/382. Both are a stagger, and a
   stagger is what the owner asked to be rid of: the four plates are one rank
   now, at one top and one bottom.

   THE ALTERNATING ENTRANCE IS UNTOUCHED, because it never came from here.
   --ray-y is declared per rank two rules below and the engine flies each plate
   along its own ray, so cards 01 and 03 still fall from above and 02 and 04
   still rise from below. What the two ranks no longer differ in is where they
   LAND.

   align-items: STRETCH, not start, and that is the half of "flush" a margin
   cannot give. With `start` each plate is its own content height and the
   bottoms are only equal when the four bodies happen to wrap to the same
   number of lines — measured at 901x900 before this run, cards 03 and 04 were
   230px against 216 for 01 and 02, so the row's bottom edge stepped 14px in
   the middle even between plates of the same rank. Stretch makes every plate
   the row's height, so BOTH edges are equal at every shape by construction
   rather than by coincidence. .card is already a column flexbox and its
   content stays top-aligned; what stretches is the plate. */
.moves,
.promises { list-style: none; margin: 0; }
.moves {
  --moves-rows: 1;            /* READ BY THE ENGINE, authored where the grid is */
  margin-top: var(--list-gap);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--act-gap);
  align-items: stretch;
}
/* ONE RULE PER RANK, and it carries the one thing a rank still differs in:
   the ray the engine flies the plate along. The seat is the same for all four. */
.moves > li:nth-child(odd)  { --ray-x: 0; --ray-y: -1; }
.moves > li:nth-child(even) { --ray-x: 0; --ray-y: 1; }

/* ── ACT 5 — THE 3+2 COURSE INSIDE A REGISTRATION FRAME ──────────────────
   THE RAY IS AUTHORED IN THE SAME DECLARATION BLOCK AS THE SEAT, and that is
   the whole anti-drift discipline: a ray cannot point somewhere the grid does
   not, because changing one without the other means editing half a rule. Each
   vector is the unit vector from the course's centre through that card's own
   centre, so on a convex course a seat is the OUTERMOST point on its own ray
   and the segment a card travels is provably empty of siblings. */
.promise-stack { position: relative; margin-top: clamp(20px, 4vh, 48px); }
.promises {
  position: relative;
  display: grid;
  /* BELOW 1101 the six-track course is too narrow to be a course: at 901 a
     two-track card is 275px and every promise heading takes two lines. Three
     across and two under, auto-placed, in the same grid — so the rays and the
     seals stay true (they only have to point OUTWARD, and on a 3+2 they do in
     both layouts) and nothing else moves. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--act-gap);
}
/* THE RAYS ARE VERTICAL, AND THAT IS A CORRECTION THE OCCLUSION GATE FORCED.
   The course was built to fly its plates in from five bearings — each along
   the unit vector from the course's centre through its own seat, which on a
   convex course makes every seat the outermost point on its own ray. It is a
   good idea and it cannot be made safe here: the course spans the whole
   container, so a seat at the left end has no air outside it, and a plate
   travelling left along a near-horizontal ray leaves the frame entirely.
   Measured when the left lane still held run 06's progress instrument: at
   1440 the flight put act 5's card 01 at 0.8 opacity inside a box whose
   contents HIT-TESTED ABOVE its heading and its sentence. The instrument is
   deleted in run 07 and the arithmetic is unchanged: 220px of travel on a
   1376px course is not an entrance, whatever is or is not in the gutter.

   There is no distance that fixes it. Capping the throw so the leftmost
   plate's box clears x = 32 gives a travel of under 220px on a 1376px course,
   which is not an entrance; and every horizontal ray on a course this wide
   has the same problem at one end or the other.

   So the course assembles from ITS OWN TWO SIDES: row 1 drops from above, row
   2 rises from below. The .pin-frame is overflow: hidden and one viewport
   tall, so a plate 612px outside it is CLIPPED — an occlusion is not merely
   absent, it is unrepresentable. And it is the entrance the run's own C21
   offers ("same alternating top/bottom rule where it fits"): in ARRIVAL order
   the slots light card 1, 4, 2, 5, 3 — above, below, above, below, above.
   What is lost against C20's "from all around" is the horizontal bearing;
   what carries the assembly instead is the 3+2 course closing on itself and
   the registration frame drawing itself around it. */
.promises > li:nth-child(1) { --ray-x: 0; --ray-y: -1; }
.promises > li:nth-child(2) { --ray-x: 0; --ray-y: -1; }
.promises > li:nth-child(3) { --ray-x: 0; --ray-y: -1; }
.promises > li:nth-child(4) { --ray-x: 0; --ray-y: 1; }
.promises > li:nth-child(5) { --ray-x: 0; --ray-y: 1; }

.promise-frame {
  position: absolute;
  inset: -16px;
  border: 1px solid rgba(245, 197, 66, 0.16);
  border-radius: 18px;
  pointer-events: none;
  /* AUTHORED DRAWN — the no-JS finished state, the same choice --draw: 1 and
     --cluster-lit: 1 make elsewhere on this stage. */
  opacity: 1;
}
.pf-t { position: absolute; width: 18px; height: 18px; }
.pf-t:nth-child(1) { top: -1px; left: -1px; border-top: 1px solid var(--v3-gold); border-left: 1px solid var(--v3-gold); }
.pf-t:nth-child(2) { top: -1px; right: -1px; border-top: 1px solid var(--v3-gold); border-right: 1px solid var(--v3-gold); }
.pf-t:nth-child(3) { bottom: -1px; left: -1px; border-bottom: 1px solid var(--v3-gold); border-left: 1px solid var(--v3-gold); }
.pf-t:nth-child(4) { bottom: -1px; right: -1px; border-bottom: 1px solid var(--v3-gold); border-right: 1px solid var(--v3-gold); }

.act-close {
  margin-top: var(--v3-7);
  font-size: var(--v3-fs-small);
  line-height: 1.6;
  color: var(--v3-ground-dim);
  max-width: 82ch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   NARROWER THAN 1100 — the act interiors reflow BEFORE the mode boundary at
   900, so a small laptop still on the scrub path never gets a squeezed
   five-column promise list.
   ═══════════════════════════════════════════════════════════════════════════ */
/* 1100.99 AND NOT 1100. `max-width: 1100px` and `min-width: 1101px` are not
   complements: every fractional width strictly between them matched NEITHER,
   and there act 5 fell back to the base 3-up promise grid this run deleted --
   the layout the file's own note calls "82-128px taller" and "what broke act
   5" -- inside a pinned frame of fixed height. Simulated at 1100 by deleting
   exactly this band's rules: .act-print .act-inner 680.0 -> 781.2px at
   1100x900 and 474.0 -> 594.4px at 1100x700. Fractional widths are reachable
   under browser zoom and fractional display scaling, which is the same seam
   oyt-stage-mobile.js already hardened in JS at 900/901 and this file had not
   hardened in CSS at 1100/1101. Chromium quantises CSS pixels to 1/64, so no
   reachable width lands in (1100.99, 1101). */
@media (min-width: 901px) and (max-width: 1100.99px) {
  /* RUN 06 — THESE THREE LINES USED TO REFLOW AND THAT IS WHAT CLIPPED ACT 5.
     Below 1101 the promises fell to a 3-column, two-row list and the moves to
     2x2. MEASURED, same container and the same type tiers, act 5's required
     height 3-up against 5-up: 1100x700 630 vs 548 · 1024x768 750 vs 658 ·
     950x650 597 vs 515 · 901x900 932 vs 804 · 901x700 698 vs 615 · 901x650
     597 vs 515. THE 3-UP FALLBACK IS 82-128px TALLER, and at 901x900 it still
     clipped by 32px after every other fix in this run. Reflowing is what
     broke act 5; it was never what saved it.
     Holding 5-up repairs a second thing: `.path::before` is ONE rule across
     the whole <ol>, so at 3-up the second row's nodes hung off nothing.
     THE PRICE, named rather than hidden: at 901 a 5-up promise column is
     148px, which is 22-26 characters of caption against today's 3-up 30.
     That is the trade for not losing words, and it is why this block exists
     rather than simply being deleted.
     Also: the query gains `(min-width: 901px)`, because below that the phone
     sheet owns the layout and this was reaching into it. */
  .stage .promises { grid-template-columns: repeat(5, 1fr); }
  .stage .moves { grid-template-columns: repeat(4, 1fr); }
  .stage .path { grid-template-columns: repeat(5, 1fr); row-gap: 0; }
  /* act 3's headline cap has exactly one owner per width regime now: it is
     20ch here and `max-width: none` on the 7-column bench above 1100. */
  .act-math h2 { max-width: 20ch; }
}
/* ═══════════════════════════════════════════════════════════════════════════
   RUN 04 — CORRECTIONS TO THE ABOVE, kept here rather than edited in place
   so the reason for each survives.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── ACT 2, THE POLISH PASS ───────────────────────────────────────────────
   THE HEAT. --tile-heat is written by oyt-stage.js from how far through its
   own journey a tile is: it peaks at the midpoint of the trip and is zero at
   both ends. So a tile in flight is lit and a tile at rest is not, which is
   the hero's rule — gold only shows where the metal is warm — applied to
   motion instead of to a plate. At rest the value is absent, and `0` is what
   the fallback supplies, so a phone and a reduced-motion render show the wall
   cold and finished. */
.tile {
  --tile-heat: 0;
  border-radius: 10px;
}
.tile::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--tile-heat);
  background: linear-gradient(140deg, rgba(245, 197, 66, 0.16), transparent 52%);
  box-shadow: 0 0 0 1px rgba(245, 197, 66, 0.30), 0 8px 26px rgba(245, 197, 66, 0.10);
}
/* the drafting tick — a registration mark in the top-right of every plate,
   in the hero's drawn-not-rendered idiom. 1px, gold, and quiet. */
.tile::before { z-index: 1; }
.tile .tile-mark { position: relative; z-index: 2; }
.tile .tile-name { position: relative; z-index: 2; }
.tile-tick {
  position: absolute;
  top: 9px; right: 9px;
  width: 9px; height: 9px;
  border-top: 1px solid rgba(245, 197, 66, 0.42);
  border-right: 1px solid rgba(245, 197, 66, 0.42);
  z-index: 2;
  pointer-events: none;
}
/* the hairline under the glyph — the plate's own rule, the way the hero's
   dashboard rules its scale */
.tile-rule {
  height: 1px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), transparent 70%);
  margin: 2px 0 0;
  z-index: 2;
  position: relative;
}

/* The cluster rule draws itself from the label outward as the cluster
   resolves, rather than appearing at full length. */
.cluster-rule { width: 26px; }

/* .tile no longer declares its own will-change: the scrub media query above
   already holds a layer for every [data-tile], and two declarations of the
   same intent is one of them going stale. */
.tile { will-change: auto; }

/* ── THE TELL IS GONE ─────────────────────────────────────────────────────
   RUN 06. Every tile carried a gold pip whose position on its bottom edge
   named its cluster, lit before the wall sorted and out as the labels landed:
   the board stating the rule before it applied it. It is removed on
   instruction — "remove the small yellow dots on the tool cards" — and with
   it go --tile-tell, the .tile-pip markup, the three :nth-child rules that
   placed it, and the TELL window in oyt-stage.js.

   What replaces the argument it made is the arrival itself: the wall now
   OPENS OUTWARD FROM ITS OWN CENTRE rather than scattering, so the reader
   sees one system moving before they see twelve tiles. See buildWall's
   measure() in oyt-stage.js. */

/* The signpost between act 1's lede and its five stations. Three words, no
   claim: it names what the row below is, so the act lands a few words at a
   time instead of as one wall. */
/* THE SIGNPOST BETWEEN ACT 1'S LEDE AND ITS FIVE STATIONS — three words, no
   claim: it names what the row below is, so the act lands a few words at a
   time instead of as one wall.

   RUN 06 TAKES IT OFF THE BLOCKY FACE. It was 11.5px mono, 700, 0.18em,
   UPPERCASE and gold — the same voice as the act eyebrow eight lines above
   it, at the same size, which made the act open with two labels rather than
   with a heading and a subtitle. It is the body face now, at 400, at rest
   tracking, in sentence case, and nearly twice the size: a SUBTITLE that
   introduces the instrument under it. The copy is untouched — `text-transform`
   was the only thing making "Where it leaks." shout. Cream rather than gold,
   because gold on this stage belongs to what is warm and this line is a
   caption; the instrument below it is where the gold goes. */
.path-lead {
  margin-top: var(--v3-7);
  font-family: var(--v3-body);
  font-size: clamp(17px, 1.5vw, 21px);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
  color: var(--v3-ground-ink);
}
.path-lead + .path { margin-top: var(--v3-5); }

/* ═══════════════════════════════════════════════════════════════════════════
   RUN 06 — THE BENCH: ONE GRID, FIVE PLACEMENTS, AND THE SCALE-UP
   "Everything bigger and more cinematic. The stage currently uses ~55% of a
   1440 screen with the content pinned left and dead space right."

   WHY IT READ AS 55%. Two separate things, and only one of them was the
   container. (a) The deleted rail's lane held `.stage .act` at
   `padding-left: 218px` with `.act-inner { margin-left: 0 }` between 901 and
   1499, so the content band at 1440 ran 218..1398 — asymmetric, with 42px of
   dead right gutter. (b) Every run of prose was capped much narrower than
   that: the lede at 62ch is 625px inside a 1180px box, so acts 1, 4 and 5
   used the left half of the frame and nothing else.

   AND THE MEASUREMENT SAYS THE CAPS MUST COME DOWN, NOT UP. Walked with a
   Range on the shipped page at 1440x900, characters per rendered line:
     .act-lede   62ch -> 625px -> 75-79 chars   near the ceiling
     .math-gap   74ch -> 746px -> 90 chars      OUTSIDE the 45-85 band
     .wall-rest  86ch -> 650px -> 94 chars      OUTSIDE
     .act-close  82ch -> 689px -> 103-107       WELL outside, in acts 4 AND 5
   `ch` IS NOT A CHARACTER. Hanken Grotesk's `0` is 0.5600em and this page's
   own mean advance is 0.4474em, so 1ch = 1.2517 real characters and 64ch is
   EIGHTY characters. JetBrains Mono is 1.0000, so a mono ch IS a character
   and the two 74ch caps in this file mean different things — never unify
   them. State every cap as the pair or someone will widen it straight back
   out of the band.

   So the width the rail's lane returns does not go into line length. It goes
   into LAYOUT: a 12-column bench with a different void in every act.

   THREE LIVE DEFECTS THIS CLOSES, all measured on the shipped page:
     D1  ACT 5 WAS CLIPPED. Slack -10px at 901x900 and -29px at 901x650. The
         cause is the max-width:1100 reflow, above.
     D2  ACT 1'S EYEBROW SAT INSIDE THE FIXED NAV at short viewports. #nav is
         fixed, 64px, z-index 1000, and paintNav fades it over the first 9
         notches of MASTER progress — a whole screen during which act 1 owns
         the frame. Act 1's first-ink top inset was 50px at 950x650 and at
         901x650. That is the run's own "nothing occluded by fixed chrome"
         gate failing. After: >= 103px everywhere, worst clear by 39px.
     D3  THE COST TABLE SCROLLED HORIZONTALLY AT EVERY WIDTH 1101-1439. The
         old .math-grid split 1.06fr/1fr and .math-table carried 24px of dead
         padding-right against its own `min-width: 560px`. Measured overflow:
         151px at 1101, 110px at 1180, 59px at 1280. In the act that carries
         the whole pitch. After: 0 at 1101-1920.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE CONTAINER AND ITS KNOBS ──────────────────────────────────────────
   The vertical knobs are banded rather than fixed: below 1101 the acts are
   one stacked column and the air has to be tight; on the bench there is room
   for it. Both bands are clamps on vh, so most of the vertical rhythm scales
   with the frame and the short-height tiers at the end of this file only
   have to move TYPE — which is why there are three mild tiers instead of two
   hard ones. */
:root {
  --act-max: 1560px;
  --act-gap: 24px;
  --act-pad: clamp(32px, 3.6vw, 80px);
  --path-hang: clamp(26px, 4vh, 44px);
  --path-tail: 0px;
  --lead-gap: clamp(20px, 3.5vh, 40px);
  --list-gap: clamp(20px, 4vh, 48px);
  --li-pad: clamp(12px, 2vh, 20px);
  --tile-h: clamp(88px, 12vh, 110px);
  --wall-gap: clamp(24px, 4vh, 48px);
  --close-gap: clamp(18px, 3.5vh, 40px);
  --move-tail: var(--v3-6);
}
@media (min-width: 1101px) {
  :root {
    --path-hang: clamp(32px, 8vh, 96px);
    --path-tail: clamp(0px, 9vh, 108px);
    --lead-gap: clamp(28px, 9vh, 108px);
    --list-gap: clamp(24px, 6.5vh, 80px);
    --li-pad: clamp(12px, 2.8vh, 30px);
    --tile-h: clamp(92px, 15vh, 148px);
    --wall-gap: clamp(28px, 8vh, 96px);
    --close-gap: clamp(20px, 5vh, 60px);
    --move-tail: clamp(24px, 9vh, 108px);
  }
}
@media (min-width: 1600px) { :root { --act-gap: 32px; } }

/* NO reduced-motion clause on this query, and that is a fix rather than an
   oversight: LAYOUT IS NOT MOTION. The rules it replaces sat inside
   `(min-width:901px) and (prefers-reduced-motion: no-preference)`, so a
   reduced-motion desktop got a CENTRED 1180 while the animated path got a
   LEFT-SHIFTED 1180 — two desktop layouts, one of which nothing measured.
   There is one now. */
@media (min-width: 901px) {
  .stage .act {
    /* RUN 07 — SYMMETRIC, AND NOTHING UNDER IT. Until this run these two were
       `max(var(--act-pad), 64px)`: a 64px floor that existed only to keep a
       glyph out of the progress instrument's lane. The instrument is deleted,
       so the floor is deleted with it and the act is back on the container
       scale it was authored in.

       MEASURED, act 1's band, before -> after (harness/lane.js):
         1920  1782 -> 1782   3.6vw is 69.12 here; the floor never bound
         1600  1472 -> 1485   +13
         1440  1312 -> 1336   +24
         1280  1152 -> 1188   +36
         1101   973 -> 1022   +49
         1024   896 ->  950   +54
          901   773 ->  836   +63
       The floor bound everywhere below 1778 (3.6vw = 64 at 1777.8), and it
       bound HARDEST at the narrow end, which is the band with the least to
       spare. Run 06's report claimed the band at 1440 was already 52..1388;
       it was 64..1376, because max(51.84px, 64px) is 64. It is 52..1388 now. */
    padding-left: var(--act-pad);
    padding-right: var(--act-pad);
    padding-top: clamp(28px, 6vh, 80px);
    padding-bottom: clamp(28px, 6vh, 80px);
  }
  .act-inner,
  .act-inner-wide { max-width: var(--act-max); margin-inline: auto; }
}
/* MEASURED CONTAINERS (getBoundingClientRect, engine stubbed):
     vw    pad     band            width   col(12)  pitch
     1920  80.00   180..1740       1560    100.67   132.67
     1600  57.60    58..1542       1485     92.00   124.00
     1440  51.84    52..1388       1336     89.37   113.37
     1280  46.08    46..1234       1188     77.00   101.00
     1101  39.64    40..1061       1022     63.17    87.17
     1024  36.86    37.. 987        950     57.17    81.17
      901  32.40    32.. 869        836     47.67    71.67
   At 1440 the band goes from 218..1398 (asymmetric, 82% of the frame) to
   52..1388 (symmetric, 92.8%). At 1920 from a centred 1180 (61.5%) to 1560
   (81.3%). At 901 the act gains 177px, which is what the rail's lane pays
   for and most of D1's fix. */

/* ═══ EVERYTHING FROM HERE TO THE BENCH IS DESKTOP ════════════════════════
   RUN 06, and the query is a correction rather than tidiness. Every rule in
   this run is a `.stage .X` at (0,2,0) or (0,2,1), and oyt-stage-mobile.css
   states the phone's own type as bare classes at (0,1,0) — so unscoped, the
   desktop scale-up SILENTLY WON ON THE PHONE.

   It was not theoretical. The phone's act 1 is a five-row grid whose rows are
   sized for a 13.5px leak line; at the desktop's 16px/1.6 the mobile geometry
   gate measured four of five stations painting over the next station's label
   at 390, 375 and 360 — 105px of copy in a 102px row, and 128 in 105 at the
   narrow-phone step. Act 1's phone choreography is gated GOOD and inherits
   only type, colour and copy; the type it inherits is stated in its own sheet
   and measured against its own rows, which is what the block at the end of
   oyt-stage-mobile.css now does.
   ═══════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  /* ── THE ACT TITLE — LESS BLOCKY, SLIGHTLY SMALLER, SAME FACE ─────────────
     Set on `.stage .act h2` (0,2,1) and NOT by retuning --v3-fs-display:
     `.act h2` and `.hero h1` both read that token, so retuning it restyles the
     frozen hero.

     "Blocky" is ink over ground inside the block, and Anton at 0.94 leading
     with -0.005em tracking is engineered to have almost none. Three levers,
     in the order they matter:
     (a) LEADING 0.94 -> 1.05 is the whole game. On a two-line headline at
         51.84px the interline ground goes from NEGATIVE — the em boxes
         overlap, cap-to-cap about 8px — to positive, about 17px. The glyphs
         shrink while the block grows per line, so the ink/ground ratio falls
         about a third. And it is nearly free: 2 x 51.84 x 1.05 = 108.9px
         against today's 2 x 63.36 x 0.94 = 119.1px.
     (b) TRACKING -0.005em -> +0.016em. Measured on the real face: act 4's
         longest line renders 638px today at 63.36px; at 51.84px and +0.016em
         it is about 550px. Uppercase advance goes 0.3873em -> 0.4083em, i.e.
         +15px of ground inside a 26-character line. Anton is a poster face;
         opening it a hair is the difference between a shout and a drawing,
         and it rhymes with the mono eyebrow above it at 0.16em.
     (c) SIZE -18%: 72 -> 58 at 1920, 63.36 -> 51.84 at 1440.

     REJECTED — A LIGHTER INK. The lede sitting beside the headline is already
     --v3-ground-dim, so dimming the title collapses the act to one value; and
     the ground under it is now a per-act gradient beneath a lamp whose alpha
     the engine writes per frame, so any ink chosen for a ground that "stays
     dark" would have a contrast ratio that is a function of scroll position.
     Take the weight out of the geometry, never out of the light.
     REJECTED — DROPPING UPPERCASE. `.hero h1` is uppercase Anton on the same
     token and every display run on this page is uppercase. One section in
     sentence case reads as a mistake rather than a choice. It is the single
     biggest available reduction in blockiness and it still is not worth it.

     CONSEQUENCE WORTH NAMING: every h2 on the stage is now two lines at every
     width >= 1101. Act 3's is three today at 1440; on 7 columns with balance
     it is two, which is 60px of act 3 recovered and one fewer ragged edge.

     RUN 13 OVERTAKES (a), (b), (c) AND THE REJECTED-UPPERCASE CALL ABOVE, on
     Jake's eye rather than on this note's argument: the act head is sentence
     case at the section-head family size (36px on every desktop width; the
     clamp's 8vw is only ever the phone's ramp), tracked 0.01em because
     lowercase carries its own fit, set 1.15 because lowercase has ascenders
     and descenders. The history above is kept because its measurements are
     still true of what caps at 52px cost; the decision it reaches is
     superseded. The hero h1 is unchanged and is now the page's one caps run. */
  .stage .act h2 {
    font-size: clamp(28px, 8vw, 36px);
    line-height: 1.15;
    letter-spacing: 0.01em;
    margin-bottom: var(--v3-5);
  }

  /* THE REGISTRATION TICK ABOVE THE EYEBROW, and it is not decoration. Once
     act 1's head is flush left, act 4's is inset two columns and act 5's is
     centred, an offset block with no top edge floats; a drafting sheet
     registers. SHORT rather than a full-width rule, so it reads as a
     registration mark and not as a divider, and so it does not compete with
     act 1's drawn gold path rule 300px below it. Five static 1px boxes inside
     .act's existing layer: empty content, no accessible name, never written
     to by script. */
  /* ── THE ACT'S OWN NAME, RUN 08 ────────────────────────────────────────
     THE ACT EYEBROW IS THE ACT'S TITLE -- THE MONEY PATH, WHAT'S INSIDE,
     THE MATH, WE MOVE YOU, NO FINE PRINT -- and it was 12px at every width
     from 901 to 1920, which is SMALLER THAN THE HERO'S OWN EYEBROW (14px)
     and 0.23 of the heading under it at 1440. Measured before: all five,
     every shape, 12px.

     16px, which is .cluster-label's number. That is deliberate rather than
     convenient: act 2 already uses 16px mono gold at 0.22em for FIELD /
     MONEY / GROWTH, on the finding that "the cluster labels are the act's
     own headings", and an act's own name cannot be smaller than the label
     of a group inside it. The eyebrow keeps 0.16em -- it is a line of two
     or three words, not a three-word ladder, and 0.22em on "THE MONEY PATH"
     runs 40px wider than the tick above it.

     THE SIZE IS SET HERE AND NOT ON --v3-fs-eyebrow: that token is read by
     the hero, which is frozen by hash. Confirmed by the gate rather than by
     this comment -- harness/titles.js prints the hero's h1 and eyebrow on
     the same row as the acts', and hero-hash.js hashes five viewports.

     THE THREE SHORT-FRAME TIERS STEP IT DOWN with everything else they own
     (16 -> 15 -> 14), because the act frames those queries exist for are the
     ones with nothing to give. */
  .stage .act-eyebrow { position: relative; padding-top: clamp(10px, 1.6vh, 18px); font-size: 16px; }
  .stage .act-eyebrow::before {
    content: '';
    position: absolute;
    left: 0; top: 0;
    height: 1px;
    width: clamp(72px, 7vw, 120px);
    background: linear-gradient(90deg, var(--v3-gold), rgba(245, 197, 66, 0.10));
  }
  /* ACT 5's TICK IS ONLY DOUBLE-ENDED HERE. Centring it is the bench's job
     and lives with the bench, below: `left: 50%` resolves against the
     eyebrow's own box, and between 901 and 1100 there is no grid, so that box
     is the full frame while the words inside it are still left-aligned.
     Measured absolute x at 1024x900: text 64..183, tick 476..548 -- a 72px
     gold rule hanging in empty ground 293px right of NO FINE PRINT, and the
     only free-floating mark on the stage. At 1101 the text jumps to 491..610
     and lands under it. The gradient is width-independent and stays here. */
  .stage .act-print .act-eyebrow::before {
    background: linear-gradient(90deg, rgba(245, 197, 66, 0.10), var(--v3-gold), rgba(245, 197, 66, 0.10));
  }

  /* ── THE MEASURES. 64ch = 80 characters, 58ch = 72 characters, at any font
     size — the ratio is size-independent. The grid column is the working
     measure and these caps are a ceiling that only binds at 1600+. Measured
     result at 1440: lede 58-74 characters and close 81-82, both inside 45-85,
     where three runs were outside it before. (The third was .act-note's 70,
     and run 08 deletes that element.) */
  .act-lede { max-width: 58ch; font-size: clamp(15px, 1.2vw, 19px); }
  .act-close,
  .wall-rest,
  .math-gap { max-width: 64ch; }

  /* ── SCALE THE VISUALS UP, act by act ─────────────────────────────────────
     THE CAPTION-GRID EXEMPTION, stated rather than smuggled: four rows here
     are 4-up or 5-up caption grids, not running prose, and a five-across
     station row cannot have a 45-character measure at any width without
     ceasing to be five across. Every one of them is WIDER and LARGER than it
     was: at 1440 the station column goes 217 -> 248px with .path-leak
     14 -> 16px, and .moves p 277 -> 316px at 13.5 -> 15.5px. */

  /* ACT 1 — the money path now spans 1336 rather than 1180, and its gold rule
     is the widest line on the stage. --path-tail is frame-filling air,
     honestly labelled, and it is on .path and NOT on .path-step: .path-step IS
     [data-step] and is promoted to its own compositing layer, while .path
     (which carries [data-flow]) is not — the same picture for about 90,000
     fewer composited pixels a frame. */
  .stage .path-lead { margin-top: var(--lead-gap); }
  .stage .path-lead + .path { margin-top: clamp(14px, 3vh, 32px); }
  .stage .path { column-gap: var(--act-gap); padding-top: var(--path-hang); padding-bottom: var(--path-tail); }
  .stage .path-n { font-size: 16px; margin-bottom: var(--v3-4); }
  .stage .path-leak { font-size: 16px; line-height: 1.6; }

  /* ACT 2 — the toolwall. Measured tile 180x98 -> 202x135 at 1440 (+54.6% of
     area) and 236x148 at 1920; the wall box 1180x242 -> 1336x319; glyph area
     +86%. */
  .stage .wall { margin-top: var(--wall-gap); gap: clamp(28px, 3vw, 48px); }
  .stage .cluster-tiles { gap: clamp(10px, 1.4vh, 16px); }
  .stage .tile { min-height: var(--tile-h); padding: 18px 18px 16px; }
  .stage .tile-mark { width: 30px; height: 30px; }
  .stage .tile-name { font-size: 14px; }
  .stage .tile-tick { width: 12px; height: 12px; top: 11px; right: 11px; }
  /* C8 — THE CLUSTER LABELS ARE THE ACT'S OWN HEADINGS, and at 11.5px mono
     they were the same size as the tick marks. FIELD / MONEY / GROWTH is the
     rule the wall sorts itself by, and it is what the reader is meant to be
     left holding, so it is 16px at 700 with the tracking opened to 0.22em and
     the rule under it doubled in length and thickness. It stays mono and it
     stays gold: this is a label, not a second display voice. */
  .stage .cluster-label { font-size: 16px; letter-spacing: 0.22em; margin-bottom: var(--v3-5); gap: 14px; }
  .stage .cluster-rule { width: 48px; height: 2px; }
  .stage .wall-rest { margin-top: clamp(18px, 4vh, 44px); font-size: 15px; }

  /* ACT 3 — the math. The table column goes 574 -> 770px (+34%). */
  /* RUN 11 WIDENS THE COLUMN GUTTER 20 -> 28px and the row band 16 -> 18.
     A fourth column on a table that is also 73% wider is more air to place,
     not less: at 20px the four headings sat closer to each other than the
     three did. The TYPE LADDER IS RUN 08'S, unchanged — 13.5/500 headings,
     16.5/700 row headings, 16.5/500 values — because those three numbers came
     out of an ink probe and a contrast bar, and neither moved. */
  .stage .math-table th,
  .stage .math-table td { font-size: 16.5px; padding: 18px 28px 18px 0; }
  .stage .math-table th:last-child,
  .stage .math-table td:last-child { padding-right: 0; }
  .stage .math-table thead th { font-size: 13.5px; }
  .stage .math-table .keep { font-size: 18px; }

  /* ACTS 4 AND 5. The step column goes 277 -> 316px in act 4 and 217 -> 248px
     in act 5. --move-tail is act 4's own frame-filler; act 5 gets none,
     because it is the tallest act and has none to give. */
  /* the plates carry their own type; what the scale-up owns here is the air */
  .stage .act-close { margin-top: var(--close-gap); font-size: 16px; }
  /* ACT 5's CLOSE, AND THE CAP THAT DID NOT DO WHAT ITS COMMENT SAID.
     It read `max-width: 96ch` and claimed the grid kept the box at eight
     columns so the cap never bound. BOTH HALVES WERE WRONG. It binds at the
     widths this run photographs -- at 1440 the eight-column track is 866.7px
     against the cap's 779.5 -- and this rule lives in a min-width: 901px
     query where, from 901 to 1100, there is no bench grid at all and the cap
     is the only thing governing. Walked with a Range, the rendered line ran
     119, 117, 37 characters at 1440 and 119, 121, 33 at 1024: the longest run
     of prose on the stage, the one carrying the $99/year term, at 40% wider
     than act 4's close directly above it and half as wide again as the 45-85
     band this run re-capped every other measure to.
     64ch IS ACT 4's OWN NUMBER and it is the one that changes the render.
     The paragraph is 273 characters, so three lines cannot be had at under 91
     each -- 72ch was tried first and measured exactly that, 91/91/91 at every
     width from 901 to 1920, still outside the band. Anything at or under 67ch
     breaks it to four, and at 64ch the lines measure 68 characters, the
     middle of the band and the same cap the act above it uses. The fourth
     line is ~23px in the tightest act on the stage, so a fourth short-frame
     tier at 880px was added below to pay for it, and the fit gate was re-run
     at all twelve shapes. */
  .stage .act-move .moves { margin-bottom: var(--move-tail); }
}

/* ═══ THE 12-COLUMN BENCH — 1101px AND ABOVE ══════════════════════════════
   WHY 1101 AND NOT 1280. The alternation is capped by act 4's headline,
   which is the binding type constraint on this stage: "AND YOU DON'T DO IT
   TWICE." is 26 uppercase characters and needs six columns. Measured span-6
   widths against it: 1440 = 656px vs 550 needed · 1280 = 582 vs 494 ·
   1101 = 498 vs 425 · 950 = 429 vs 429, one pixel · 901 = 406 vs 429, which
   fails. 1101 is also this file's own existing seam, so no new breakpoint.
   BELOW 1101 there is no grid: one stacked column, exactly as before, but in
   the new 836-1021px container rather than the old 659-858px one.

   "OFFSET" NUMERICALLY, at 1440: act 1's head runs x=52..595 with its lede
   at 619..1161 and 227px of void to the RIGHT; act 4's head runs x=279..935
   with its lede at 959..1388 and 227px of void to the LEFT. The same void on
   opposite sides — a mirror, not a second idea. Act 2 pins the head's two
   ends to the frame's two ends; act 5 is symmetric, void 1-2 and 11-12.
   ═══════════════════════════════════════════════════════════════════════ */
@media (min-width: 1101px) {
  .act-inner,
  .act-inner-wide {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--act-gap);
    row-gap: 0;
  }
  /* THE SAFETY NET, and `:where()` in it is LOAD-BEARING. .m-head and
     .m-panel are `display: contents`, so THEIR CHILDREN are the grid items.
     Written as `.act-inner > .m-head > *` this rule is (0,2,1) and silently
     outranks every `.act-money h2` (0,1,1) below it — all five acts then
     render full width with the headline and the lede painted on top of each
     other, AND IT STILL MEASURES AS FITTING, because two overlapping items
     are shorter than two stacked. :where() makes it (0,1,0).
     `display: contents` on those two wrappers is what makes this whole grid
     work at all, and it works only because neither wrapper appears in the
     engine's selector list. Any of transform, will-change or a style write
     on either one generates a box and collapses all five acts into column 1
     at once. */
  .act-inner > *,
  .act-inner > :where(.m-head, .m-panel) > * { grid-column: 1 / -1; }

  /* ACT 1 — OFFSET LEFT. Void: columns 11-12. */
  .stage .act-money .act-eyebrow { grid-column: 1 / span 5; grid-row: 1; }
  .stage .act-money h2           { grid-column: 1 / span 5; grid-row: 2; }
  .stage .act-money .act-lede    { grid-column: 6 / span 5; grid-row: 2; align-self: end; }
  .stage .act-money .path-lead   { grid-column: 1 / -1;     grid-row: 3; }
  .stage .act-money .path        { grid-column: 1 / -1;     grid-row: 4; }

  /* ACT 2 — FULL WIDTH. The widest head separation of the five. */
  .stage .act-wall .act-eyebrow { grid-column: 1 / span 5; grid-row: 1; }
  .stage .act-wall h2           { grid-column: 1 / span 6; grid-row: 2; }
  .stage .act-wall .act-lede    { grid-column: 8 / span 5; grid-row: 2; align-self: end; }
  .stage .act-wall .wall        { grid-column: 1 / -1;     grid-row: 3; }
  .stage .act-wall .wall-rest   { grid-column: 1 / span 6; grid-row: 4; }

  /* ACT 3 — TABLE LEFT / PAYOFF RIGHT on a real 7|5 meridian, and THE HEAD
     SPLITS ON THE SAME SEAM, so the argument has one seam top to bottom.
     This is what makes act 3 stop reading as a different page. */
  .stage .act-math .act-eyebrow { grid-column: 1 / span 5; grid-row: 1; }
  .stage .act-math h2           { grid-column: 1 / span 7; grid-row: 2; max-width: none; }
  .stage .act-math .act-lede    { grid-column: 8 / span 5; grid-row: 2; align-self: end; }
  .stage .act-math .math-grid   { grid-column: 1 / -1;     grid-row: 3; }
  .stage .act-math .math-cta    { grid-column: 8 / span 5; grid-row: 4; align-self: start; }

  /* A nested identical 12-column grid lands on the parent's exact tracks —
     no subgrid, no new failure mode. RUN 07: the table keeps its --math-pad
     gutter here. Zeroing it was run 06's fix for D3 and it traded a scrollbar
     for a clipped first column; the min-width is 460 now and both are closed
     at once.

     RUN 08 MADE THE BENCH'S TWO COLUMNS THE TWO PANELS, and run 11 leaves
     ONE. The finding that put them there is kept because it is still the
     reason this is a grid at all and not a pair of floats: a grid ROW is as
     tall as the tallest thing in it ACROSS BOTH COLUMNS, so while there were
     four items in a 2x2 the cost-gap line's distance from the table was a
     function of how tall the RIGHT column's first item happened to be — 32px
     as authored, 99px once the right column was one 598px plate. One item per
     column fixed it; one column cannot have the problem.
     The panels are display: contents by default (oyt-stage.css, .m-head and
     .m-panel) and this is the one query where they are not. The phone is
     unaffected: the mobile sheet is loaded at media="(max-width: 900px)", so
     nothing in it can see this query. */
  .math-grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--act-gap);
    align-items: start;
    margin-top: clamp(24px, 5vh, 56px);
  }
  .math-grid > .m-panel { display: block; grid-row: 1; }
  /* RUN 11 — ONE PANEL, ALL TWELVE TRACKS. The bench was 7|5, table left and
     the payoff plate right, and the head split on the same seam so the act
     had one meridian top to bottom. The plate is gone and there is no right
     column to hold a seam with, so the table takes the act's whole measure:
     770 -> 1336px at 1440x900. THE HEAD KEEPS ITS SEAM — h2 on 1/7, lede on
     8/12 — which is where run 08's note put the argument for it ("the head
     splits on the same seam, so the argument has one seam top to bottom");
     what has changed is that the body is now one object rather than two, so
     there is nothing below the head for a seam to separate. */
  .math-grid > .m-panel[data-panel='2'] { grid-column: 1 / -1; }
  .math-grid .math-table  { margin-top: 0; padding: 18px var(--math-pad) 14px; }
  .math-grid .math-gap    { margin-top: var(--v3-4); }

  /* ACT 4 — OFFSET RIGHT, the mirror. Void: columns 1-2. The head shifts
     right AS A UNIT, so reading order stays title-then-lede and is never
     mirrored. The four plates keep the full twelve columns underneath it —
     one card per flight corridor, which is what makes the alternating
     entrance provably non-occluding. */
  .stage .act-move .act-eyebrow { grid-column: 3 / span 6; grid-row: 1; }
  .stage .act-move h2           { grid-column: 3 / span 6; grid-row: 2; }
  .stage .act-move .act-lede    { grid-column: 9 / span 4; grid-row: 2; align-self: end; }
  .stage .act-move .moves       { grid-column: 1 / -1;     grid-row: 3; }
  .stage .act-move .act-close   { grid-column: 3 / span 6; grid-row: 4; }

  /* ACT 5 — CENTRED. Void: 1-2 and 11-12, symmetric. The closing disclosure
     is a CENTRED BOX with LEFT-ALIGNED text, spanning 8 so the 64ch cap
     governs: at span 6 the centring cost it a fifth line, +19px, in the
     tightest act on the stage. */
  .stage .act-print .act-eyebrow    { grid-column: 4 / span 6; grid-row: 1; text-align: center; }
  /* and the registration tick centres WITH the words, not without them */
  .stage .act-print .act-eyebrow::before { left: 50%; transform: translateX(-50%); }
  .stage .act-print h2              { grid-column: 3 / span 8; grid-row: 2; text-align: center; }
  /* SPAN 8, NOT 6. At six columns act 5's lede is three lines and act 5 has
     no slack to spend on one; at eight it is two, and 872px at 19px is 73
     characters — inside the readable band either way. */
  .stage .act-print .act-lede       { grid-column: 3 / span 8; grid-row: 3; text-align: center; margin-inline: auto; max-width: 78ch; }
  .stage .act-print .promise-stack  { grid-column: 1 / -1;     grid-row: 4; }

  /* THE 3+2 COURSE, on its own six-track grid inside the twelve. The seats
     and the rays are authored in the same declaration block, so a ray cannot
     point somewhere the grid does not. */
  .stage .promises { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .stage .promises > li:nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
  .stage .promises > li:nth-child(2) { grid-column: 3 / 5; grid-row: 1; }
  .stage .promises > li:nth-child(3) { grid-column: 5 / 7; grid-row: 1; }
  .stage .promises > li:nth-child(4) { grid-column: 2 / 4; grid-row: 2; }
  .stage .promises > li:nth-child(5) { grid-column: 4 / 6; grid-row: 2; }
}

/* ═══ SHORT FRAMES — THREE TIERS, AND NOTHING IS CUT ═══════════════════════
   A pinned frame is exactly one viewport tall and `overflow: hidden`, so an
   act taller than the frame is CLIPPED and the words at the bottom of it are
   simply gone.

   EVERY SELECTOR HERE IS (0,3,x), WRITTEN AS `.stage .pin-frame .act ...`,
   AND THAT IS NOT STYLE. The scale-up rules above are `.stage .X` at (0,2,0)
   and the old tiers were `.pin-frame .X` at (0,2,0) — equal specificity, so
   source order decided, and the scale-up silently nullified the entire
   short-viewport system. Act 5 then clipped by 62px at 950x650 and it looked
   like a spacing bug.

   The boundary is 820 and not the 880 it was, because at 1440x840 the new
   layout has 63-150px of slack with no tier at all — measured. Most of the
   vertical rhythm is on vh now, so the tiers only have to move TYPE.

   MEASURED FIT, need px / slack inside the pinned frame, engine stubbed:
     viewport    act1        act2        act4        act5
     1920x1080   839 (+241)  874 (+206)  795 (+285)  832 (+248)
     1440x900    742 (+158)  786 (+114)  716 (+184)  809 ( +91)
     1366x768    644 (+124)  655 (+113)  604 (+164)  717 ( +51)  <- worst
     1280x800    676 (+124)  668 (+132)  631 (+169)  720 ( +80)
     1024x768    628 (+140)  668 (+100)  618 (+150)  704 ( +64)
      950x650    484 (+166)  552 ( +98)  467 (+183)  507 (+143)
      901x900    721 (+179)  766 (+134)  685 (+215)  827 ( +73)
      901x650    505 (+145)  552 ( +98)  467 (+183)  507 (+143)
   Before this run, for comparison: 1440x900 +226/+175/+228/+146, so the
   frame is FULLER everywhere and everything in it is bigger; 901x900
   +114/+183/+147/-10 and 901x650 +34/+109/+90/-29. Worst case goes from
   -29px to +51px.

   IF IT EVER DID NOT FIT, in order: the title (one clamp step is ~35px of a
   ~780px act and a two-line headline stays two lines), then the vh
   coefficients --list-gap / --move-tail / --path-tail, which are air by
   construction, then body sizes LAST and never below 12.5px. NEVER a word, a
   line, a display:none, or the 5-up / 4-up / 5-up structure.
   ═══════════════════════════════════════════════════════════════════════ */
/* A FOURTH TIER, AND IT IS ACT 5's ALONE. 1440x840 is above every tier this
   file had, so nothing stepped down there, and act 5's close gaining a fourth
   line from its new 72ch cap put the act 7px past its frame at exactly that
   shape. The concessions are taken in this file's own documented order: the
   plate's air first (min-height and padding, 168/18-22-16 -> 156/16-20-14,
   twice over because the course is two rows) and body size last and never
   below 12.5px. The ladders read 168 -> 156 -> 148 -> 138 -> 132 and
   14.5 -> 14 -> 13.5 -> 13 -> 12.5, monotone at every step. */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) and (max-height: 880px) {
  .stage .pin-frame .act .card.promise-card { min-height: 156px; padding: 16px 20px 14px; }
}

@media (min-width: 901px) and (prefers-reduced-motion: no-preference) and (max-height: 820px) {
  /* RUN 13: the h2 ladder is 36 -> 34 -> 32 -> 30 at 1440 now (was 58 -> 50
     -> 40 -> 36), monotone under the new 36px family size. */
  .stage .pin-frame .act h2 { font-size: clamp(28px, 2.4vw, 34px); margin-bottom: var(--v3-4); }
  .stage .pin-frame .act .act-lede { font-size: 16px; }
  .stage .pin-frame .act .act-close { font-size: 15px; }
  .stage .pin-frame .act .path-leak { font-size: 15px; line-height: 1.55; }
  .stage .pin-frame .act .path-n { font-size: 14.5px; }
  .stage .pin-frame .act .card h3 { font-size: 17px; }
  .stage .pin-frame .act .card p { font-size: 13.5px; line-height: 1.45; }
  .stage .pin-frame .act .card { min-height: 190px; padding: 18px 20px 16px; }
  /* ACT 5 IS THE BINDING CASE at every short height. Measured against the
     act-fit gate: at 1366x768 the course above clipped by 26px, at 1280x800
     by 1 and at 1101x700 by 2, so its plate gets its own step here rather
     than sharing act 4's. */
  .stage .pin-frame .act .card.promise-card { min-height: 148px; padding: 15px 18px 13px; }
  .stage .pin-frame .act .promise-card h3 { font-size: 16.5px; }
  .stage .pin-frame .act .promise-card p { font-size: 13px; }
  .stage .pin-frame .act .promise-card .card-rule { margin: 9px 0 11px; }
  .stage .pin-frame .act .promise-card .card-mark { width: 28px; height: 28px; stroke-width: 1.1; }
  .stage .pin-frame .act-print .promise-stack { margin-top: clamp(16px, 3vh, 34px); }
  .stage .pin-frame .act .card-mark { width: 32px; height: 32px; stroke-width: 0.96; }
  .stage .pin-frame .act .card-rule { margin: 11px 0 13px; }
  .stage .pin-frame .act .tile { min-height: clamp(90px, 13.5vh, 116px); padding: 15px 15px 13px; }
  .stage .pin-frame .act .tile-mark { width: 26px; height: 26px; }
  .stage .pin-frame .act .tile-name { font-size: 13px; }
  .stage .pin-frame .act .wall-rest { font-size: 13.5px; }
  .stage .pin-frame .act-move .moves { margin-bottom: clamp(24px, 6vh, 64px); }
}
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) and (max-height: 740px) {
  .stage .pin-frame .act h2 { font-size: clamp(26px, 2.2vw, 32px); margin-bottom: var(--v3-3); }
  .stage .pin-frame .act .act-eyebrow { font-size: 15px; }
  .stage .pin-frame .act .act-lede { font-size: 14.5px; }
  .stage .pin-frame .act .act-close { font-size: 14px; }
  .stage .pin-frame .act .path-leak { font-size: 13.5px; }
  .stage .pin-frame .act .path-n { font-size: 13.5px; }
  .stage .pin-frame .act .card h3 { font-size: 15.5px; }
  .stage .pin-frame .act .card p { font-size: 12.5px; line-height: 1.4; }
  .stage .pin-frame .act .card { min-height: 164px; padding: 15px 17px 14px; }
  /* ACT 5's PLATE AND ITS CLOSE, ON A COMPOUND THE GENERIC RULES CANNOT
     OUTRANK. Written as `.act .promise-card` they tied with `.act .card` and
     lost on source order, so below 740 act 5's plate took act 4's padding and
     its min-height went UP -- 148px at 820, 150px at 740 -- and its close went
     13.5px at 820 to 14px at 740. Both ladders stepped backwards in the middle,
     in the act this file calls the one with the least to give. The ladders are
     168 -> 148 -> 138 -> 132 and 14.5 -> 13.5 -> 13 -> 12.5 now, monotone. */
  .stage .pin-frame .act .card.promise-card { min-height: 138px; padding: 13px 16px 11px; }
  .stage .pin-frame .act .card-mark { width: 28px; height: 28px; stroke-width: 1.1; }
  .stage .pin-frame .act .card-rule { margin: 9px 0 11px; }
  .stage .pin-frame .act .tile { min-height: clamp(80px, 12vh, 96px); padding: 13px 13px 11px; }
  .stage .pin-frame .act .tile-mark { width: 24px; height: 24px; }
  .stage .pin-frame .act .tile-name { font-size: 12.5px; }
  .stage .pin-frame .act .cluster-label { font-size: 11.5px; }
  .stage .pin-frame .act .card-n { font-size: 11.5px; }
  .stage .pin-frame .act-move .moves { margin-bottom: var(--v3-6); }
}
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) and (max-height: 680px) {
  .stage .pin-frame .act { padding-top: clamp(20px, 3.4vh, 32px); padding-bottom: clamp(20px, 3.4vh, 32px); }
  .stage .pin-frame .act h2 { font-size: clamp(24px, 2vw, 30px); }
  .stage .pin-frame .act .act-lede { font-size: 13.5px; }
  .stage .pin-frame .act .act-close { font-size: 13px; }
  .stage .pin-frame .act .card p { font-size: 12px; }
  .stage .pin-frame .act .card { min-height: 146px; }
  .stage .pin-frame .act .card.promise-card { min-height: 132px; padding: 12px 15px 10px; }
  .stage .pin-frame .act .card-mark { width: 24px; height: 24px; stroke-width: 1.28; }
  .stage .pin-frame .act .tile { min-height: 76px; }
  .stage .pin-frame .act .tile-mark { width: 22px; height: 22px; }
  .stage .pin-frame .act .act-eyebrow { margin-bottom: var(--v3-2); padding-top: 8px; font-size: 14px; }
  .stage .pin-frame .act-move .moves { margin-bottom: var(--v3-5); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT 1'S FLOW LINE — THE SURVEYED RUN
   RUN 06. "More considered and more technical in feel."

   WHAT IT READS AS. From the first painted frame, before anything has moved,
   a DASHED cream datum runs from Lead to Paid with five tall graduations
   standing where the five stations will report and sixteen short minors
   quartering each interval: the path is SURVEYED but not yet WALKED. As each
   station reports, a gold monument with a centre point lands on its
   graduation, a short leader ties it to its label, and THE INTERVAL BEHIND IT
   converts from dashed cream to solid gold with gold graduations. A gold head
   rides the leading edge with a heat bloom trailing it, alive only while a
   station is arriving and dead the instant it parks.

   Cold is the surveyed mark; warm is the reading taken. Dashed cream at
   3.14:1 against solid gold at 12.05:1 — a 3.8x step on two independent cues,
   so the before/after read survives any single one of them failing.

   THE GRID BECOMES A SCALE, and this is what makes every number below exact.
   With a column-gap, station k's left edge is k*(colW + gap) and no percentage
   background can land on it. With `column-gap: 0` and five 1fr columns it is
   EXACTLY k * 20% of .path, so the graduations and the monuments are two
   consequences of ONE grid instead of two sets of numbers that can drift. The
   gutter moves to the station's own padding-right and nothing looks different.

   GEOMETRY, in .path's padding-box coordinates (y = 0 at its top):
     y  8..27   major graduations, 1px, full band
     y 11..17   minors, 1px x 6px, terminating on the datum's top edge
     y 17..18   THE DATUM
     y 12..23   the monument, 11px square, centred on the datum's pixel
     y 23..25   its 2px ground ring, punched out of the graduation behind it
     y 25..--chain-pad   the leader
     x = 5      node 1's centre pixel; node k is at 5 + (k-1)*20%
     the run    x 5 .. 4C+5, i.e. 80% of .path

   THE ENGINE WRITES TWO CUSTOM PROPERTIES AND NOTHING ELSE NEW, both on
   .path itself: --chain-cut, a right inset in WHOLE PIXELS, and --chain-heat,
   an alpha at four decimal places. RUN 07 dropped run 06's quantisation of
   both -- the cut to sixteenths of the run and the heat to eighths. A
   sixteenth of the run is 67px at 1440, so the trace advanced in chunks: at a
   10px scroll step the edge took SEVENTEEN distinct positions in 741 frames
   and teleported 67px between them. Rounded to a pixel it takes 254 positions
   and never moves more than the step. Determinism is untouched -- both values
   are pure functions of progress written as exact strings, which is all the
   gate holds them to -- and THE SEGMENT MARKS DO NOT MOVE: they are drawn by
   the cold instrument, so the survey still shows every graduation and only the
   trace's own leading edge is continuous. See paintA in oyt-stage.js.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  .stage .path {
    /* the band's own geometry, all of it relative to --chain-pad so one
       number moves the datum, the monuments and the leaders together */
    --chain-pad: var(--path-hang);
    --chain-top: 8px;
    --chain-h: 19px;
    --chain-datum: 9px;
    --div: 20%;
    --run: 80%;
    /* THE PROJECTED INK. Cream, not dim gold: gold is reserved for what is
       warm. 0.38 on #0D0C0B composites to (100,96,90) = 3.14:1, which clears
       SC 1.4.11 — and 1.4.11 binds here, because these marks are what carry
       the state "not yet surveyed". */
    --chain-cold: rgba(241, 234, 219, 0.38);
    --chain-cold-min: rgba(255, 255, 255, 0.14);   /* 1.44:1 — texture, not state */
    /* THE AUTHORED REST STATE, exactly as the phone authors --m-draw-inset:
       the stylesheet declares the FINISHED drawing and the engine only ever
       writes a value that makes the run SHORTER, so a script that never
       arrives cannot produce a half-drawn page. calc(20% - 6px) is the right
       inset that lands the cut on node 5's centre pixel. */
    --chain-cut: calc(20% - 6px);
    --chain-heat: 0;
    column-gap: 0;
    padding-top: var(--chain-pad);
  }
  .stage .path-step { padding-right: var(--act-gap); z-index: 2; }

  /* ── THE COLD INSTRUMENT. Static, painted once at layout, never repainted
     for the life of the act. At progress 0 the reader gets a complete, empty,
     measured scale with five marked stations and nothing yet claimed about
     them: the instrument exists before the evidence does. */
  .stage .path::before {
    content: '';
    position: absolute;
    left: 0; right: 0;
    top: var(--chain-top);
    height: var(--chain-h);
    z-index: 0;
    pointer-events: none;
    background-image:
      linear-gradient(90deg, var(--chain-cold) 0 1px, transparent 1px),
      repeating-linear-gradient(90deg, var(--chain-cold-min) 0 1px, transparent 1px 6.25%),
      repeating-linear-gradient(90deg, var(--chain-cold) 0 6px, transparent 6px 11px);
    background-size:
      var(--div) var(--chain-h),
      var(--run) 6px,
      calc(var(--run) + 1px) 1px;
    background-position:
      5px 0,
      5px 3px,
      5px var(--chain-datum);
    background-repeat: repeat-x, no-repeat, no-repeat;
  }

  /* ── THE SURVEYED RUN. The same instrument in gold, in ONE element that ONE
     clip reveals — so "the line cannot get ahead of the evidence" stops being
     an arithmetic promise in paintA and becomes a geometric fact. An INSET
     CLIP and not scaleX, for the reason oyt-stage-mobile.css writes out at
     length about the phone's rail and which the desktop never got: a scale
     compresses the gradient it reveals, so a station's own segment would be a
     different colour depending on how far the line had got past it.

     NO will-change AND NO transform, DELIBERATELY. [data-flow] is absent from
     the compositing list in the scrub query and MUST STAY ABSENT:
     unpromoted, this clip edge is painted into the parent in a coordinate
     space that does not depend on arrival history. Add [data-flow] to that
     selector list and this design's determinism argument dies silently. */
  .stage .path::after {
    content: '';
    position: absolute;
    left: 0; right: 0;
    top: var(--chain-top);
    height: var(--chain-h);
    z-index: 1;
    pointer-events: none;
    background-image:
      /* 1  THE READING HEAD — 1px, full band, on the last painted pixel
            inside the cut. At rest it coincides exactly with node 5's major
            graduation and is therefore invisible: a finished drawing has no
            cursor on it. */
      linear-gradient(var(--v3-gold) 0 0),
      /* 2  warm majors */
      linear-gradient(90deg, var(--v3-gold) 0 1px, transparent 1px),
      /* 3  warm minors */
      repeating-linear-gradient(90deg, rgba(245, 197, 66, 0.55) 0 1px, transparent 1px 6.25%),
      /* 4  THE SURVEYED DATUM, SOLID. The whole point. */
      linear-gradient(var(--v3-gold) 0 0),
      /* 5  THE HEAT BLOOM, trailing the head, UNDER the marks so they stay
            crisp. Alive only while a station is arriving. */
      linear-gradient(90deg, rgba(245, 197, 66, 0), rgba(245, 197, 66, calc(var(--chain-heat) * 0.40)));
    background-size:
      1px var(--chain-h),
      var(--div) var(--chain-h),
      var(--run) 6px,
      calc(var(--run) + 1px) 1px,
      72px var(--chain-h);
    /* A % in background-position resolves against (box width - image width).
       For the 1px head, 100% ALREADY means x = W - 1, so calc(100% - cut) is
       the last pixel INSIDE the clip; for the 72px bloom the same expression
       puts its RIGHT edge on the cut. Do not also subtract 1px — that lands
       the head outside the clip and it vanishes. */
    background-position:
      calc(100% - var(--chain-cut)) 0,
      5px 0,
      5px 3px,
      5px var(--chain-datum),
      calc(100% - var(--chain-cut)) 0;
    background-repeat: no-repeat, repeat-x, no-repeat, no-repeat, no-repeat;
    clip-path: inset(0 var(--chain-cut) 0 0);
  }

  /* ── THE MONUMENT. An open square with a centre point, on the ground
     colour, punching a 2px hole in the graduation behind it so 2px of
     graduation still shows above and below: the monument sits ON the mark
     rather than replacing it. The graduation is the COLD state and is always
     there; the monument is the reading, and it rides the station's own
     opacity clock, so it needs no engine change of its own. */
  .stage .path-step::before {
    content: '';
    position: absolute;
    left: 0;
    top: calc(var(--chain-top) + var(--chain-datum) - 5px - var(--chain-pad));
    width: 11px; height: 11px;
    box-sizing: border-box;
    border: 1px solid var(--v3-gold);
    background:
      radial-gradient(circle at 50% 50%, var(--v3-gold) 0 1.5px, transparent 1.5px)
        no-repeat 50% 50% / 100% 100%,
      var(--v3-ground);
    box-shadow: 0 0 0 2px var(--v3-ground);
    z-index: 1;
  }

  /* ── THE LEADER. The callout that ties the reading to what it says. It
     stops at the copy's top edge and never crosses a glyph, so it costs the
     measure nothing. */
  .stage .path-step::after {
    content: '';
    position: absolute;
    left: 5px;
    top: calc(var(--chain-top) + var(--chain-datum) + 8px - var(--chain-pad));
    height: calc(var(--chain-pad) - var(--chain-top) - var(--chain-datum) - 8px);
    width: 1px;
    background: linear-gradient(180deg, var(--v3-gold), rgba(245, 197, 66, 0.18));
  }
}

/* THE STATION COPY AT 901-1180. Five columns are held at every desktop width,
   so the narrow end needs the measure back: a smaller gutter and a step down
   in size.

   AND THE STEP DOWN WAS DEAD AT EVERY FRAME 820px TALL OR SHORTER. It was
   written `.stage .path-leak`, which is (0,2,0); the short-frame tier states
   the same property as `.stage .pin-frame .act .path-leak` at (0,4,1) and
   therefore won at any source order. Measured at 1024 wide: 14px at h=900,
   15px at h=800, 15px at h=768 -- the copy got LARGER as the frame got
   shorter, in the width band with the least room for it, and 1024x768 is in
   this run's own fit table. The gutter half of the rule survived, because
   nothing else declares padding-right, so the two halves of one trade came
   apart: a 16px-narrower gutter carrying a 1px-larger face.

   Both rules are at the tier's own specificity now, and the ladder is
   monotone: 16px tall, 14px in this band, 13.5px from 740 down, where the
   tier's own number is already smaller than this band's. */
@media (min-width: 901px) and (max-width: 1180px) {
  .stage .path-step { padding-right: var(--v3-4); }
  .stage .pin-frame .act .path-leak { font-size: 14px; }
}
@media (min-width: 901px) and (max-width: 1180px) and (prefers-reduced-motion: no-preference) and (max-height: 740px) {
  .stage .pin-frame .act .path-leak { font-size: 13.5px; }
}
