/* =============================================================
   THE STAGE — Confidence. Ambition. Belonging.

   A sticky scene the reader scrolls through. The track is several
   screens tall; the view pins for its length, and the three words
   arrive one at a time as scroll progress crosses each threshold
   (js/stage.js). Every arrival is a physical event, and each one
   acts out its own word rather than decorating it:

   THE WORDS ARE WRITTEN — 2026-09-18, the owner's instruction,
   pointing at the "COMING SOON" on the client's own brochure: the
   words are to be in that hand, and *the animation shows as if the
   words are being written in front of the visitor*.

   So there is ONE gesture now, not three. The three that were here
   — a word that approached the reader along Z, one that flipped up
   off its baseline, one that closed from two hinged halves — are
   scrapped with the serif they were set in. THE CUTS below are kept
   as the history of a thing that is gone; nothing in them is live.

   HOW IT IS DONE, because it cannot be done with text. A draw-on
   needs the path a hand took, and a font has no such thing — a
   glyph outline traces the EDGE of a stroke, up one side and back
   down the other, so animating it draws a wire around the letter
   rather than the letter. Each word is therefore a GENERATED SVG:
   the letterforms as filled paths, and beside each one its own
   centreline, recovered by thinning the glyph
   (assets/_source/words/make-words.py). The page paints the letter
   through a `<mask>` holding that centreline, stroked wide enough
   to cover it, and animates the stroke's `stroke-dashoffset`. What
   the reader sees is ink arriving where a brush would have laid it,
   letter after letter, left to right.

   THE HAND KEEPS ONE SPEED — across a letter, across a word and
   across all three words. Every pen path carries `pathLength="1"`
   and its own share of its word's ink as `--t0` / `--dur`, so CSS
   turns ink into time and never measures a path (measuring one too
   early is how a draw-on ends up running twice). `--write-dur` per
   word is that word's ink over one rate.

   RE-CUT 2026-09-06: the chalk icons beside the words, the chalk
   rules under them and the marks behind the scene are gone. The
   words stand alone, on the bare wall.

   SCROLL IS THE FRAME. Since 2026-09-19 (evening) there is no
   timer under the writing: each scene's `--wp` (0..1) is the
   reader's position in that word's slice of the track's travel,
   set by js/stage.js on every scroll frame, and it seeks a paused
   CSS animation to that frame via a negative delay. Scrolling
   forward writes, scrolling back un-writes, and the reader who
   flicks past sees the composition wherever it lands and nothing
   is played AT them. Same either way by construction.
   The two clocks that stood here — the barrier-gated arrivals
   with a sequencer and a scaled reverse run (`--rw`), and before
   that a scroll-lock that held Belonging on screen — both put a
   timer between the reader and the page and the reader lost.
   ============================================================= */

.stage {
  position: relative;
  z-index: 1;
  /* THE TRACK IS THE WORDS' CANVAS OF SCROLL, and since the writing was made
     scroll-scrubbed on 2026-09-19 (evening) it is also all there is to their
     pace: reader crossing 30% of the track = each word 30% written, so a
     brisk flick sweeps them past and a slow read draws them in the browser's
     own hand. `SLICES` in js/stage.js divides this 100% into three writing
     zones (~28% each) and a hairline of silence at head, tail and between.
     Longer track = longer reading; shorter = shorter; the timing goes with
     the length.

     176vh survives the timer cut with no reason to move: it was chosen for
     rhythm against a barrier-and-timer that no longer runs, and rhythm at
     scroll-scrub pace is just how far the reader has to move to write each
     word. If the owner wants the words slower, this is the number.

     History: 480vh (relaxed cut of 2026-09-06) → 176vh (fifth cut of
     2026-09-18, "the words cost too much scrolling"). Before 480 it was
     340; before all of that, 375 was briefly stretched to try to give
     Belonging time to finish before the block scrolled off, which never
     worked because the arrivals were timed animations and could be
     outrun at any track length. That whole thread ended when the timer
     did. */
  /* --stage-length RETIRED 2026-09-20 at the owner's ask ("no scrolling
     friction at all"). The scene no longer pins; it flows at section
     rhythm, and every one of the reduced-motion overrides below is now
     universal, not gated on the media query. Kept in `.js:not` and
     mobile-breakpoint blocks below as commented history for a possible
     reinstatement. */
  --scene-gap: clamp(var(--space-m), 7.5vh, var(--space-3xl));
  /* Owner's instruction, 2026-09-07: the three words shrunk by 20% off
     the previous ramp (was clamp(2.3rem, 10.4vw, 6rem)). Every anchor
     scales together — floor, vw slope, ceiling — so the sizing ramp
     keeps its shape and the icons keep their optical relation to the
     letters (0.72em / 0.21em / 0.36em gap all in `em`, so they follow
     the word down without a second edit). */
  /* BIGGER — owner, 2026-09-18: the brush words up ~22% from the ramp that
     carried L20's 20% shrink. Both the words and the three icons follow it
     (the icon is 0.72em of this), so the composition keeps its proportion.
     Was clamp(1.84rem, 8.32vw, 4.8rem). Fit re-verified at 14 viewports.

     THE RAMP IS 12.5vw — design-rules L29d, corrected 2026-09-19 on the
     owner's own phone. THE CAP AND EVERY DESKTOP VIEWPORT ARE UNCHANGED:
     6.74rem is reached at 862px instead of 924px, so 862 and up resolve to the
     same 107.8px they always did, continuously, with no breakpoint cliff.

     IT WAS BRIEFLY 17vw AND THAT WAS WRONG TWICE OVER.

     Wrong on the fit: the ceiling was set from `getBoundingClientRect()` on
     `.scene`, which reported 19.8px of gutter either side. `.scene__ink` is
     `overflow: visible`, so the glyph PAINTS about 11px past its own box —
     the real gutter at 17vw was 19.8 LEFT and 8.7 RIGHT, lopsided and
     effectively edge to edge, and `Confidence.`'s final stop sat on the screen
     edge. Measure `getBBox()` through `getScreenCTM()`, never the element box.

     Wrong on the reason: 17 was chosen to stop hairline dry drags closing into
     grey smear, off a ratio of anti-aliased to solid pixels measured on a
     capture DOWNSAMPLED to CSS pixels. That throws away the dsf-3 resolution a
     phone actually has, so it modelled a phone as a small desktop. A real
     phone resolves those drags SHARPLY, and at 291px they open the other way —
     into slots. The owner's word for the C at that size was "glitchy, broken",
     and it was hollow: a drag running most of a stroke (DRY_LEN reaches 0.82)
     read as an outlined letter rather than a brush one.

     So the drags have a window. Too small and they smear; too large and they
     slot. Rendered and compared at true size, 12.5vw (word 214px, painted
     gutters 61.8/62.4) sits inside it and 14.5 is already opening. This is 7%
     above the 11.67 that stood before any of it, which is the size that had
     never drawn a complaint. */
  --word-size: clamp(1.574rem, 7.625vw, 4.111rem);
  /* NO REWIND SPEED ANY MORE. The reverse run used to be a scaled play-back
     of the forward animation on a clock of its own (`--rw` here, `REWIND_MS`
     in stage.js). Both went with the timer on 2026-09-19: `--wp` (a share of
     the reader's scroll through a word's slice) IS the animation frame, and
     scrolling back is the reverse run — seeking a paused animation to a lower
     `wp` runs the same keyframes back, at the reader's pace, with no matching
     number to keep in sync. See THE WRITING below. */
  --p: 0;
}

/* No sticky pin, no track-driven friction — the scene flows at section
   rhythm now (owner, 2026-09-20). What used to be gated on
   prefers-reduced-motion is now universal; every override is at the head
   of the file below the writing block. 2026-09-21: the pinning stayed
   retired but the section's own vertical air was restored ("everything
   got too crammed … restore the scrolling distance to and from the 3
   words"). `min-height: 100svh` reserves one viewport for the section
   and `justify-content: center` sits the three words in the middle of
   it, so the reader scrolls a full screen through the section at their
   own pace with no pin. */
.js .stage__track { height: auto; }

.stage__view {
  position: static;
  min-height: 100svh;
  padding-block: var(--space-3xl);
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: clip;
  isolation: isolate;
}

/* no script: a plain, complete list, at section rhythm */
html:not(.js) .stage__view { position: static; height: auto; padding-block: var(--space-3xl); }

/* sized to the widest word and centred, so the three left-aligned words
   sit as one block on the page's axis */
.stage__list {
  list-style: none;
  margin: 0 auto;
  padding: 0 var(--gutter);
  width: max-content;
  max-width: min(100%, var(--container-mid));
  display: flex;
  flex-direction: column;
  gap: var(--scene-gap);
  position: relative;
  z-index: 1;
}

/* the icon and its word, on one line, the icon first; the gap is in the
   word's em. The icon is set on the word's OPTICAL centre, not the box's:
   measured on Fraunces 600 at 96px, the baseline is 0.858em down the 1em
   box, the cap height 0.708em and the x-height 0.448em, so the caps centre
   on 0.504em and the lowercase on 0.634em — and a word that is one capital
   and nine lowercase letters reads as centred between the two, at ~0.57em.
   The 0.72em icon's top therefore sits at 0.21em. */
.scene { display: flex; align-items: flex-start; gap: 0.36em; font-size: var(--word-size); }

/* The word. As of 2026-09-21 (owner's ask) the three words are SET IN THE
   PAGE'S DISPLAY FACE — Newsreader italic — not painted as SVG outlines.
   The painted pipeline (`make-words.py`, the `.scene__ink` block below,
   the pen dash animation, `js/stage.js`'s `--wp` scrubbing) is retired
   with this cut; the CSS rules for `.scene__ink` and `.pen` are left as
   history for a possible reinstatement and do not run against anything
   that ships. Size matched to the previous painted rendering by feel,
   not by pixel: `--word-size` remains the ramp it was, and `.scene__text`
   is set at `calc(var(--word-size) * 1.5)` so the italic's cap-to-baseline
   sits at roughly the painted word's box. */
.scene__word { display: block; }

.scene__text {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--weight-display);        /* 500, same as h1/h2 */
  font-variation-settings: var(--wonk-display);  /* opsz 96, SOFT 0, WONK 0 */
  font-style: italic;                        /* the previous cut leaned; the italic keeps that gesture */
  font-size: calc(var(--word-size) * 1.5);
  line-height: 1;
  letter-spacing: -0.02em;
  color: inherit;                            /* .scene owns the green */
}

/* THE SVG IS A REPLACED ELEMENT, so BOTH axes are stated. Given a height
   alone the width resolves off the intrinsic ratio, which is how a chalk
   underline once shipped at 40% of its word. `--ink-w` and `--ink-h` are the
   word's own box in its own em, written beside the viewBox by the generator,
   so the pair cannot drift from the drawing. `max-width: none` is the other
   half of the same lesson: the reset's `img, svg { max-width: 100% }` reaches
   in here and would cap a word at its parent and scale the viewBox down with
   no error to see. */
.scene__ink {
  display: block;
  flex: none;
  width: calc(var(--word-size) * var(--ink-w));
  height: calc(var(--word-size) * var(--ink-h));
  max-width: none;
  overflow: visible;
  fill: currentColor;
}

/* THE THREE WORDS ARE BLACK — the owner, 2026-09-06 (evening). They were in
   the three display tones (coral, gold, teal); they are now the guide's own
   body black, `--ink-900`, which is what `--ink` resolves to. The ICONS COME
   WITH THEM and need no rule of their own: `.scene__icon` is
   `fill: currentColor` (`stroke: currentColor` while they were hairlines), so
   each mark has always taken its word's colour and still does — and so do the
   letters now, since a generated glyph is `fill: currentColor` too. `--dust` is
   left named per word because the dust and the streak have been `display: none`
   since the elegance cut — the variables paint nothing, and re-pointing them
   would only make a dead value look live.
   Contrast goes up, not down: computed on white, #111111 is 18.88:1 against
   3.57 / 3.43 / 3.53:1 for coral, gold and teal — the tones were legal only
   because the words are display-sized. See design-rules L1/L2 — the pastels still carry the page's headings;
   this is the stage only. */
/* GENERATED by assets/_source/words/make-words.py — re-run it rather
   than editing these. Each word's own hand time over one shared rate,
   so the hand does not hurry on the longer word. */
.scene--confidence  { --write-dur: 1500ms; }
.scene--ambition    { --write-dur: 1245ms; }
.scene--belonging   { --write-dur: 1421ms; }

/* THE THREE WORDS AND THEIR ICONS ARE FOREST GREEN — owner, 2026-09-19
   (evening). They were `--ink` (`#111111`, guide black) from 2026-09-06
   until here. The mark beside each word is `fill: currentColor`, so the
   colour follows the letters and both change together. */
.scene--confidence { color: var(--green); --dust: var(--dust-coral); }
.scene--ambition   { color: var(--green); --dust: var(--dust-gold); }
.scene--belonging  { color: var(--green); --dust: var(--dust-teal); }

/* THE DEBRIS IS GONE — 2026-09-06, the elegance cut. The dust under a word,
   the ring round each stamped letter and the smear under Ambition's row were
   impact effects: a comic's way of saying "hit". They were left in the markup
   and simply not painted until 2026-09-18, when the words were rewritten as
   generated SVG and the spans went with the text they decorated. The rule is
   kept for one release so a cached page cannot paint one. */
.scene__dust, .scene__streak { display: none; }

/* `.is-hold` and its style-flush routine went with the barrier-and-sequencer
   on 2026-09-19: nothing swaps class names any more, so there is no forward
   animation for a reverse one to shadow-overwrite (see design-rules L28 /
   memory css-same-name-animation-no-restart). One paused `write` per pen,
   with a delay that follows `--wp`, is the whole run. */


/* =============================================================
   THE CUTS OF 2026-09-06 — HISTORY. NOTHING BELOW IS LIVE.

   Every gesture described here was scrapped on 2026-09-18, when the
   owner replaced the three arrivals with one: the words are written
   (THE WRITING, below, and design-rules L29). It is kept because it
   is the record of four rounds of the owner's own notes on how these
   three words should move, and the conclusions outlived the gestures
   — energy before contact, no bounce, no squash, no tilt, no debris,
   and a frame that is never dead. A hand writing obeys all of them.
   Read it as reasoning, never as a description of the page.

   The three arrivals were re-shaped four times on the owner's notes.

   FIRST (the elegance cut): the words read as playful when the
   brand is premium. What made them playful was measured, not
   guessed (scrubbed frame by frame, transforms sampled every 40ms):
     · every move REVERSED at least once — Confidence pressed to
       0.88 and sprang back to 1.025; Ambition jumped 0.34em past
       its line and dropped; Belonging overshot the seam by 5px and
       wobbled three times. A reversal is a bounce; a bounce is a
       cartoon.
     · the GLYPHS DEFORMED — stretched 1.5× tall, pressed to 0.88.
       Squash and stretch is the first principle of character
       animation; a serif that deforms stops being a typeface.
     · everything TILTED ±3° on the way in. The wonk was taken out
       of the type (L13) and left in the motion.
     · DEBRIS on impact — rings, puffs, a smear.
     · the visible part of each move was VIOLENT and SHORT: 230px
       in 80ms; 1.16 → 0.88 in 40ms; Belonging drifted invisibly for
       a second, stuttered at a keyframe join, then slammed.
   That cut removed all five and put every move on a single
   ease-out. Premium — and, the owner's note, lifeless: it read as
   the fade the client had refused. Measured why: an ease-out's top
   speed is at frame zero, when the letter is still behind the clip
   or under the opacity gate, so the reader only ever saw the tail.

   SECOND: THE ENERGY GOES BEFORE CONTACT. A cartoon spends its
   energy after the hit — squash, bounce, debris. Cinema spends it
   before — the approach, the weight — and after the hit there is
   only stillness, or a slow camera. Chalk hits a board and stops.
   A die tumbles fast and comes to rest without bouncing. So every
   arrival became a VISIBLE ACCELERATION ending in a HARD, CLEAN
   STOP — nothing after contact but the composition itself, which
   went onto a slow scroll-bound push-in (THE CAMERA, below) so the
   frame is never dead between the three events.

   THIRD (this one): DEPTH. The owner liked Confidence and found the
   other two flat — and they were, literally: Confidence is the only
   word that arrived along the Z axis, the reader watching the whole
   word hover near and then land, while Ambition and Belonging were
   slides in the plane of the wall, and a slide through a clip is
   the vocabulary of a heading, not an event. The client's own
   images — chalk hitting a board, a die landing — are objects with
   mass arriving at a surface. So the two flat words became rigid
   3D events with the same ease-in and the same hard stop:
   Ambition STANDS UP off its baseline in perspective; Belonging's
   halves swing in on hinges at their outer edges and MEET at the
   seam. Same vocabulary, three inflections.

   FOURTH (the relaxed cut): SLOWER, AND MORE SCROLL. The owner's
   note on the picks: more deliberate, more relaxed — the words are
   not in a rush, they know they are cool, they take their time. So
   the track is 480vh instead of 340 (each word takes half a screen
   more scrolling to reach), every approach is ~1.6× longer with a
   wider ripple, and the sequencer's gap on a flick is 650ms instead
   of 450. The stop is the same stop; it just comes later.
   Still banned: overshoot, spring, squash, stretch, tilt, dust.
   ============================================================= */


/* ---------- THE CAMERA ----------
   The whole composition is on a slow push-in bound to progress
   through the track — `--p`, which js/stage.js writes on the stage
   every frame — with the three rows on slightly different depths so
   they draw apart by a few pixels as the reader goes down. Nothing
   here is timed; it is the scroll, so it holds still when the reader
   does. It exists because a frozen frame between arrivals is what
   made the arrivals read as fades. */
/* THE CAMERA IS A RATE, AND IT DID NOT COME DOWN WITH THE TRACK — fixed
   2026-09-19 on the owner's *"if you scroll up, the three words kind of shrink,
   it's a strange effect."*

   `--push` and `--dz` are amounts spread over the track's whole TRAVEL, so what
   the reader feels is the amount divided by the travel. Both were set at
   480vh of track — 380vh of travel — and both survived the cut to 176vh / 76vh
   untouched, which multiplied the rate by FIVE. The same 5% zoom that used to
   take 3205px of scrolling at a 844px viewport now takes 641px, and scrolling
   back up plays it in reverse: the words visibly shrink.

   `GAP` and `BREATH` were brought down with the track for exactly this reason
   and are commented as such in js/stage.js; the camera was simply missed.

   Rate-matched to what it was: 0.05 x (76/380) = 0.01, and 14px x (76/380) =
   ~3px. Both now move at the pace they were designed to. Anything set here in
   px or in absolute scale IS a rate — if the track length changes again, these
   two change with it. */
.stage { --push: 0.01; }
.js .stage__list {
  transform: scale(calc(1 + var(--push) * var(--p)));
  transform-origin: 50% 50%;
  will-change: transform;
}
.js .scene { transform: translateY(calc((var(--p) - 0.5) * var(--dz, 0px))); }
.js .scene--confidence { --dz: -3px; }
.js .scene--belonging  { --dz:  3px; }


/* =============================================================
   THE WRITING — the one gesture, and how a word spends its time

   THE WORDS ARE NOT TYPESET. There is no font on this page for them:
   each letter is built out of brush strokes by
   assets/_source/words/make-words.py, and what is in the markup is
   the ink it laid plus, per letter, the strokes that laid it. See
   design-rules L29 — and note that `.pen` below is now MANY paths
   per letter, one per stretch of stroke, each carrying its own width
   so the head of the stroke is the size of the stroke it is laying.

   A scene's run is `--write-dur`, set on the SCENE (not on the word's
   `<svg>` — the mark beside it is a sibling and would never inherit
   it there, which cost all three icons for one cut) from that word's
   own ink at one shared rate: 1500ms for Confidence, 1007 for
   Ambition, 1224 for Belonging.
   THE NUMBER IS A UNIT, NOT A SPEED, since the writing was made
   scroll-scrubbed on 2026-09-19 (evening). Nothing plays over
   `--write-dur` — the animation is `paused` — but each pen's slice
   of the run is still `--dur * (1 - --icon-share) * --write-dur`,
   and the negative delay that seeks it uses the same multiplier. So
   the number governs the RATIO of each letter's slice to the whole,
   which is what carries the ink's own rhythm. Changing it in ms scales
   every letter's slice by the same factor — invisible, since the
   frame is `wp * --write-dur` on both sides. Confidence's 1500 is
   the number its arrival used to take on the timer; kept for
   continuity with make-words.py's numbers.

   THE SCENE IS SPLIT, ICON FIRST. The mark sits to the LEFT of its
   word, so a hand crossing the line reaches it first: `--icon-share`
   of the run uncovers the mark, and the remaining share writes the
   word. Splitting the run rather than adding to it is what keeps
   the total at `--write-dur` and everything downstream true.

   EVERY NUMBER A LETTER NEEDS IS ON THE LETTER. `--t0` is where it
   starts as a share of its word's ink, `--dur` how much of that ink
   it is. `--t1` is still emitted by make-words.py — a letter's
   distance from the END of the word, which the scaled-reverse rewind
   used and this scroll-scrub replacement no longer does — kept
   because dropping a generated field would ripple through the tool
   for no gain. CSS multiplies them by `--write-dur` and nothing is
   measured at runtime — `pathLength="1"` normalises every path in
   the markup, so there is no `getTotalLength()` to run too early
   and no `--len` to set on a second frame.

   `linear`, and deliberately. The time each letter gets is already
   proportional to the ink it lays, so a linear ramp IS a constant
   hand speed; easing it would make the pen hurry and dawdle inside
   every letter while the letters themselves stayed evenly spaced.
   ============================================================= */

.scene {
  /* of a scene's run, the share spent uncovering the mark before the
     first letter is written. 0.16 of ~1.5s is ~240ms — long enough to
     read as a stroke of its own, short enough that the word does not
     feel held up behind it. */
  --icon-share: 0.16;
}

.scene__ink .pen {
  fill: none;
  stroke: #fff;                  /* a mask is luminance: white reveals */
  stroke-linecap: round;
  stroke-linejoin: round;
  /* pathLength="1" is on every path in the markup, so one dash covers any
     stroke whatever its real length, and the offset is a fraction.
     `1 2`, NOT `1`. A single value is a period of `1 1` — dash one, gap one —
     so with the offset at 1 the END of the path lands exactly on the pattern's
     wrap, a zero-length dash begins there, and A ZERO-LENGTH DASH WITH A ROUND
     CAP PAINTS A FULL CIRCLE. That shipped: one dot per letter, twenty-nine of
     them sitting on the page before a word had been written, which is what the
     owner saw and reported on 2026-09-18. With a gap of 2 the next dash starts
     one whole path-length past the end and can never be reached. */
  stroke-dasharray: 1 2;
  stroke-dashoffset: 1;
}
/* no script: the words are simply written */
html:not(.js) .scene__ink .pen { stroke-dashoffset: 0; }

/* SCROLL DRIVES THE FRAME. `--wp` is 0..1, written on the scene by
   js/stage.js from the reader's position in that word's slice of the
   track's travel. The animation is `paused` — the browser never advances
   it — and a negative delay seeks it to `wp * write-dur` on its own
   timeline, which is where the paused `write` keyframe puts stroke-
   dashoffset for that reader-frame. So the ink appears when the reader
   scrolls into it and retreats when they scroll back, at whatever pace
   they scroll. See THE HOLD IS GONE in stage.js: this is the answer
   that replaced both the scroll-lock (which held the reader hostage
   to the clock) and the barrier-timer cascade (which forced them to
   watch three rewinds on the way up as well).
   THE MATH. A pen writes over its own share of the run — starting at
   wp = icon-share + t0 * (1 - icon-share), ending at wp = icon-share +
   (t0 + dur) * (1 - icon-share). Seeking a paused animation with delay
   `-(wp - start_wp) * write-dur` puts stroke-dashoffset at 1 - progress
   through that pen's slice. With animation-fill-mode: both, wp before
   start holds the from state (dashoffset 1, nothing painted) and wp
   past end holds the to state (dashoffset 0, fully painted). */
.js .scene .scene__ink .pen {
  animation: write
    calc(var(--dur) * (1 - var(--icon-share)) * var(--write-dur)) linear
    calc((var(--icon-share) + var(--t0) * (1 - var(--icon-share)) - var(--wp, 0)) * var(--write-dur))
    both paused;
}
@keyframes write { to { stroke-dashoffset: 0; } }


/* ---------- the icons ----------
   THE ICONS ARE BACK — 2026-09-06, the owner's instruction. The middleman
   struck the chalk icons as childish; the owner's read is that the subjects
   were never the problem, the execution was — thick chalk strokes, rays
   round the fist, a wobble. So: the same three subjects the client's own
   page put beside these words — the sparkle, the raised fist, the handshake
   — in one 120-unit box each, BEFORE the word (owner's note, later that
   day), in the word's own colour.

   SOLID — 2026-09-06, night, the owner's instruction: the hairline
   execution was not good either; "invert fill and outline: full icons, no
   outline". So each mark is now ONE FILLED SHAPE — `fill: currentColor`, no
   stroke anywhere — its detail cut OUT of the mass as uniform, round-capped
   seams, which is how solid icons are cut. They are constructed, not drawn:
   assets/_source/icons/build-icons.py builds each from primitives and
   booleans (shapely) and writes the path into index.html; the fist is the
   owner's pick "S" (thumb as one crescent sweep), the sparkle "R1, slightly
   pointier", the handshake "Q3c" — the composition of Phosphor's MIT
   `handshake`, the one handshake that still reads at 26px, rendered solid.

   HOW THEY ARRIVE, RE-CUT 2026-09-18. Each mark used to be uncovered in the
   direction of its own word's gesture — Confidence's grew from its centre as
   the word closed onto the wall, Ambition's rose from its baseline as the
   letters stood up, Belonging's met in the middle as the halves swung in.
   Those three gestures are gone, and with them the only reason the three
   reveals differed. **All three are now uncovered LEFT TO RIGHT**, because
   that is the direction of the hand that is about to write the word beside
   them, and a mark that appeared some other way would be the one thing on
   the line not obeying it.
   No opacity: a fully inset clip paints nothing, so the waiting mark can
   never ghost. The animation lives inside the scene, so the sequencer's
   "one word at a time" and the hold on Belonging wait for it, and REWIND
   runs it in reverse — and LAST, after the word it introduces has gone,
   which is the same hand going back the way it came. */
.scene__icon {
  flex: none;
  width: 0.72em; height: 0.72em;
  /* THE OPTICAL CENTRE, RE-DERIVED FOR THIS FACE. The old 0.21em was measured
     on Fraunces inside a 1em line box; the word is now an SVG 1.451em tall
     with its baseline 1.084em down from the top, so the number had to move
     even though the two faces are near enough the same shape — Kaushan
     Script's cap height is 0.714em against Fraunces' 0.708 and its x-height
     0.452 against 0.448. Caps therefore centre 1.084 - 0.714/2 = 0.727em down
     the box and lowercase 1.084 - 0.452/2 = 0.858em, and a word that is one
     capital and nine lowercase letters reads as centred between them, at
     ~0.794em. A 0.72em mark's top sits half its height above that. */
  margin-top: 0.434em;
  overflow: visible;
  fill: currentColor;
  stroke: none;
}
/* the waiting state is the first keyframe: nothing painted */
.js .scene .scene__icon { clip-path: inset(0% 100% 0% 0%); }
.js .scene .scene__icon {
  animation: uncover calc(var(--icon-share) * var(--write-dur)) linear
             calc(-1 * var(--wp, 0) * var(--write-dur))
             both paused;
}
@keyframes uncover {
  from { clip-path: inset(0% 100% 0% 0%); }
  to   { clip-path: inset(0% 0% 0% 0%); }
}
/* no script: the marks are simply there */
html:not(.js) .scene__icon { clip-path: none; }


/* ---------- the cue ----------
   Shown while the stage is pinned and empty; gone once the first
   word is up. */

.stage__hint {
  position: absolute;
  left: 0; right: 0;
  bottom: clamp(var(--space-m), 5vh, var(--space-2xl));
  z-index: 1;
  text-align: center;
  font-size: var(--step--2);
  font-weight: var(--weight-semi);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
  max-width: none;
  transform: translateY(14px);
  opacity: 0;
  transition: transform 700ms var(--ease-out), opacity 700ms var(--ease-out);
  pointer-events: none;
}
html:not(.js) .stage__hint { display: none; }
.stage.is-active:not(.has-word) .stage__hint { transform: none; opacity: 1; }


/* ---------- short and narrow screens ---------- */

@media (max-width: 719px) {
  /* `--stage-length` retired 2026-09-20 (see the head of this file); no
     mobile override needed because the track is `height: auto` and the
     scene is `position: static`. */
}
@media (max-height: 540px) {
  /* short-height override, matched to the 2026-09-07 20% shrink above
     (was clamp(1.9rem, 6vw, 2.6rem)). */
  .stage { --word-size: clamp(1.299rem, 4.111vw, 1.781rem); --scene-gap: var(--space-xs); }
}


/* ---------- no scrolling friction, universal ----------
   These used to sit inside `@media (prefers-reduced-motion: reduce)`;
   hoisted out on 2026-09-20 when the owner retired all scroll-scrubbed
   friction on the three words. The words are simply *there* at section
   rhythm — the mask is fully revealed (see the last rule; killing the
   animation alone leaves `stroke-dashoffset: 1`, i.e. a word that has
   never been written), the entry parallax and scale are killed so a
   frame-jump of `--p` isn't visible, and the "scroll down" hint that
   named the missing friction is hidden. `js/stage.js` still writes
   `--p` and `--wp` on scroll; these overrides make those writes visually
   inert. */
.js .stage__list, .js .scene {
  transform: none !important; animation: none !important;
  opacity: 1 !important;
}
.stage__hint { display: none; }
.js .scene__icon { clip-path: none !important; animation: none !important; }
.js .scene__ink .pen { animation: none !important; stroke-dashoffset: 0 !important; }

/* Icons removed per owner 2026-09-20 — the SVG is left in the markup
   but hidden, and `--icon-share` is zeroed so the word reveal starts
   immediately rather than after the (invisible) icon window. */
.scene__icon { display: none; }
.scene { --icon-share: 0; }
