/* ═══════════════════════════════════════════════════════════════════════════
   OYT LANDING v3 — THE MOBILE STAGE
   Run M1. Linked from index.html with media="(max-width: 900px)", so every
   rule in this file is already inside that query and none of it is written
   again below. Above 900px a browser still DOWNLOADS this sheet — a media
   attribute lowers a stylesheet's priority and makes it non-render-blocking,
   it does not prevent the fetch — but it applies not one declaration, and
   that is the part the run's desktop hash gate proves.

   WHY THE PHONE HAS ITS OWN SHEET AND ITS OWN ENGINE. Until run M1 the phone
   got the desktop layout in one column with an IntersectionObserver reveal
   bolted on: a fallback, not a design. Everything below is written for a
   thumb.

   ── THE THREE FACTS, AS VERIFIED RATHER THAN AS ASSUMED ─────────────────
   The run that commissioned this file named three facts. Two hold as
   stated. The first does not, and the correction matters enough to write at
   the top of the file it decides.

   (a) STATED: "iOS Safari resizes the viewport during scroll, so anything
       sized in vh RESIZES MID-SCROLL, and anything pinned JUMPS."
       WHAT IS ACTUALLY TRUE: on iOS Safari `vh` resolves to the LARGE
       viewport — the bars-retracted height, i.e. `lvh` — and it is STATIC.
       `dvh` is the unit that tracks the bar. This repo says so in its own
       words at oyt-design.css:924, "iOS: vh = toolbar-collapsed viewport;
       dvh tracks the visible one". So `100vh` does not resize; it
       UNDERHANGS — a box one large-viewport tall keeps its last 60-100px
       underneath the toolbar for as long as the toolbar is showing, which on
       a pinned scene means the bottom of every frame is hidden exactly when
       the reader arrives at it, and the bottom of every frame is where this
       stage puts its rail.

       `svh` — the SMALL viewport, the bars-VISIBLE height — is also static,
       and it is the one that is never covered. So the run's prescription is
       right, for a different reason than the one it gave, and this file is
       sized in svh throughout with a vh fallback behind @supports.

       WHAT DID TRACK THE ADDRESS BAR on the old phone path was JAVASCRIPT,
       not CSS: the deleted tick() read `window.innerHeight` on every scroll
       event to compute progress, while the boxes it was measuring were sized
       in svh. Two unit bases for one quantity, wrong by exactly the toolbar's
       height whenever it was retracted. That is why /oyt-stage-mobile.js
       measures one svh off .m-probe below and never reads innerHeight for
       geometry — the run's own most important instruction, and the correct
       fix for the real mechanism.

   (b) VERIFIED TRUE. ScrollTrigger's `pin:` appears nowhere in this repo —
       four ScrollTrigger.create() call sites, not one with a pin key — and
       the pin has always been CSS `position: sticky`. It stays that way
       here. No ScrollTrigger, no ScrollSmoother, no scroll hijacking.

   (c) VERIFIED TRUE OF THE STAGE, AND FALSE OF THE PAGE, which is worth
       stating plainly rather than leaving as a green tick. Neither GSAP nor
       ScrollTrigger is fetched by the stage at <=900px: oyt-stage.js returns
       before its loader and this file's engine has no loader at all. But
       oyt-hero-loop.js still pulls GSAP on a phone — its only gate is
       prefers-reduced-motion and it has no width test anywhere — so the PAGE
       is not GSAP-free at 390. That belongs to the hero this run must not
       touch, and it is reported rather than fixed.

   ── ONE MORE FACT THE RUN DID NOT NAME ─────────────────────────────────
   env(safe-area-inset-bottom) IS CURRENTLY ZERO ON iOS, because index.html's
   viewport meta has no `viewport-fit=cover` (no page in this repo has one).
   Every env() below is still written, because it is the correct expression
   and because the day that meta gains viewport-fit the reserve becomes real
   without another edit — but on a notched iPhone today it contributes 0px,
   and the home-indicator clearance is whatever the 7px pad gives. Adding
   viewport-fit=cover moves every bottom reserve on the whole page at once,
   which is a page-wide decision and not this run's.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── THE SIX NUMBERS ────────────────────────────────────────────────────
     Each act's scene height in svh, and the only place any of them is
     stated. A scene of N svh holds its frame still for N-100 svh: the last
     screen is spent letting the frame's own bottom reach the viewport's,
     which is the same viewport-height a sticky pin costs on the desktop path
     and the reason that path's total is 36 screens for 34 of scrub.

         act 1   260 svh  ->  160 of scrub
         act 2   360 svh  ->  260 of scrub   (the largest: four things to say)
         act 3   260 svh  ->  160 of scrub
         act 4   260 svh  ->  160 of scrub
         act 5   260 svh  ->  160 of scrub
         ────────────────
                1400 svh

     RUN 11 DELETES THE HANDOFF, here as on the desktop: it was a sixth entry
     of 100 svh carrying an authored gradient and no scene, and the page below
     the stage is the stage's own ground now, so there is nothing to fade to.
     1500 svh becomes 1400, --m-h-h is gone, and act 5's frame releases on the
     same scroll row that pricing crosses the fold.

     /oyt-stage-mobile.js READS these off :root rather than keeping a copy,
     exactly as oyt-stage.js reads --stage-screens, so the mobile stage
     retunes from this block alone. */
  --m-h-1: 260;
  --m-h-2: 360;
  --m-h-3: 260;
  --m-h-4: 260;
  --m-h-5: 260;

  /* -- THE INSTRUMENT'S LANE, ON THE RIGHT EDGE --------------------------
     RUN 06. The 36px bottom bar is gone and 22px of every phone scene comes
     back with it: the reserve was --m-rail-h + the home-indicator inset, and
     it is now --m-pad-t + the same inset. THE INSET IS KEPT -- today env()
     resolves to 0 because index.html's viewport meta has no
     viewport-fit=cover, and keeping the expression means the reserve is
     already correct the day it gains one.

     22px is 2.6% of an 844px iPhone and 3.3% of a 667px SE, and it lands
     where act 5's five promises need it. The bigger prize is what goes with
     the bar: a full-width backdrop-filter: blur(12px) re-blurred over a
     ground that moves under it on every frame.

     THE INSTRUMENT GOES TO THE RIGHT EDGE and the argument is the phone's.
     Act 1's money-path rail already lives 28px from the screen's LEFT
     (--m-path-rail-x below), so the left is taken and two vertical hairlines
     12px apart is one rail too many; and the gesture is vertical, so the
     thumb side is the right. Every translate in oyt-stage-mobile.js is
     Y-only, which is what makes the horizontal clearance a constant rather
     than something to re-prove at every scroll position. */
  /* THE BOX IS THE INK ENVELOPE. The frame's content box ends 20px from
     the edge (--m-pad-x), so the clearance is 8px and the occlusion
     gate's box test and its hit test say the same thing. */
  /* The frame's gutters. */
  --m-pad-x: 20px;
  --m-pad-t: 14px;

  /* ACT 1'S RAIL. 28px from the LEFT EDGE OF THE SCREEN, so inside the frame
     the offset is that minus the frame's own gutter. */
  --m-path-rail-x: calc(28px - var(--m-pad-x));
  --m-path-gap: 22px;      /* rail centre to the station's text column */
  --m-path-node-y: 11px;   /* node centre below the top of its row. READ BY
                              THE ENGINE off :root — the rail's top is node
                              1's centre, so this number is what makes the
                              first station light on the frame the line
                              starts, and a second copy of it in JS would be
                              a number that can drift out of step. */

  /* Written by the engine. Authored at their FINISHED values so a visitor
     whose script never runs, and a crawler, get the path drawn and every
     station lit. The engine only ever writes smaller numbers. */
  --m-draw-inset: 0px;
  --m-lit: 1;
}

/* ── THE BOTTOM'S LAMP IS FITTED AGAIN HERE — RUN 11 ──────────────────────
   index.html's .v3-below .stage-glow carries three numbers, and the third of
   them is a FIT rather than a derivation: the alpha at which this layer's
   constant value on the seam row best balances the stage's own lamp, which is
   viewport-anchored and therefore sweeps as the seam crosses the screen.
   0.022 is that fit AT 1440x900. It is not that fit at 390x844, because a
   radial gradient's stops are percentages of its ray to the farthest corner
   and the phone's boxes are a different shape: the stage's own layer has a
   612px ray in a 390x844 box against 1206px in a 1440x900 one, so its lamp
   falls off two and a half times faster across the screen and its swing at
   the seam row is 4.7 L* rather than 2.9.

   harness/seamsweep.js at 390x844, worst over the crossing:
     a 0.022 -> 1.0567:1      a 0.038 -> 1.0482:1
     a 0.030 -> 1.0553:1      a 0.046 -> 1.0545:1

   AND 0.038 IS THE FLOOR, NOT A PASS. One constant cannot follow a 4.7 L*
   sweep, so the best any value can do is half of it -- about 2.3 L*, which is
   1.048:1 against the 1.046:1 bar this run takes from /oyt-stage.css's own
   "a colour change you cannot see". It is over that bar by two parts in a
   thousand, at three of nineteen crossing positions, and the run-11 report
   says so rather than moving the bar. */
.v3-below .stage-glow { --stage-glow-a: 0.038; }


/* ═══ THE GROUND ══════════════════════════════════════════════════════════
   Same ink, same lamp, same grain, same vignette — .stage-ground is
   unconditional in oyt-stage.css and already sized in svh, so it needs
   nothing here but a grid that reads at phone density. A 64px cell on a
   390px screen gives six columns and the radial mask eats four of them; 40px
   gives nine, and the grid reads as a drawing surface rather than as three
   lines. */
.stage-grid { background-size: 40px 40px; }

/* ═══ THE SVH PROBE ═══════════════════════════════════════════════════════
   ONE SVH, MEASURED — NEVER window.innerHeight.
   innerHeight follows the address bar; this element does not. It is sized in
   the same unit the scenes are, so whatever a scene means by "one screen",
   this box is exactly that and the engine reads its height off it. It is
   absolutely positioned inside .stage (which is position: relative), one
   pixel wide, hidden, contributes nothing to layout, and is aria-hidden in
   the markup. */
.m-probe {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 100svh;
  visibility: hidden;
  pointer-events: none;
}
@supports not (height: 100svh) {
  .m-probe { height: 100vh; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ACTS' PHONE LAYOUT
   Everything from here to the motion query is LAYOUT, not choreography: it
   applies to the reduced-motion path and to a visitor whose script never
   runs exactly as it does to the animated one. That is the desktop's own
   rule — the authored page is the finished page and the animation is a
   decoration on top of it.

   NOTE ON oyt-stage.css's 1100px BLOCK. That query is (max-width: 1100px),
   so it still reaches a phone: three columns for .path and .promises, two
   for .moves. Every one of them is overridden below. It is not deleted there
   because 901-1100 is a laptop still on the scrub path.
   ═══════════════════════════════════════════════════════════════════════════ */

.act-inner,
.act-inner-wide { max-width: none; margin: 0; width: auto; }

/* -- RUN 06: THE PHONE'S OWN TYPE, IN ITS OWN IDIOM ----------------------
   The desktop's scale-up is scoped to >= 901px and cannot reach here, which
   is deliberate: act 1's phone choreography is a five-row grid whose rows are
   sized against this type, and the desktop's 16px/1.6 leak line put 105px of
   copy in a 102px row. What the phone inherits is the DIRECTION — bigger,
   less blocky — sized against its own rows and re-measured against them.

   THE HEADLINE IS THE SAME "less blocky" TREATMENT AS THE DESKTOP'S and
   costs the same nothing: leading 1.0 -> 1.06 and tracking 0 -> 0.014em open
   the block up without a single extra pixel of size. The clamp is unchanged
   -- 42px is already the top of the brief's phone band.

   RUN 13: the phone head takes the section-head family -- the same three
   values as /oyt-stage.css's .act h2 and index.html's .sec-title (the case
   comes from the desktop sheet's base rule, text-transform: none, which this
   sheet never overrode). 31.2px at 390, 28px at 320, 36px from 450 up; the
   max-height: 700px tier below (25-30px) stays under it, so the ladder is
   still monotone. */
.act h2 {
  font-size: clamp(28px, 8vw, 36px);
  line-height: 1.15;
  letter-spacing: 0.01em;
  margin-bottom: var(--v3-4);
}
/* THE ACT'S NAME ON THE PHONE. 12px came from --v3-fs-eyebrow, which the
   FROZEN HERO reads, so the phone states its own here the way every other
   phone size in this sheet is stated. 13.5px against a 33.15px heading at
   390 -- the same 0.41 ratio the bench's 16 against 51.84 lands on at 1440,
   rather than the 0.36 the shared token happened to give. */
.act-eyebrow { margin-bottom: var(--v3-2); font-size: 13.5px; }
.act-lede { font-size: 16px; line-height: 1.55; max-width: none; }
.path-n { font-size: 14px; }
.path-leak { font-size: 15px; line-height: 1.5; }

/* one column, everywhere the desktop had a grid */
.wall { grid-template-columns: 1fr; gap: var(--v3-4); margin-top: var(--v3-4); }
.cluster-tiles { grid-template-columns: repeat(2, 1fr); gap: 8px; }
/* -- THE PLATES ON THE PHONE -------------------------------------------
   RUN 06. Acts 4 and 5 are plates now, and on a phone they are one column of
   them, in the same idiom at the phone's own scale: a 28px two-ink mark and
   a mono index on one ruled header row, a 16px heading, a 13.5px sentence.
   The 3+2 course and the registration frame are DESKTOP: a single column
   has no left and no right for a card to come from and no structure for a
   frame to bound. RUN 08 removes the last line this block needed for act 4:
   the desktop interlock is gone, so the rule that cancelled it here -- a
   margin-top of 0 on the even cards -- addressed nothing and is deleted with
   it. A rule that overrides a declaration nobody makes is not defensive; it
   is a claim about the desktop that stops being true silently.
   The tick and the seal stay -- they are 14px and 22px of drawing and they
   are what makes a phone card the same object as a desktop one.
   Every desktop card rule this overrides is a bare class at (0,1,0) or
   (0,1,1); these are the same or higher and later, which is why they win. */
.moves, .promises { grid-template-columns: 1fr; gap: var(--v3-4); margin-top: var(--v3-4); }
.promises > li { grid-column: auto; grid-row: auto; }
.card { min-height: 0; border-radius: 12px; padding: 15px 16px 14px; }
/* ACT 5 CARRIES FIVE PLATES IN ONE PANEL and is the only act on the phone
   whose panel does not fit its frame — the run-05 report measured the same
   thing about the old thin layout ("five promises with a heading and a
   sentence each do not fit in 415px") and built the panel advance for it.
   The plate is tighter here than act 4's on every axis so the walk is as
   short as it can be: measured, panel 2 needs 801px of a 668px budget at
   390x844 before this block and 726 after. */
.promise-card { min-height: 0; padding: 12px 14px 11px; }
.promise-card .card-mark { width: 24px; height: 24px; stroke-width: 1.28; }
.promise-card .card-rule { margin: 8px 0 9px; }
.promise-card h3 { font-size: 15px; margin-bottom: 5px; }
.promise-card p { font-size: 13px; line-height: 1.4; }
.promise-seal { width: 18px; height: 18px; top: 11px; right: 11px; }
.card-mark, .promise-card .card-mark { width: 28px; height: 28px; stroke-width: 1.1; }
.card-n { font-size: 12px; }
.card-rule, .promise-card .card-rule { margin: 10px 0 11px; }
.card h3, .promise-card h3 { font-size: 16px; margin-bottom: 6px; }
.card p, .promise-card p { font-size: 13.5px; line-height: 1.45; }
.card-tick { width: 11px; height: 11px; right: 11px; }
.moves > li:nth-child(odd) .card-tick { top: 11px; }
.moves > li:nth-child(even) .card-tick { bottom: 11px; }
.promise-seal { width: 20px; height: 20px; top: 13px; right: 13px; }
.promise-stack { margin-top: var(--v3-4); }
/* THE REGISTRATION FRAME IS A DESKTOP OBJECT. It bounds a 3+2 course; around
   a single column it is a border on a list, which says nothing. */
.promise-frame { display: none; }
/* THE PANEL CARRIES THE AIR THE LIST USED TO. On a phone there is no left and
   no right, so the plate is not a column -- it is the same object stated the
   same way, at the phone's own scale: 16px of air inside a 14px radius rather
   than 22px inside the bench's. */
/* RUN 11 — the right-hand plate is deleted; act 3 is the table and the line
   under it. .math-panel, .paths and .path-col had a phone tier here and a
   narrow-phone tier below, and both go with the element. */

/* THE ONE NESTED SCROLLER INSIDE THE STAGE, closed. oyt-stage.css gives
   .math-table `overflow-x: auto` with `table { min-width: 560px }`, so at
   390 act 3's table is a horizontal scroll container inside a sticky frame —
   a second scroll axis under the same thumb that is driving the stage.
   index.html already does exactly this for its own two tables at <=480
   (#compareTable, #ladderTable { min-width: 0 }); this table was never given
   the same treatment. The 5-Year Total is the point of the table and it fits
   at 320 once the cells stop reserving desktop padding. */
.math-table { padding: 10px 14px 6px; border-radius: 14px; margin-top: var(--v3-5); overflow-x: visible; }
.math-table table { min-width: 0; }
.math-table th, .math-table td { padding: 10px 10px 10px 0; font-size: 14px; }
.math-table th:last-child, .math-table td:last-child { padding-right: 0; }   /* the gutter is the plate's */
.math-table thead th { padding-top: 10px; font-size: 11px; letter-spacing: 0.06em; }
.math-table tbody th { white-space: normal; }
/* RUN 11's fourth column. 14px is the phone's own cell size, so the payoff is
   the row's weight rather than its size — a 390px screen has no width to
   spend on a step up, and the gold is the emphasis. */
.math-table .keep { font-size: 14px; }
.math-gap { margin-top: var(--v3-4); font-size: 14.5px; }
.math-cta { margin-top: var(--v3-5); }
/* B7 on the phone, at the phone's scale: the same fuller pill the desktop
   gets, one step down. The hover, active and sweep come from oyt-stage.css
   and cost a touch device nothing -- none of them has a resting state. */
.stage-cta { font-size: 15px; padding: 15px 26px; min-height: 52px; gap: 10px; }
/* the plate carries its own type and its own padding now; the old
   hairline-topped column rules are gone with the columns */
.act-close { margin-top: var(--v3-4); font-size: 13px; line-height: 1.5; }
.wall-rest { font-size: 12.5px; line-height: 1.5; margin-top: 0; max-width: none; }
.tile { min-height: 0; padding: 9px 10px 8px; gap: 6px; }
.tile-mark { width: 18px; height: 18px; }
.tile-name { font-size: 12px; line-height: 1.25; }
.tile-tick { top: 6px; right: 6px; width: 7px; height: 7px; }
/* C8: the cluster label is the rule the wall sorts by, and at 10.5px it was
   the smallest thing in the act. */
.cluster-label { margin-bottom: var(--v3-3); font-size: 12.5px; letter-spacing: 0.18em; }

/* ═══════════════════════════════════════════════════════════════════════════
   ACT 1 — THE MONEY PATH, VERTICAL
   On a phone the path runs top to bottom, because that is the direction the
   thumb is already moving: the scroll gesture and the story become one
   motion.

   AUTHORED FINISHED. The line fully drawn, every station lit, every line of
   copy visible — that is what a reduced-motion visitor, a crawler and a
   visitor whose script never arrives get, and it is what the engine
   interpolates BACK FROM. --m-draw-inset and --m-lit default to their
   finished values and nothing but the engine ever writes anything else.
   ═══════════════════════════════════════════════════════════════════════════ */
.path {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* FIVE ROWS OF EXACTLY THE SAME HEIGHT — minmax(0, 1fr) and not 1fr,
     because a bare 1fr row is minmax(AUTO, 1fr) and grows past its share when
     its content is taller. Even rows are what "five stations, evenly spaced"
     means: the node positions the engine measures step by a constant, so the
     line passes them on a constant beat and the pitch between two stations is
     the same pitch everywhere. */
  grid-template-rows: repeat(5, minmax(0, 1fr));
  gap: 0;
  row-gap: 0;
  margin: 0;
  padding: 0;
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

/* THE RAIL. 2px, gold, 28px from the left edge of the screen, from the
   centre of station 1's node — which sits directly under the heading — to
   the bottom of the frame's content area.

   IT IS DRAWN WITH AN INSET CLIP AND NOT WITH scaleY, and that is a
   determinism decision rather than a taste; the run report carries the
   measurement of both. In short: scaleY leaves the line's leading edge at a
   FRACTIONAL pixel, and run 04c established that a composited layer rasters
   a fractional offset against whichever integer snapping it had a frame
   earlier — which is exactly what differs between arriving at a scroll
   position from above and from below. An inset clip whose value the engine
   ROUNDS puts the edge on a whole pixel every time. scaleY would also
   compress the line's own gradient as it grew, so the colour AT a station
   would depend on how far the line had got past it. */
.path::before {
  content: '';
  position: absolute;
  left: var(--m-path-rail-x);
  top: var(--m-path-node-y);
  bottom: 0;
  right: auto;
  width: 2px;
  height: auto;
  border-radius: 1px;
  background: linear-gradient(180deg, var(--v3-gold), rgba(245, 197, 66, 0.55));
  transform: none;
  clip-path: inset(0 0 var(--m-draw-inset, 0px) 0);
}

.path-step {
  position: relative;
  padding-left: calc(var(--m-path-rail-x) + var(--m-path-gap));
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

/* THE NODE, IN TWO LAYERS SO THAT LIGHTING IT IS AN OPACITY AND NOTHING
   ELSE. ::before is the cold node — a hollow ring on the ground colour, the
   same shape the desktop path uses. ::after is the lit node — a gold fill
   inside an ink ring — and the whole of "a station lights" is that second
   layer's opacity going 0 -> 1. No colour is interpolated, no colour
   function is required, and there is nothing here that a browser without
   color-mix() renders differently. */
.path-step::before {
  content: '';
  position: absolute;
  left: calc(var(--m-path-rail-x) - 4px);
  top: var(--m-path-node-y);
  margin-top: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-sizing: border-box;
  background: var(--v3-ground);
  border: 1.5px solid rgba(245, 197, 66, 0.34);
  z-index: 2;
}
.path-step::after {
  content: '';
  position: absolute;
  left: calc(var(--m-path-rail-x) - 4px);
  top: var(--m-path-node-y);
  margin-top: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-sizing: border-box;
  background: var(--v3-gold);
  box-shadow: 0 0 0 2px var(--v3-ground);
  opacity: var(--m-lit, 1);
  z-index: 3;
}

/* The label. Cream, and its DIM state is the same cream at 0.56 — which on
   #0D0C0B computes to 5.5:1 against a 4.5 bar, so an unlit station is quiet
   but never below AA. The engine writes this element's own opacity, so the
   label is one of the elements the "opacity and transform only" rule covers
   literally rather than through a custom property. */
.path-n {
  display: block;
  font-family: var(--mono);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--v3-ground-ink);
  margin-bottom: 3px;
}
.path-leak {
  font-size: 15px;
  line-height: 1.55;
  color: var(--v3-ground-dim);
  max-width: none;
  margin: 0;
}
/* C2 ON THE PHONE. The same change the desktop makes: off the mono face, out
   of uppercase, up in size, and read as a subtitle rather than as a second
   eyebrow. The copy is untouched -- `text-transform` was the only thing
   making "Where it leaks." shout. */
.path-lead {
  margin-top: 0;
  margin-bottom: var(--v3-3);
  font-family: var(--v3-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
  color: var(--v3-ground-ink);
}
/* AND THE ADJACENT-SIBLING RULE THAT OUTRANKS `.path { margin: 0 }`.
   oyt-stage.css:1034 declares `.path-lead + .path { margin-top: var(--v3-5) }`
   at (0,2,0); the `.path` block above is (0,1,0), and coming later in the
   cascade does not help against higher specificity. The selector still matches
   here — index.html keeps the signpost immediately before the list — so the
   grid was 24px shorter than this file's own arithmetic assumes, and each of
   its five minmax(0,1fr) rows lost 4.8px of the budget the station copy is
   measured against. */
.path-lead + .path { margin-top: 0; }

/* RUN 07 — THE INSTRUMENT IS DELETED, on this side too. The phone carried
   the same scale turned through 90 degrees onto a 12px right-edge strip;
   the strip, its ink and --m-ind-w/--m-ind-x go with it. Nothing else on
   the phone was sized against that strip: --m-pad-x is 20px and the ink
   ended 8px inside it, so no scene changes width. */

/* ═══════════════════════════════════════════════════════════════════════════
   THE SCENES — the choreography's geometry.
   Everything above is layout and applies whatever the visitor's motion
   preference is. Everything below is the pinned, scrubbed stage, and under
   prefers-reduced-motion: reduce it simply does not exist: the panels go
   back to being nothing (display: contents, from oyt-stage.css), the acts go
   back to being five ordinary full-height sections, and there is nothing for
   a script to undo. That is what makes "build nothing" the correct
   implementation of the accessibility path rather than a second one.

   THE HEIGHTS ARE UNCONDITIONAL WITHIN THIS QUERY. No class gate, no "arm it
   from JavaScript", because the document height must be identical whether or
   not a line of JS ever runs — the moment a script changes the page's height
   is a layout shift, and CLS 0.00000 is a hard gate. It is the same choice
   the desktop makes with .pin-track's height.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {

  /* -- THE PHONE GROUND MAP ------------------------------------------------
     RUN 06, and the same convention as the desktop's: a gradient stop is a
     DOCUMENT ROW, so a boundary at scene-top s takes the +0.5-viewport term
     because the reader's eye is half a screen below the viewport's top.

     The box is 1400svh (260/360/260/260/260 for the five acts, and nothing
     after them). Boundaries at 260 / 620 / 880 / 1140 svh; half a viewport is
     50svh = 3.571pp; ramp half-width 75svh = 5.357pp.
       centres  22.143% / 47.857% / 66.429% / 85.000%
       seam     1400svh = 100%  -- THE STAGE'S OWN BOTTOM EDGE, no +0.5, and
                the row the page below it starts on

     RUN 11 CHANGES ONLY THE DENOMINATOR, exactly as on the desktop: every
     stop below is at the same DOCUMENT ROW (svh from the stage's top) it was
     at in run 08 -- 235 / 385 / 595 / 745 / 855 / 1005 / 1115 / 1265 / 1300 /
     1400 -- because the five scenes have not moved and are the same heights.
     1500 became 1400, so each percentage is the same row over a shorter box.

     IT MUST BE INSIDE THE no-preference QUERY, not merely inside this file:
     the --m-h-* scene heights this map describes only exist inside it, and a
     phone with motion refused gets the natural-height layout and must fall
     through to the base map in oyt-stage.css. No width test -- the <link>
     already carries media="(max-width: 900px)". */
  .stage {
    background-image: linear-gradient(180deg,
      var(--v3-ground)     0%,      var(--v3-ground)    16.786%,   /* 235svh */
      var(--v3-ground-g2) 27.500%,  var(--v3-ground-g2) 42.500%,   /* 385/595 */
      var(--v3-ground-g3) 53.214%,  var(--v3-ground-g3) 61.071%,   /* 745/855 */
      var(--v3-ground-g4) 71.786%,  var(--v3-ground-g4) 79.643%,   /* 1005/1115 */
      var(--v3-ground-g5) 90.357%,  var(--v3-ground-g5) 92.857%,   /* 1265/1300 */
      var(--v3-ground-gb) 100%);                                   /* 1400svh */
  }

  .m-scene {
    position: relative;
    height: calc(var(--m-h, 100) * 1svh);
    min-height: 0;
    display: block;
    padding: 0;
  }
  .m-frame {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-columns: minmax(0, 1fr);
    box-sizing: border-box;
    /* THE RESERVE. The rail's height plus the home-indicator inset, in the
       one expression that also sizes the rail. Panels are grid items, not
       absolutely-positioned boxes, precisely so that this padding insets
       them — an absolutely-positioned child resolves against the PADDING
       box and would ignore it. The run asks for this to be proved by hit
       test rather than by eye, and the harness does. */
    padding: var(--m-pad-t) var(--m-pad-x) calc(var(--m-pad-t) + env(safe-area-inset-bottom, 0px));
  }

  /* THE FALLBACK — the same rules in vh, as the run specifies. It is a worse
     answer and it is here only for a browser with no svh: on iOS, vh is the
     bars-RETRACTED height, so the bottom of every frame — which is where the
     rail lives — sits under the toolbar while the toolbar is showing. */
  @supports not (height: 100svh) {
    .m-scene { height: calc(var(--m-h, 100) * 1vh); }
    .m-frame { height: 100vh; }
  }

  /* The six numbers, bound to the six scenes. */
  #act-money-path    { --m-h: var(--m-h-1); }
  #act-whats-inside  { --m-h: var(--m-h-2); }
  #act-the-math      { --m-h: var(--m-h-3); }
  #act-we-move-you   { --m-h: var(--m-h-4); }
  #act-no-fine-print { --m-h: var(--m-h-5); }
  /* THE HEAD — the act's eyebrow and heading, held above everything the act
     does, for the whole act. Present on the very first frame the sticky
     engages, with no entrance of its own: run 04 shipped an act that faded in
     from progress 0 and so was invisible exactly when it was first fully on
     screen. An act's argument is authored present and the engine only ever
     takes it away. */
  .m-head {
    display: block;
    grid-row: 1;
    grid-column: 1;
    transform: translate3d(0, 0, 0);
    will-change: transform, opacity;
  }

  /* THE PANELS. An act's argument is a short deck: each panel takes the
     frame's whole content area and the scrub hands one to the next. They
     stack in one grid cell, which is what lets the frame's padding reserve
     the rail for all of them at once.

     PANEL 1 IS LIT AND THE REST ARE NOT, authored here — the exact mirror of
     the desktop's `.pin-frame .act + .act { opacity: 0 }`. With no script the
     visitor gets each act's first panel finished and still, every word of the
     others in the DOM for a crawler and a screen reader, and a document whose
     height did not move when the script arrived. */
  .m-panel {
    display: flex;
    flex-direction: column;
    /* TOP-ALIGNED, UNDER THE HEAD, and this is a reading decision rather than
       a mechanical one. Centred in a 685px box, a two-line lede floats at the
       middle of the screen with 280px of ground between it and the heading it
       belongs to — two elements that read as unrelated. Top-aligned they are
       one block: the act says its name, then its argument, then hands the box
       to the next panel. It also removes the overflow question entirely,
       because a panel taller than its box now overflows only downward, which
       is the half THE ADVANCE in oyt-stage-mobile.js can walk through. */
    justify-content: flex-start;
    padding-top: var(--v3-5);
    /* AND THE PANEL CLIPS ITSELF rather than letting a tall panel paint over
       the head above it and the rail below it. Nothing is lost to this clip:
       see THE ADVANCE in oyt-stage-mobile.js — a panel taller than its box is
       walked through by the same scroll that is already moving. */
    overflow: hidden;
    grid-row: 2;
    grid-column: 1;
    min-height: 0;
    transform: translate3d(0, 0, 0);
    will-change: transform, opacity;
  }
  .m-panel:not([data-panel='1']) { opacity: 0; }
  /* A panel's first child inherits a top margin written for a block that used
     to follow a heading. Inside a panel that is dead space at the top of the
     frame, and on a 568px screen dead space is a clipped line. */
  .m-panel > *:first-child,
  .m-head > *:first-child { margin-top: 0; }

  /* Act 1's path panel fills its box top to bottom rather than centring:
     the rail has to start directly under the heading and reach the bottom of
     the content area, which is what "evenly spaced" means for the five
     stations hanging off it. */
  #act-money-path .m-panel[data-panel='2'] { justify-content: stretch; }

  /* ACT 3'S TWO MIDDLE PANELS live inside .math-grid, because splitting the
     cost argument anywhere else would have meant wrapping .math-table and
     .paths in boxes that the >=1101px two-column grid addresses with a child
     combinator. display: contents dissolves .math-grid on a phone so its two
     panels become the frame's own grid items; above 1101px the panels are the
     ones that dissolve (oyt-stage.css gives .m-panel display: contents by
     default) and .math-grid's own items are unchanged. The four rules there
     that used `>` are descendant selectors now — identical specificity,
     identical box tree, and the 63-frame desktop hash gate is what proves it
     rather than this comment. */
  .math-grid { display: contents; }

  /* The scene is the block; the frame is the box. The desktop's centred flex
     act and its padding belong to the pinned frame above 900px. */
  .stage .act.m-scene { display: block; align-items: initial; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CHROME
   #stickyBar is 172px tall at 390 — it wraps to three lines — it is fixed at
   the bottom over everything at z-index 99, and its "Get Started" duplicates
   the stage's one gold CTA. Over a full-height scene that is a fifth of the
   screen of duplicate chrome sitting on the act. It is hidden for as long as
   any part of the stage is on screen and restored after the handoff.

   THE !important IS LOAD-BEARING AND NOT A SHORTCUT. #stickyBar carries its
   whole style as an inline attribute on its tag, `transform: translateY(100%)`
   included, and index.html's own scroll handler writes
   stickyBar.style.transform on every scroll event. An inline declaration
   beats any author rule on specificity alone, so !important is the only way a
   stylesheet can win — and winning in the stylesheet rather than fighting
   that handler from script is what keeps one property from having two
   writers.

   THE TRANSITION IS KILLED WITH IT, and that is a determinism fix. The bar's
   own `transition: transform .5s` is a clock: with it, the frame at a given
   scroll position depends on when the stage came on screen. Off, the bar is
   simply gone for the whole stage. It returns with its normal slide once the
   handoff has passed and this class is off, which is the page's existing
   behaviour and outside the stage's determinism contract.
   ═══════════════════════════════════════════════════════════════════════════ */
html.m-stage-over #stickyBar {
  transform: translateY(100%) !important;
  transition: none !important;
  /* AND VISIBILITY, for the reason spelled out at oyt-stage.css's html.stage-over
     rule -- and this file did not have it, so the same defect was live on the
     phone path after the desktop's was fixed. Measured at 320x568, scroll
     y=6970, act 5: getBoundingClientRect put the bar at top 568 in a 568-tall
     viewport, computed transform translateY(171.594px), and
     document.elementsFromPoint(76, 567) STILL returned #stickyBar as the
     topmost element, above act 5's "registered Idaho company" line. The bar
     is not drawn there; it is still hit-testable there, and #stickyBar's
     pointer-events are the default auto, so a tap on that row of pixels would
     reach its button instead of the text. `visibility` is the property that
     means gone. */
  visibility: hidden !important;
}

/* ═══════════════════════════════════════════════════════════════════════════
   NARROW PHONES — 380px and below, at any height.
   Act 1's five stations share whatever the frame has left after the head, in
   five rows of exactly the same height, so the tallest station's copy sets the
   floor. Measured at 360x780: the row is 103px and the worst station needs 104
   at the 15px/1.55 the run specifies, because 290px of column takes that
   sentence to four lines instead of three. One pixel of overlap is still a
   station's copy painting on the next station's label.

   The type stays at 15px/1.55 at 390 and 430 — every width the run captures
   evidence at — and steps down only where the column is too narrow to hold it.
   Measured after: 360x780 goes to 46px of air between every station and the
   next, and the same sentence is three lines rather than four.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 380px) {
  .path-n { font-size: 13px; }
  .path-leak { font-size: 13.5px; line-height: 1.45; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SHORT PHONES
   A sticky frame is exactly one screen tall and overflow: hidden, so a panel
   taller than the frame is CLIPPED — the failure run 04 shipped on a 768px
   laptop and did not see, because its evidence was taken at 900. 320x568 is
   the floor this run names and it is 276px shorter than an iPhone 15's 844:
   the type steps down, the leading closes up, and the same words fit.
   Nothing is hidden and no line is cut.

   AND IT IS GATED ON MOTION, like the frame it exists to serve. Under
   prefers-reduced-motion: reduce there is no sticky frame and no clip — the
   acts are ordinary sections as tall as their own copy — so there is nothing
   for the shrink to buy, and all it would do is hand a reader who has asked
   for less motion 9.5px tile names on a 375x667 phone.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-height: 700px) and (prefers-reduced-motion: no-preference) {
  :root { --m-pad-t: 10px; }
  .act h2 { font-size: clamp(25px, 7.6vw, 30px); margin-bottom: var(--v3-3); }
  .act-lede { font-size: 13px; line-height: 1.42; }
  .act-eyebrow { font-size: 11.5px; margin-bottom: 4px; }
  /* ACT 1'S ROWS ARE THE TIGHTEST THING ON THE PHONE. Five equal rows share
     whatever the frame has left after the head, and the tallest station's copy
     runs to three lines at 320. Measured at 320x568: the row is 69px and the
     worst station needs 65 at 12.5px — true, and two lines of leading from
     reading as one paragraph. 11.5px/1.32 takes the worst station to 60 and
     puts nine pixels of air back between "all day." and INVOICE. */
  .path-n { font-size: 11px; margin-bottom: 2px; }
  .path-leak { font-size: 11.5px; line-height: 1.32; }
  .path-lead { font-size: 10px; margin-bottom: 6px; }
  .m-panel { padding-top: var(--v3-3); }
  .card h3, .promise-card h3 { font-size: 14px; }
  .card p, .promise-card p { font-size: 12.5px; line-height: 1.38; }
  .moves, .promises { gap: 10px; margin-top: var(--v3-3); }
  .card, .promise-card { padding: 12px 13px 11px; }
  .card-mark, .promise-card .card-mark { width: 24px; height: 24px; stroke-width: 1.28; }
  .card-rule, .promise-card .card-rule { margin: 8px 0 9px; }
  .act-close { font-size: 11.5px; line-height: 1.4; margin-top: var(--v3-3); }
  .tile { padding: 6px 8px 5px; gap: 3px; }
  .tile-mark { width: 15px; height: 15px; }
  .tile-name { font-size: 9.5px; line-height: 1.2; }
  .tile-tick { top: 4px; right: 4px; width: 6px; height: 6px; }
  .tile-rule { margin-top: 0; }
  .cluster-tiles { gap: 5px; }
  .cluster-label { font-size: 9.5px; margin-bottom: 4px; }
  .wall { gap: 10px; margin-top: var(--v3-3); }
  .math-table { padding: 6px 10px 4px; margin-top: var(--v3-4); }
  .math-table th, .math-table td { padding: 5px 8px 5px 0; font-size: 11.5px; }
  .math-table thead th { font-size: 9.5px; padding-top: 6px; }
  .math-table .keep { font-size: 11.5px; }
  .math-gap { font-size: 11.5px; }
  .math-cta { margin-top: var(--v3-4); }
  .wall-rest { font-size: 11px; line-height: 1.42; }
}

/* ── RUN 20 — THE PHONE TABLE IS A LEDGER OF 2x2 ENTRIES ─────────────────────
   Four columns in 322px could not be made uniform: "Own Your Tools" split, the
   OYT five-year cell ran five lines deep beside one-line neighbours, and the
   monthly parentheticals wrapped three deep. Below 600px each row is a grid:
   the name and the You-keep figure on the first line, the monthly and the
   five-year total on the second, right column right-aligned. The head row maps
   the same way, so SOFTWARE / YOU KEEP sits over MONTHLY / 5-YEAR TOTAL and
   every entry beneath reads the same shape. Names never wrap; the two sentence
   cells (the OYT total, the capped monthlies) wrap inside their own half.
   Nothing is hidden and not a word or number changes. Same idiom as the
   tranche ladder in index.html at <=480. */
@media (max-width: 600px) {
  .math-table table, .math-table thead, .math-table tbody { display: block; min-width: 0; }
  .math-table tr { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 5fr); column-gap: 14px; row-gap: 3px; padding: 11px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.07); }
  .math-table thead tr { padding: 4px 0 9px; }
  .math-table tbody tr:last-child { border-bottom: none; }
  .math-table th, .math-table td { padding: 0; border-bottom: none; }
  .math-table tr > :nth-child(1) { grid-column: 1; grid-row: 1; }
  .math-table tr > :nth-child(4) { grid-column: 2; grid-row: 1; text-align: right; }
  .math-table tr > :nth-child(2) { grid-column: 1; grid-row: 2; }
  .math-table tr > :nth-child(3) { grid-column: 2; grid-row: 2; text-align: right; }
  .math-table tbody th { white-space: nowrap; }
  .math-table .keep, .math-table .keep-none { white-space: nowrap; }
}
