/* =============================================================
   COMPONENTS — Imposter Play Lab
   Order: motion · masthead · hero · (words: stage.css) · the idea ·
   early access · the closer + form · buttons · footer · welcome
   page · breakpoints.

   RE-CUT 2026-09-06. The chalk system — a filter roughening dozens
   of small SVG strokes, swipes under words, scribbles and stars in
   every corner, a ring round "Coming soon", a spine down the page —
   is retired. It was the playful half of the brief and it tipped
   into childish. Nothing decorative replaces it. What the page has
   now is type, space, the white ground and the stage's motion;
   the brand's pastels appear as text tones on the light ground
   and at full strength on the green.
   ============================================================= */


/* =============================================================
   1. MOTION
   One reveal primitive, driven by IntersectionObserver in site.js.
   Everything animates transform + opacity only. Contained: a rise
   of 26px over 2.04s on an expo-out, the opacity over in the first
   40% so what is watched is the settle, not the fade.
   THE DURATION IS ONE TOKEN — `--dur-cine` in tokens.css, which
   nothing outside this section reads. It went 1200 -> 2040ms on
   2026-09-06 on the owner's note (1.7x slower); every multiplier
   below is a share of it and moved with it. See the token.
   ============================================================= */

.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y, 26px), 0);
  transition:
    opacity calc(var(--dur-cine) * 0.4) var(--ease-out),
    transform var(--dur-cine) var(--ease-cine);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }

/* the big serif lines travel a little further */
[data-reveal="rise"] { --reveal-y: 34px; }

/* LIFT — the sign-up card rising into place, the fade over inside the first
   quarter, so what you watch is the travel. */
.js [data-reveal="lift"] {
  opacity: 0;
  transform: translate3d(0, 44px, 0);
  transition:
    opacity calc(var(--dur-cine) * 0.25) var(--ease-out),
    transform var(--dur-cine) var(--ease-cine);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal="lift"].is-in { opacity: 1; transform: none; }

/* LINES — the serif headings rise line by line out of their own baseline.
   Each `.line` is a clipped box; the text inside starts below it and comes
   up on the expo-out, one line a beat after the last. Nothing fades: the
   line is either below its baseline or on it. The clip is padded and pulled
   back by the same amount so descenders and the italic's overhang are not
   cut, and it is the INNER span that moves, so the heading's own box never
   changes size. */
.js [data-reveal="lines"] { opacity: 1; transform: none; }
.line {
  display: block;
  overflow: hidden;
  padding: 0.08em 0.14em 0.18em;
  margin: -0.08em -0.14em -0.18em;
}
.line > span {
  display: block;
  text-wrap: balance;
}
.js .line > span {
  transform: translate3d(0, 115%, 0);
  transition: transform calc(var(--dur-cine) * 1.15) var(--ease-cine);
  /* 150ms until 2026-09-06; scaled by the same 1.7 as --dur-cine, because
     the beat between lines is this primitive's own rhythm rather than the
     composition's arrival order (which stayed where it was) */
  transition-delay: calc(var(--reveal-delay, 0ms) + var(--i, 0) * 255ms);
}
.js .is-in .line > span { transform: none; }
.line:nth-child(2) { --i: 1; }
.line:nth-child(3) { --i: 2; }

/* HAIRLINES DRAW IN — a rule is drawn from its left end as its block arrives,
   over the same clock as the reveal. Movement that belongs to the structure. */
.rule-in {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform calc(var(--dur-cine) * 1.4) var(--ease-cine) var(--reveal-delay, 0ms);
}
.is-in > .rule-in, html:not(.js) .rule-in { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; transform: none; }
  .js .line > span { transform: none; transition: none; }
  .rule-in { transform: none; transition: none; }
}

/* parallax — JS writes --shift, CSS applies it. Transform only. Nothing on
   the page carries it since the re-cut; the rule stays for the day something
   does, because site.js still drives it. */
[data-parallax] { will-change: transform; transform: translate3d(0, var(--shift, 0px), 0); }


/* =============================================================
   2. MASTHEAD
   The logo centred and large, as the client's own page opens and
   as the owner set it on 2026-09-05; "Coming soon" under it as a
   quiet tracked tag — the client's string, no ring, no sparkle,
   no pulsing dot. Those were the childish part.
   ============================================================= */

.masthead {
  position: relative;
  z-index: 3;
  /* asymmetric: air above, nothing below — the space under the mark
     belongs to the body's own distribution (design-rules, Layout) */
  padding-block: clamp(var(--space-m), 6.5vh, var(--space-3xl)) 0;
  flex-shrink: 0;
}
.masthead__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(var(--space-s), 2vh, var(--space-l));
}
.masthead__logo { display: block; }
.masthead__logo img {
  /* THE SIZE IS `--logo-w`, AND EXACTLY ONE DECLARATION APPLIES THE SCALE.
     Every rule below states the unscaled width and nothing else, so the
     enlargement cannot be applied twice, missed at one breakpoint, or
     silently dropped when a rule is edited — which is the whole reason the
     ladder is a ladder. */
  --logo-w: clamp(150px, 19vw, 260px);
  width: calc(var(--logo-w) * var(--logo-scale, 1));
  height: auto;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  font-size: var(--step--2);
  font-weight: var(--weight-medium);
  letter-spacing: 0.26em;
  text-indent: 0.26em;   /* the tracking after the last letter, given back on the left */
  text-transform: uppercase;
  color: var(--gold-ink);
  max-width: none;
}
/* set between two short rules, the way a running head is */
.tag::before, .tag::after {
  content: '';
  width: 22px;
  height: 1px;
  background: var(--gold-ink);
}


/* =============================================================
   3. HERO — one screen
   The client's headline in the serif, the last two words in
   coral; their join sentence; one gold button to the sign-up
   card; the scroll cue. Four beats, unevenly spaced on purpose:
   the headline and the way in are one idea and sit close, the cue
   is a separate invitation and gets the biggest gap. Two flexible
   spacers at 1 : 1.9 do it (design-rules, Layout).
   ============================================================= */

.hero {
  /* THE HERO'S MARK IS 30% LARGER THAN EVERY OTHER MASTHEAD'S — the owner,
     2026-09-18. It is ONE scalar and not six edited numbers because the mark's
     width is set in six places (the base rule, the narrow rule, the `--rise`
     ramp and three short-screen rules) and they are a designed ladder: edited
     by hand they drift apart, which is the defect the ramp below was written
     to end. `calc(clamp(...) * var(--logo-scale))` scales every anchor of a
     clamp together, so the ramp keeps its shape and still reaches its ceiling
     at the same viewport height.
     SCOPED TO `.hero`, and that scoping is the same lesson as the ramp's: the
     bare `.masthead__logo img` rules also style the CONFIRMATION PAGE, which
     is a different composition with its own steps. It inherits the fallback
     `1` and is untouched. */
  --logo-scale: 1.3;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100svh;
  padding-bottom: var(--space-s);
  isolation: isolate;
}

.hero__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  text-align: center;
  padding-block: clamp(var(--space-xs), 1.4vh, var(--space-m));
  gap: 0;
}
.hero__body::before,
.hero__body::after { content: ''; display: block; width: 0; }
.hero__body::before { flex: 1 1 0; }
.hero__body::after  { flex: 1.9 1 0; }

.hero__title {
  font-size: var(--step-5);
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--heading);
  /* 12.11em IS 20ch IN FRAUNCES, and it is written in `em` on purpose.
     `ch` resolves against the FIRST AVAILABLE font, so while the webfont is
     swapping the box is measured in the fallback's `0` — and both the text
     and the box then scale together, which makes the ratio between them
     invariant under `size-adjust`. No metric match can fix a `ch` box: the
     fallback needed 46.8ch where Fraunces needs 41.3ch, so the headline
     wrapped to four lines however the face was tuned. In `em` the box is
     font-independent and the metric match lands. Fraunces' `0` is 0.6055em,
     so 20ch = 12.11em — 790.66px vs 790.73px at 1440, a 0.07px difference
     from today's approved rendering. See design-rules L16. */
  max-width: 12.11em;
}
/* two lines, each its own clipped box (see LINES above); the break the
   client's copy is written for is where the boxes meet. Below 900px each box
   wraps inside itself, balanced. */
@media (max-width: 899px) {
  .hero__title { max-width: 9.0825em; }   /* 15ch in Fraunces — see above */
}
/* the last two words — a brand pastel, at a size where the display tone is
   legal (≥24px at every width the headline takes) */
.hero__accent { color: var(--coral-display); font-style: italic; }

/* the tie between the claim and the way in */
.hero__join {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(var(--space-m), 3.4vh, var(--space-l));
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  margin-block-start: clamp(var(--space-l), 6.2vh, var(--space-2xl));
}
.hero__prompt {
  font-size: var(--step-0);
  font-weight: var(--weight-medium);
  color: var(--ink);
  max-width: 44ch;
  margin-inline: auto;
  text-wrap: balance;
}

/* the cue: a hairline that keeps being drawn down, and the client's word */
.hero__cue {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-s);
  margin-inline: auto;
  padding: var(--space-2xs);
  font-size: var(--step--2);
  font-weight: var(--weight-semi);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.hero__cue:hover { color: var(--green); }
.hero__cue-line {
  display: block;
  width: 1px;
  height: clamp(28px, 5vh, 48px);
  background: currentColor;
  transform-origin: 50% 0;
  animation: cue-draw 2.6s var(--ease-in-out) infinite;
}
@keyframes cue-draw {
  0%   { transform: scaleY(0); transform-origin: 50% 0; }
  45%  { transform: scaleY(1); transform-origin: 50% 0; }
  46%  { transform-origin: 50% 100%; }
  90%  { transform: scaleY(0); transform-origin: 50% 100%; }
  100% { transform: scaleY(0); }
}
.js .hero__cue { opacity: 0; animation: cue-in var(--dur-slow) var(--ease-out) 1100ms forwards; }
@keyframes cue-in { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .js .hero__cue { opacity: 1; animation: none; }
  .hero__cue-line { animation: none; }
}


/* =============================================================
   4. (retired) THE THREE WORDS — see css/stage.css
   ============================================================= */


/* =============================================================
   5. THE IDEA
   The client's statement in the serif with its second sentence in
   the italic and in a brand tone; the paragraph. The right column
   is the space the chalk doorway used to fill — left BLANK on
   2026-09-06 on the owner's instruction, reserved.
   ============================================================= */

.idea { position: relative; }
.idea__grid {
  display: grid;
  gap: var(--space-2xl);
  align-items: start;
}
/* AN EVEN SPLIT, AND THE FIGURE CENTRED AGAINST THE COPY (2026-09-17 night,
   the owner: "make it bigger, more centred, more prominent within the page
   section"). It was 6fr/5fr with `align-items: start`, which put the mark in
   the narrower column AND hung it from the top of a row whose height the copy
   sets — so it sat high and to the side, reading as a decoration beside the
   paragraph rather than its other half. An even split costs the copy nothing:
   `.idea__body` caps itself at 40ch, and the narrowest column an even split
   leaves on a desktop is 397px at 900px wide, which is wider than the copy
   used there anyway. `align-items: center` then hangs the mark from the row's
   middle rather than its top. Both are scoped to the two-column layout; below
   900 the grid stacks and neither applies. */
@media (min-width: 900px) {
  .idea__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-3xl); align-items: center; }
}

/* THE IDEA AND 01–04 ARE ONE TABLEAU and are sized to one screen together;
   the ramp that does it lives with `.offer-section` in section 6, because it
   is that section's top padding that carries the whole gap. It also drives
   `.idea__figure`'s `max-width` — see there before changing either. */



.idea__lead {
  font-size: var(--step-4);
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: var(--heading);
  /* Centred like the hero (owner, 2026-09-21). The h2 sits directly inside
     `.idea__grid` — not inside `.idea__text` — so at desktop it spans both
     grid columns via `grid-column: 1 / -1` (see the media block below);
     at mobile the grid stacks single-column and `margin-inline: auto`
     puts the balanced text on the page's axis. `max-width` widened from
     16ch because it now uses the full container. */
  max-width: 30ch;
  margin-inline: auto;
  text-align: center;
}
@media (min-width: 900px) {
  .idea__lead { grid-column: 1 / -1; margin-block-end: var(--space-l); }
}
.idea__em {
  display: block;
  color: var(--teal-display);
}
.idea__body {
  margin-block-start: var(--space-xl);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--ink);
  max-width: 40ch;
}

/* ---------- the figure ----------
   Where the chalk doorway was: the client's own mark, traced from their logo
   (assets/_source/figure/), made of a few continuous THREADS. Twenty-one of them
   each make one journey: a short comet from a ring 300–540 units out, looping
   on the way, that arrives at the mark and keeps going — and what it follows
   there is the mark's own edges. The threads are peeled off the silhouette
   contour by contour (`make-threads.py`): a thread enters on the outer ring of
   a letter's stroke, runs it round, steps to the next ring one pass inside it
   and runs that, folding in on itself until the stroke is solid. The outline
   therefore arrives first and fills out to full weight. The head moves on, the
   tail stays where the thread entered, and the trails are what accumulate:
   when the last one finishes there is no white left inside the outline.
   No fill, no snap. About five seconds, 42 paths, all CSS.

   THEY WERE RANDOM WALKS UNTIL 2026-09-18 (L25), steered toward whatever cell
   was still uncovered. That covers the mark and it reads as exactly what it
   is — the owner: "it's still too sketchy. The lines are just segments. It's
   too chaotic." A greedy coverage walk has nothing to follow, so it scribbles,
   and neither smoothing nor retiming turns a scribble into a line. What fixed
   it was giving the thread something to follow.

   HOW A LINE IS BUILT. Two paths, both normalised to 1000 units (`pathLength`)
   and driven by stroke-dashoffset alone. The APPROACH carries the comet: one
   dash `--c` units long (the comet's length as a share of the path) and a gap
   LONGER than the path, so exactly one dash ever exists — with a gap equal to
   the path, the next dash sat at the path's end at zero length and its round
   cap printed a dot. Sliding the offset from +c to −1000 walks the dash from
   before the start to off the far end. The TRAIL is the thread itself: one
   dash the length of the path, drawn on from the end where the comet arrived
   (offset 1000 → 0), starting the instant the comet's head gets there, and
   clipped to the mark's silhouette — so a hop across a gap in the ink is
   invisible and the edges are the mark's edges. Every delay and duration is
   measured by the generator and written into the markup; nothing is computed
   at run time and no JS runs the animation — `.is-in` starts it, and
   reduced-motion or no script shows the finished mark. */
/* THE BOX IS THE MARK'S OWN FRAME, NOT A SQUARE (2026-09-17 night). The trace
   works in a 400x400 frame with the monogram — which is 400 x 256.67 — centred
   in it, and this box used to be that whole square. So 36% of the box was
   nothing, and the zone below it multiplied that nothing by 1.5: the page
   reserved 215px of white under a mark 202px tall, and the mark paid for it by
   being small. The box is now the mark, so the ratio is the mark's. Every
   percentage on the SVG below is unchanged — they were always percentages of
   THIS box. The ratio and the viewBox are written together by the generator
   and must agree, or the SVG letterboxes inside the box and the mark shrinks
   again by the difference. */
.idea__figure {
  position: relative;
  width: 100%;
  max-width: 560px;
  aspect-ratio: 400 / 256.67;
  margin-inline: auto;
}
/* the base reveal rule would fade it; it draws instead — and keeps its
   parallax drift */
.js .idea__figure[data-reveal] { opacity: 1; transform: translate3d(0, var(--shift, 0px), 0); }
/* THE SAFE ZONE. The SVG's own box is larger than the figure's — 28% of the
   box beyond it on the left, 34% above, 300% to the right, 50% below — and it
   clips, so the lines exist only where the page has no type: the idea copy
   ends 31% left of the box at 1024px (40% at 1440); and above, the stage's
   three words and its hint sit in the released sticky panel, the hint ending
   40% above the box.

   THE LOWER NEIGHBOUR IS NO LONGER A SINGLE NUMBER. This comment used to say
   "the early-access list begins 57% below it at 1280px", measured once at one
   comfortable height. Since 2026-09-06 (L17) the gap under the figure is
   driven by viewport HEIGHT, so that clearance is a range, and the binding
   case is the SHORT viewport where the gap is at its 48px floor. What keeps
   the 50% band clear there is that the figure box rides the same scalar and
   ends higher as the gap shrinks (see §6, `--idea-air`). Measured at thirteen
   sizes: 19–114px of clearance below the band, the tightest at 960x720.
   `zone.js` sweeps those heights and must print `hits: []`.

   So the zone is a band, shallow above and below and open to the right for
   the width of the viewport and beyond: a line from above or below is born
   on the band's edge, a line from the right at the viewport's. The viewBox in
   the markup is these same numbers ×4 (1% of the box is 4 units); the
   generator writes both, and zone.js beside it re-measures the page. The
   reset's `max-width: 100%` on images would shrink it to the box — hence none. */
.idea__figure svg { position: absolute; left: -14%; top: -34%; width: 414%; max-width: none; height: 184%; overflow: clip; }

/* THE MARK'S COLOUR IS THE CLIENT'S, AND IT IS NOT ONE COLOUR (2026-09-17).
   This drew in `--green` until then, which was neither the logo's colour nor
   the page's ink — a green man where the masthead has a black one. What is
   drawn is now the client's MONOGRAM, whose colour is a two-dimensional
   field: the "L" is black along the bottom at the height where the figure's
   leg has gone gold, and the ring is gold across the whole width. No
   gradient gets that, and the ring crosses both letters and the figure, so
   the silhouette is one fused outline with no seam to colour against. So the
   trails are stroked with `#mark-ink`: a pattern, one tile the size of the
   mark's box, holding the client's own colour field as a raster. A thread
   crosses black and gold in one journey and cannot be told a colour;
   the pattern means it does not have to be. Pattern and paths live in the
   generated markup — `assets/_source/figure/` — never here. The comets fly
   outside that box, where a pattern would tile and band them, so each
   carries the flat colour of the place it lands, as `--comet` —
   not `--ink`, which is the page's own text-colour token and would be
   inherited here. */

/* THE CLOCK IS EASED, AND THAT IS THE WHOLE OF THE 2026-09-18 RE-CUT (L24).
   Both phases ran at a CONSTANT SPEED until then, and the owner's verdict was
   "clunky, slow, not fluid" — three names for one fault, and turning the
   speeds up fixes none of them.

   A comet is born 500–860 units from the mark's centre and almost none of
   that is on screen: measured in situ at 1440x900, the SVG's clip reaches
   1357px right of the box where the viewport reaches 181, and a comet from
   above is born ~600px up with the last 98 of it showing. At a constant speed
   TIME IS DISTANCE, so most of every flight was spent where nobody could see
   it — the first 1400ms rendered a blank white box, and what finally appeared
   did so at full speed, out of nowhere, already halfway down.

   So the flight is no longer timed by distance. Every comet takes the same
   ~900ms whatever its approach is long, and inside that it decelerates — hard
   at first, then settling — so the invisible far part is crossed in the first
   quarter. `--fly-ease` is that curve: an exponential approach to a terminal
   velocity, written into the markup as a `linear()` stop list by the
   generator, because no cubic-bezier both starts that steep and then HOLDS a
   constant speed. THE STOP LIST IS THE VELOCITY PROFILE, not an approximation
   of it — `linear()` holds a constant speed between stops — so its times carry
   three decimals and there are 96 of them, each segment shorter than a frame.
   Rounding those times to whole per cent sawtoothed the speed by 25% every 1.7
   frames and was visible as stepping (L26). Holding it is the point — the terminal speed is the trail's
   own drawing speed, so a comet arrives at exactly the pace the ink continues
   at and the handoff has no step in it. The `cubic-bezier` on the line before
   is the fallback for a browser without `linear()`; it is an expo-out, which
   is the same idea coarsely, and the second declaration simply does not parse
   there. The trail then starts at that speed (the bezier's initial slope is
   1.17x its mean, which is where the comet lands) and settles to rest.

   THE NUMBERS ARE IN `assets/_source/figure/build-figure.js`, under THE
   CLOCK, with the arithmetic. Nothing here is computed at run time. */
/* the approach: a hairline comet, parked before the path's start */
.figure__fly {
  fill: none;
  stroke: var(--comet, var(--ink));
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--c) 1100;
  stroke-dashoffset: var(--c);
}
.js .is-in .figure__fly {
  animation: fly var(--dur, 2000ms) cubic-bezier(0.16, 1, 0.3, 1) var(--d, 0ms) forwards,
             horizon var(--fd, 120ms) linear var(--fi, 0ms) both;
}
/* THE `@supports` IS LOAD-BEARING, and a plain fallback declaration would not
   have been. `--fly-ease` holds a `linear()`, and a var() that resolves to
   something the browser cannot parse does not fall back to the declaration
   above it — it is invalid at computed-value time, which for an unregistered
   property means `unset`, and `animation-timing-function` unset is `ease`.
   So a browser without `linear()` would have got neither the curve nor the
   expo-out written above but a slow-start `ease`, which is the one shape this
   whole rewrite exists to avoid. Guarded, it gets the expo-out in the
   shorthand — the same idea, coarsely — and nothing is silently swapped. */
@supports (animation-timing-function: linear(0, 1)) {
  .js .is-in .figure__fly { animation-timing-function: var(--fly-ease, cubic-bezier(0.16, 1, 0.3, 1)), linear; }
}
@keyframes fly { to { stroke-dashoffset: -1000; } }
/* THE BAND'S TOP AND BOTTOM EDGES ARE SOFT. They run through empty white page,
   and a ray from the mark's centre leaves the band through one of them for
   every approach angle past 0.153 rad — so for nearly all of them. The edge is
   98px above the box and 146 below at 1440, and a comet crossing it used to
   MATERIALISE there: nothing, then a hairline at speed, a thumb's width from
   the mark. Each one now fades up as it crosses the outer tenth of the band,
   and is at full strength long before it reaches the ink. `--fi` and `--fd`
   are where that crossing falls in ITS own flight, in ms, solved by the
   generator through the same easing the dashoffset uses.
   A `mask` over the approaches does the same thing in two lines of markup and
   was the first cut. It costs an offscreen buffer the size of the whole band —
   2318 x 661px — recomposited every frame: measured on the real page in real
   Chrome, 49.4% of frames over 33ms and 14.8% over 50, against 32.8% and 6.1%
   with it gone. The path is already repainting for its dashoffset, so fading
   its stroke alongside is free. */
@keyframes horizon { from { stroke-opacity: 0; } }

/* the trail: the line's body, heavy enough that its folds leave no pores */
.figure__trail {
  fill: none;
  stroke: url(#mark-ink);
  stroke-width: 5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1000 1100;
  stroke-dashoffset: 1000;
}
/* AND IT IS LINEAR, WHICH IS BOTH THE RIGHT ANSWER AND THE CHEAP ONE. A pen
   drawing ink moves at one speed, and the flight above settles to a terminal
   velocity, so a constant-speed trail matches the comet's arrival exactly.
   It also costs less than everything else in the 2026-09-18 re-cut put
   together: this line eased — cubic-bezier(0.3, 0.35, 0.25, 1) — measured
   37.0% of frames over 33ms on the real page in real Chrome, against 24.3%
   with `linear`. NOT because of the bezier: cubic-bezier(0.33, 0.33, 0.67,
   0.67), which is straight, measures 24.4%. What costs is the non-uniform
   VELOCITY — a dashoffset that advances by a different amount every frame is
   a different stroke geometry every frame. Price any easing put here. */
.js .is-in .figure__trail { animation: trail var(--dur, 2000ms) linear var(--d, 0ms) forwards; }
@keyframes trail { to { stroke-dashoffset: 0; } }

/* THE RING'S HALO. It is in the client's monogram and it is measured, not
   invented — the ring's own gold at 0.09 coverage at the median, 0.42 at the
   very most. The lines cannot draw it: it lies outside the silhouette they
   are clipped to. So it comes up underneath them over the second half of the
   draw, as the ring it belongs to is laid down. IT IS ON THE RING'S OWN CLOCK
   since 2026-09-18: it starts when the first gold line lands and is full when
   the ink settles. It used to run over the second half of the whole draw,
   which put the glow up while the thing it belongs to did not exist yet. */
.figure__glow { opacity: 0; }
.js .is-in .figure__glow { animation: glow var(--dur, 2000ms) var(--ease-in-out) var(--d, 0ms) forwards; }
@keyframes glow { to { opacity: 1; } }

/* THE MARK THE PAGE RESTS ON IS THE CLIENT'S FILE (2026-09-18), and the lines
   are how it arrives. Three passes tried to reproduce the artwork as vectors
   and the owner could see each one: the tolerance was a sixth of the ring's
   width (L21), the cut threw away the stroke's shoulder (L22), and then —
   with both fixed and every number reading 1.00 — the ring still had hairlines
   through it. Those are not a tracing fault at all. Twenty-one stroked paths
   overlapping inside a clip cannot add up to solid ink: where two anti-aliased
   edges meet the compositor gives 1-(1-a)(1-b), so two halves make three
   quarters, and a seam shows along every join. Coverage never saw it because
   coverage asks whether ink reaches a pixel, not whether it reaches it ONCE.
   And filling the outline instead only exposes the outline's own wobble,
   because a traced edge is hard where the artist's is soft.

   So the lines still draw the mark — that is the whole point of the section —
   and over the final 820ms the page crosses to `imposter-monogram-ink.webp`,
   which IS the file the client sent, its own anti-aliasing included. Exact by
   construction, and there is nothing left to approximate. The halo underneath
   is the same composite, on the ring's own clock. Same frame as the paint and
   the halo (L19), so it lands on the drawing rather than beside it.
   IT STARTS WHEN THE INK IS IN, NOT WHEN THE LAST LINE STOPS (2026-09-18).
   The ring's three walks spend their last third folding back along an ellipse
   that a single pass has already covered, so the picture is complete while
   they are still moving. The generator measures that moment by rendering this
   schedule at a series of times and counting ink, and the crossfade is hung on
   it: on the current run the ink is in at 3943ms of a draw whose last line
   stops at 4083. The lines keep drawing underneath, over ink they are no
   longer adding to, and are covered before they finish. */
/* AND THE LINES GO OUT AFTER IT IS IN — after, not during, and that ordering
   is the whole of it. They do have to go: the silhouette they are clipped to
   is cut at 36% coverage, so it reaches a little past where the artwork's ink
   visibly ends, and a hard edge poking out from under a soft one shows as
   notches wherever the ring crosses a leg.
   CROSSFADING THEM IS WRONG AND LOOKS IT. Two layers at half opacity over
   white give 1-(1-.5)(1-.5) = 0.75, not 1 — so a straight crossfade walks the
   whole mark down to three-quarters ink and back, and at 10,000ms the monogram
   went visibly grey (that figure is from the clock this one replaced). Sequenced instead: the artwork fades IN over the first
   340ms while the lines are still solid underneath (nothing can dip, the two
   are the same picture), then the lines fade OUT over the next 320 with the
   artwork already opaque above them. All that changes in the second half is
   the overhang, and it changes to nothing. */
.figure__art { opacity: 0; }
.js .is-in .figure__art { animation: glow var(--dur, 520ms) var(--ease-in-out) var(--d, 0ms) forwards; }
.js .is-in .figure__trails { animation: unglow var(--dur, 470ms) var(--ease-in-out) var(--d, 0ms) forwards; }
@keyframes unglow { to { opacity: 0; } }

/* no script, or no motion: the finished mark — trails whole, comets gone */
html:not(.js) .figure__fly { stroke-dashoffset: -1000; }
html:not(.js) .figure__trails { opacity: 0; }
html:not(.js) .figure__glow { opacity: 1; }
html:not(.js) .figure__art { opacity: 1; }
/* AND THE TRAILS THEMSELVES ARE STOPPED, not just their group. The group's
   `animation: none` and `opacity: 0` hide the drawing but leave twenty-one
   paths animating `stroke-dashoffset` underneath, invisibly, for four and a
   half seconds — which is both wasted paint and, on a reduce setting, motion
   that was asked not to happen. The singular selector was missing here. */
@media (prefers-reduced-motion: reduce) {
  .figure__fly { stroke-dashoffset: -1000 !important; animation: none !important; }
  .figure__trail { animation: none !important; }
  .figure__trails { opacity: 0 !important; animation: none !important; }
  .figure__glow { opacity: 1 !important; animation: none !important; }
  .figure__art { opacity: 1 !important; animation: none !important; }
}
/* on a phone the paragraph sits directly above the box and the list below it,
   so the box keeps its lines to itself: the zone shrinks to the box */
/* AND IT IS 10% SMALLER THAN THE COLUMN IT SITS IN — owner, 2026-09-19 (late):
   on a phone the mark read "a bit too close to the paragraph above and to
   01–04 below, and a bit too big", and the instruction was to fix it by
   SHRINKING THE MARK, not by pushing its neighbours away. The air is not
   touched: above is `.idea__grid`'s 36px row gap, below is the 32px of
   `.idea` padding-block-end + `.offer-section` padding-block-start, and both
   are padding/gap rather than anything the box's height feeds — so a shorter
   box leaves those two numbers exactly where 2026-09-19 (evening) set them
   and simply asks less of them. `width` AND `max-width` both carry the 0.9,
   because which of the two binds depends on the width: on a phone the box is
   column-bound (335px of a 375px viewport) and the cap is inert, while from
   about 440px up the cap is what decides. 380 → 342 keeps the same step on
   the wide half of the stacked range. */
@media (max-width: 899px) {
  .idea__figure { width: 90%; max-width: 342px; overflow: clip; }
}


/* =============================================================
   6. EARLY ACCESS TO — the four items from the client's sentence
   Editorial: a hairline over each, a tracked ordinal, the title in
   the serif. The brand hues on the ordinals only.
   ============================================================= */

.offer-section { padding-block-start: var(--space-2xl); }

/* ---------- ONE SCREEN, ON THE OWNER'S NOTE (2026-09-06 evening) ----------
   The statement, the mark, the paragraph and 01–04 are one tableau and have
   to be READABLE WITHOUT SCROLLING. They were not: at 1440x900 the ink ran
   from the statement's first line to the last of 01–04 over 874px against
   900 of viewport, and every shorter screen was worse — 1440x800 was 74px
   over, 1366x768 95px over. The reader had to scroll, and scrolling is what
   broke the tableau.

   WHAT ACTUALLY HAS TO FIT is the INK SPAN — the statement's first line to
   the foot of 01–04 — not the two sections' boxes. The 221.6px between them
   at 1440 was `.idea`'s own `padding-block-end` (--space-3xl) plus
   `.offer-section`'s `padding-block-start` (--space-2xl), and it is the only
   thing in that span that is air rather than type. So the whole gap is
   collected into ONE declaration on `.offer-section` and driven by viewport
   HEIGHT, in the same shape as the hero's `--rise` ramp below:
   clamp(short, ramp, tall), where `tall` is exactly the 221.6px the page
   already had, so above a certain viewport height the rendering is
   bit-identical to the one the owner approved. (That height was 922px in the
   first cut; it is 1036px now — see THE BUDGET RESERVES THE CLOSER'S ARRIVAL,
   below, which is the amendment this paragraph should be read through.)

   `100vh - 700px` WAS that ramp, and 700 was measured, not chosen: the ink
   span minus this gap is 652px at 1440 and 659px at 1024 (where the 01–04
   titles wrap to two lines and the block is at its tallest), and 659 + 40px
   of breathing room at top and bottom is 699. The 652 survives the amendment
   and is still the type figure the budget is built on; the 40px of breathing
   room does not — it was slack at the last LINE, and the reader never stops
   there. The 48px floor holds the span at ~700px, and the height the tableau
   fits down to is ~768px, not ~700px. Under the floor it does not fit and the
   page simply scrolls; the ink alone is 625px, so no amount of air removal
   saves a 640px viewport.

   THE MARK RIDES THE SAME SCALAR, and that is load-bearing rather than
   cosmetic. The figure's SVG is a band 50% of the box deep BELOW the box
   (`ZONE.D` in assets/_source/figure/build-figure.js) inside which its lines
   are drawn; shrink the gap under the figure and 01–04 walks into that band
   and the lines cross type. Because the row's height is set by the text
   column, not the figure, a shorter figure box ends higher and hands the
   band back the room the gap gave up, and then some.

   RE-SOLVED 2026-09-17 NIGHT, when the box stopped being a square and became
   the mark's own frame (see `.idea__figure`): the box's HEIGHT is now 0.642
   of its width rather than equal to it, so the same `ZONE.D` reserves far
   less page, and the same band affords a much wider mark. The coefficients
   are solved against the band, not chosen, and the two ends are solved
   against different things:

     · the 360px FLOOR is band-bound. At the gap's 48px floor it puts the box
       at 410, and the tightest clearance in zone.js's list is then 21px, at
       1024x768 and 1024x720. Measured by forcing the box wider at 1152x720,
       the binding width: 410 leaves 27px, 425 leaves 17, 440 leaves 7, and at
       455 the label is inside the band.
     · the 560px CEILING is inert on any real desktop, and is a guard rather
       than a number: the wrap caps the grid at 1240, so an even split never
       offers the figure more than 484–485px, and from about 1440 wide the
       COLUMN decides, not this clamp. At 1024 the column decides too (409).
       The clamp only bites in between — 452 of a 485 column at 1440x900.

   Measured, not asserted, and at short viewports specifically:
   `node assets/_source/figure/zone.js` must print `hits: []` at every size in
   its list; it does at all twenty, with 21–192px below the band and 140–187
   above. The gap is smaller at every height under 1036, which is why zone.js
   also sweeps NARROW screens at middle heights.

   THE BUDGET RESERVES THE CLOSER'S ARRIVAL — 2026-09-06 (late), the owner's
   second note on this block: *"if I hover around the very beginning of the
   green card, the section top is seriously cropped out."* It was, and the
   first cut of this rule is why. **A reader does not stop scrolling when the
   last line is on screen; they stop when the next thing appears.** What
   appears is `.closer`, the green card, and it sits `--space-2xl` (80px)
   below the foot of 01–04. So the scroll position the reader actually lands
   on is not `span ≤ 100vh`, it is `span + 80 ≤ 100vh`, and against THAT the
   old ramp was over at almost every size: measured, span / tail / slack at
   the moment the card's top edge touches the viewport's bottom —

     1920x1080  877 / 80 / +123      1440x900   852 / 80 / −32
     1920x960   877 / 80 /   +3      1440x800   752 / 80 / −32
     1920x920   873 / 80 /  −33      1440x720   700 / 80 / −60
     1512x945   877 / 80 /  −12      1366x768   714 / 80 / −26
                                     1024x768   727 / 80 / −39

   Every negative is type cut off the TOP of the screen — the statement's
   first line, exactly what the owner saw. (At 1920x920, the owner's kind of
   viewport, the card touching the bottom put the first line 32px above the
   top; one more nudge — 40px of green showing, which is what their screenshot
   had — put it 72px above. Both measured on the page.)

   SO THE BUDGET IS NOW WRITTEN AT THE CARD, NOT AT THE LAST LINE, and it
   reserves two things the old constant did not:

     100vh − 652px − var(--space-2xl) − var(--tableau-tail)
              type      headroom          the boundary

   652px is the same measured type height as before. The two 80s are the
   answer to "how much headroom": ONE `--space-2xl`, which is exactly the
   boundary's own depth — so the tableau stays whole until a full band of
   green as deep as the gap that separates it from 01–04 is showing, and the
   reader has to have committed to the next section before anything is cut.
   That is a derivation, not a preference; measured after, it holds a constant
   79–80px at every viewport 828px tall and up.

   THE TAIL IS THE SECOND THING TO GO, NEVER THE FIRST. It has its own clamp,
   80px down to 48px, on a ramp that only starts where the internal gap has
   already reached ITS floor (780 = 652 + 80 + 48). So above 860px of height
   the section boundary is untouched at its full 80px and the tableau's
   INTERNAL air is what gives way; below it, and only there, the boundary
   tightens too, and stops at 48px rather than closing up against the green.
   The order is deliberate: air inside a composition is composition, air
   between two sections is structure, and structure yields last.

   THE COST IS THE ANCHOR, and it is stated rather than hidden. The
   bit-identical band moves from ≥922px of viewport height to ≥1036px: between
   those two the gap is up to 132px tighter, and the mark up to 96px smaller,
   than the rendering the owner approved. That IS the fix — the owner's own
   screen is in that band, and 1920x1080 (where it was approved) is not.

   Measured after, span / tail / slack at the card, three engines' worth of
   sizes on one: 1920x1080 877/80/+123 (unchanged); 1920x960 801/80/+79;
   1920x920 761/80/+79; 1512x945 786/80/+79; 1440x900 740/80/+80; 1440x800
   700/48/+52; 1366x768 694/48/+26; 1024x768 707/48/+13; 960x900 740/80/+80.
   Only 720-tall viewports are still over, by 28px, and that is the floor
   arithmetic rather than a missed saving: 652px of type plus the two 48px
   floors is 748, and nothing but cutting type gets that under 720.

   Scoped to the two-column layout. Below 900px the grid stacks, the ink span
   is 1300–1600px, and no viewport of any phone can hold it — that is
   geometry, not spacing, and the phone's rhythm is left exactly as it was. */
@media (min-width: 900px) {
  :root {
    --tableau-tail: clamp(48px, calc(100vh - 780px), var(--space-2xl));
    --idea-air: clamp(48px, calc(100vh - 652px - var(--space-2xl) - var(--tableau-tail)), calc(var(--space-3xl) + var(--space-2xl)));
  }
  .idea { padding-block-end: 0; }
  .offer-section { padding-block-start: var(--idea-air); padding-block-end: var(--tableau-tail); }
  .idea__figure { max-width: clamp(360px, calc(360px + 1.05 * var(--idea-air)), 560px); }
  /* 01-04 TAKES ITS COLUMN COUNT FROM THIS BREAKPOINT, not the grid
     utility's generic 1024. Between 900 and 1023 the utility left the
     four items two-across, which is two rows: 346px of block where four
     across is 220, and the tableau could not fit any viewport in that
     band — 960x900 was 68px over with the gap already at its floor.
     Four-across is also what the band above it does, so this removes a
     seam rather than adding one; measured at 960, `Practical tools`
     wraps to two lines exactly as it already does at 1024. */
  .offer { --cols: var(--cols-lg); }
}

.label + * { margin-block-start: var(--space-l); }

.offer { --grid-gap: var(--space-xl); --cols-sm: 2; }

/* SCALE, ON THE OWNER'S NOTE (2026-09-06). The section was reading as fine
   print under the stage. Taken up in one pass and then HALVED BACK the same
   day — doubling the eyebrow, the ordinals and the rule and putting the titles
   at 1.6× ballooned the section past the stage it sits under. The multipliers
   are the midpoint of that pass and the original: 1.5× on the eyebrow, the
   ordinals and the rule, 1.3× on the titles.
   The eyebrow and the rule are only ever used here — `.label` and `.rule-in`
   have no other instance on the page — so the size lives on the section, not
   on the shared class. */
.offer-section .label { font-size: calc(var(--step--2) * 1.5); }
.offer .rule-in { height: 1.5px; }

.offer__item {
  position: relative;
  padding-top: var(--space-m);
}
.offer__num {
  display: block;
  font-variant-numeric: tabular-nums;
  font-size: calc(var(--step--2) * 1.5);
  font-weight: var(--weight-semi);
  letter-spacing: var(--ls-caps);
  color: var(--coral-ink);
}
.offer__item:nth-child(2) .offer__num { color: var(--gold-ink); }
.offer__item:nth-child(3) .offer__num { color: var(--teal-ink); }
.offer__item:nth-child(4) .offer__num { color: var(--coral-ink); }

.offer__title {
  margin-top: var(--space-s);
  font-size: calc(var(--step-2) * 1.3);
  line-height: var(--lh-tight);
  color: var(--heading);
  max-width: none;
}


/* =============================================================
   6b. THE THREAD
   A single hairline down the left margin, from the foot of the
   hero to the top of the closer, whose length is bound to scroll:
   the line is ruled as the page is read, and while the stage is
   pinned it goes on being ruled beside the words. The chalk spine
   this replaces was 5px and hand-drawn; this is 1px and straight.
   js/site.js sizes it and writes --offset.
   ============================================================= */

main { position: relative; }
.spine {
  display: none;
  position: absolute;
  top: 100svh;
  /* 50%, NOT 50vw — see design-rules L16. `vw` counts the classic scrollbar
     and the layout does not, so `(100vw - --container) / 2` put the thread
     where the container's edge would be on a browser with NO scrollbar. On
     Chrome/Edge/Opera on Windows (15px track) the declared 2.75rem offset was
     delivered as 2.28rem — 36.5px instead of 44px at 1440 — while Safari,
     Chrome on macOS and every phone got the full 44px. Same page, two
     positions. `50%` resolves against `main`, whose width IS the layout
     width, so the offset is 2.75rem on every engine and every platform. */
  left: max(12px, calc(50% - var(--container) / 2 - 2.75rem));
  width: 2px;
  height: 0;            /* set by site.js: down to the closer */
  z-index: 0;
  pointer-events: none;
  color: var(--teal-ink);
}
.js .spine { display: block; }
.spine path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  stroke-dasharray: var(--len, 1000);
  stroke-dashoffset: var(--offset, 1000);
}
@media (max-width: 1099px) { .js .spine, .spine { display: none; } }
@media (prefers-reduced-motion: reduce) { .js .spine, .spine { display: none; } }


/* =============================================================
   7. THE CLOSER — the page's one capture, on the anchor colour
   The client's closing line in gold, the three fields, the gold
   button. Nothing else.
   ============================================================= */

.closer {
  position: relative;
  background: var(--surface-invert);
  overflow: clip;
  isolation: isolate;
  /* ITS OWN PADDING, NOT `.section`'s. Owner's note, 2026-09-06: "way too
     tall, lots of wasted space". They were right, and the cause is that
     `.section` pays `var(--space-3xl)` — `clamp(4.5rem, 3rem + 6.5vw, 9rem)`,
     a WIDTH-driven value spent on VERTICAL padding. At 1440 that resolved to
     141.6px top AND bottom, so the band stood 776px tall to hold 497px of
     content: 36% of the one block the page exists for was empty green, and
     the wider the monitor the taller it got.

     ASYMMETRIC ON PURPOSE. The top carries a two-line heading and has to let
     it arrive; the bottom is a floor with nothing beneath it, so the room
     there was doing no work at all — it read as unfinished rather than
     generous. The vw term is kept, because a wider screen may still breathe,
     but at roughly half the old coefficient and under a much lower cap. */
  padding-block: clamp(3.25rem, 2rem + 3.5vw, 5.5rem)
                 clamp(2.25rem, 1.25rem + 2.75vw, 4rem);
}
.closer .wrap { position: relative; z-index: 1; }

.closer__title {
  font-size: var(--step-4);
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--heading-invert);
  max-width: 16ch;
}


/* =============================================================
   8. THE SIGN-UP
   ============================================================= */

.signup { width: 100%; max-width: 560px; margin-inline: auto; }

.signup__form { display: flex; flex-direction: column; gap: var(--space-xs); }

.signup__field { width: 100%; min-width: 0; }
.signup__field input {
  width: 100%;
  min-width: 0;
  padding: 1.05em 1.35em;
  background: var(--film-invert);
  border: 1px solid var(--line-invert-field);
  border-radius: var(--radius-s);
  font-family: var(--font-body);
  font-size: var(--step--1);
  letter-spacing: 0.02em;
  color: var(--white);
  text-align: left;
  transition:
    border-color var(--dur-base) var(--ease-out),
    background-color var(--dur-base) var(--ease-out);
}
.signup__field input:focus { outline: none; }
/* the placeholder is the only visible label a field has: the client's
   string, unchanged, set small and tracked */
.signup__field input::placeholder {
  color: var(--placeholder-invert);
  opacity: 1;
  font-size: var(--step--2);
  font-weight: var(--weight-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
}
.signup__field:focus-within input {
  background: var(--film-invert-lit);
  border-color: var(--gold);
}
/* the email row is a field like the other two; `display: contents` means the
   field, the consent line and the button stack in the form's own rhythm as
   though this wrapper were not here */
.signup__row { display: contents; }

/* ---------- consent (UK GDPR) ----------------------------------
   A checkbox and one sentence, and the sentence is the CLIENT'S OWN — the
   wording their live homepage sign-up already uses, verbatim.

   L13 discipline: no decoration and nothing drawn. The control is the
   browser's own checkbox with `accent-color` set to the brand gold, which is
   why there is no custom box, no SVG tick and no pseudo-element here — a
   native control also keeps the keyboard, the forced-colors mode and the
   platform's own touch target for free.

   It is quiet by SIZE AND WEIGHT, not by colour. The guide is white on dark,
   never a tint (design-rules L1), and this is the one line on the card a
   visitor is being asked to agree to, so it holds full `--ink-invert`. */
.consent {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  margin-top: var(--space-2xs);
  max-width: none;                 /* it is a <p>; --measure would cap it */
  font-size: var(--step--1);
  font-weight: var(--weight-body);
  line-height: 1.45;
  text-align: left;                /* the closer is .center */
  color: var(--ink-invert);
}
.consent__box {
  flex: none;
  width: 1.05em;
  height: 1.05em;
  margin: 0.16em 0 0;              /* optical: sit on the first line's cap height */
  accent-color: var(--gold);
  cursor: pointer;
}
.consent__text { cursor: pointer; }

/* The two documents the client's sentence names. They carry no href until the
   client supplies the addresses (index.html, CONFIGURATION POINT — PRIVACY),
   and an <a> with no href must not dress itself as a link that goes nowhere —
   `.on-invert a` would otherwise colour it teal. Add the href and it becomes a
   link with no other change. */
.consent__link {
  color: var(--teal);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
.consent__link:not([href]) { color: inherit; text-decoration: none; }

/* ---------- the honeypot ---------------------------------------
   Mailchimp's own `b_<account>_<audience>` field. OFF SCREEN, NEVER
   `display: none`: a field that is not rendered at all is a field a
   well-built bot knows to skip, and a screen reader must not meet it either
   (the markup carries aria-hidden and tabindex="-1" for that half). */
.hp {
  position: absolute;
  left: -5000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.signup__error {
  display: none;
  margin-top: var(--space-2xs);
  max-width: none;
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  color: var(--coral);
  text-align: center;
}
.signup.has-error .signup__error { display: block; }
/* an empty field is a failure, and the field it names wears the flag */
.signup.has-error input:placeholder-shown { border-color: var(--coral); }
/* the consent box has no placeholder, so the rule above cannot reach it, and
   an unticked box is always a blocker at submit time whatever else failed */
.signup.has-error .consent__box:not(:checked) {
  outline: 2px solid var(--coral);
  outline-offset: 2px;
}
.signup.has-error .signup__form { animation: nope 380ms var(--ease-out); }
@keyframes nope {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-7px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(2px); }
}


/* =============================================================
   9. BUTTONS
   Square-cornered, tracked, uppercase. Gold for the landing
   page's action, teal for the confirmation page's — the guide's
   own button rule.
   ============================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-s);
  padding: 1.1em 2em;
  border: 0;
  border-radius: var(--radius-s);
  font-family: var(--font-body);
  font-size: var(--step--2);
  font-weight: var(--weight-semi);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* flat. The ledge shadow the buttons used to carry was a toy detail. */
.btn--primary,
.btn--gold {
  background: var(--gold);
  color: var(--green-deep);
}
.btn--primary:hover, .btn--gold:hover { background: var(--gold-lit); color: var(--green-deep); }
.btn--primary:active, .btn--gold:active { transform: translateY(1px); }

.btn--hero { padding-inline: var(--space-2xl); }
.btn--block { width: 100%; }

.btn--teal {
  background: var(--teal);
  color: var(--green-deep);
}
.btn--teal:hover { background: var(--teal-lit); color: var(--green-deep); }
.btn--teal:active { transform: translateY(1px); }

.btn--ghost {
  background: none;
  color: var(--ink-muted);
  padding-inline: var(--space-m);
}
.btn--ghost:hover { color: var(--green); }
.on-invert .btn--ghost { color: var(--ink-invert); }   /* no muted grade on dark */

/* busy reads as held-down: same ink, same ground, no fade (Colour rule) */
.btn[aria-busy="true"] {
  pointer-events: none;
  cursor: progress;
  transform: translateY(1px);
}


/* =============================================================
   10. FOOTER
   ============================================================= */

.foot {
  padding-block: var(--space-xl);
  border-top: 1px solid var(--line);
}
.foot__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
}
/* the ink centroid of the logo sits below its box centre; a share of its
   own height corrects it (measured 3.4px at 2×, design-rules Layout) */
.foot img { width: 160px; translate: 0 -4%; }
.foot small { color: var(--ink-muted); letter-spacing: 0.02em; }
/* the copyright breaks after "Play Lab." on the phone so the two clauses read
   as a symmetric two-line block beside the mark rather than a lopsided wrap
   (owner, 2026-09-19 evening). On desktop it fits on one line, so the break
   is display: none there — the <br> stops taking a line and the sentence
   flows through it. Kept scoped to the class rather than every <br>. */
.foot__cr-break { display: none; }
/* There used to be a `.js .foot [data-reveal] { --reveal-y: 20px }` rule
   here — a shorter rise for the mark and the copyright. Removed
   2026-09-06 with the reveal attribute itself: the foot is the page's
   ground and does not arrive. See index.html above the <footer>. */


/* =============================================================
   11. WELCOME PAGE (page 2)
   ============================================================= */

.welcome { min-height: 100svh; display: flex; flex-direction: column; }
.welcome__body { position: relative; flex: 1; display: flex; align-items: center; padding-block: var(--space-l); }

/* the two end states are short; anchored nearer optical centre than dead
   centre, or the wall becomes the subject of the screen */
.welcome__body:has([data-state="signed-up"].is-active),
.welcome__body:has([data-state="shared"].is-active) {
  align-items: flex-start;
  padding-block-start: clamp(var(--space-l), 11vh, var(--space-3xl));
}

.tick {
  width: 44px; height: 44px;
  margin-inline: auto;
  color: var(--green);
}
.tick circle { fill: none; stroke: var(--teal); stroke-width: 1.5; }
.tick path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

.welcome h1 { font-size: var(--step-5); letter-spacing: -0.02em; }
.welcome__em { color: var(--lilac-display); }

.story { width: 100%; }
.story__q {
  display: block;
  font-family: var(--font-display);
  font-variation-settings: var(--wonk-display);
  font-size: var(--step-1);
  font-weight: var(--weight-display);
  line-height: var(--lh-tight);
  color: var(--green);
  max-width: 42ch;
  margin-inline: auto;
  text-wrap: balance;
}
.story > p { font-size: var(--step-0); }
.story__form { display: flex; flex-direction: column; }
.story__q { margin-block-end: var(--space-s); }
.story__actions { margin-block-start: var(--space-m); }
.story__actions > .btn--ghost:last-child { margin-inline-end: calc(-1 * var(--space-m)); }

.story__field {
  width: 100%;
  height: clamp(72px, 17vh, 200px);
  padding: var(--space-m);
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  font-size: var(--step-0);
  line-height: var(--lh-body);
  resize: vertical;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.story__field:focus { outline: none; border-color: var(--green); box-shadow: var(--shadow-2); }
.story__field::placeholder { color: var(--ink-muted); opacity: 1; }

[data-state] { display: none; }
[data-state].is-active { display: block; }


/* =============================================================
   12. BREAKPOINTS — the first screen has to hold at every size
   ============================================================= */

@media (max-width: 719px) {
  .masthead { padding-block-start: var(--space-m); }
  .masthead__logo img { --logo-w: clamp(140px, 40vw, 220px); }
  .hero__body { padding-block: var(--space-s); }
  .hero__title { font-size: var(--step-4); }
  .hero__join { margin-block-start: var(--space-l); }
  .idea__lead { max-width: 18ch; }

  /* THE 01–04 GRID IS A COMPACT 2×2 ON MOBILE, NOT A TALL STACK. Owner,
     2026-09-18, on the mobile pass: on a phone the four items — which only
     repeat the hero's own "early access to experiences, research, events and
     practical tools" — were a whole screen of scroll in one column. Two
     columns halves that. `--cols: 2` because below 640px `.grid` is otherwise
     1 column (layout.css); it does not touch the ≥640 (2) or ≥1024 (4) steps.
     TYPE UP, VERTICAL SPACE DOWN — owner, 2026-09-19 (evening): "01/02/03/04
     and the subheader are too small, and there is too much space above (the
     IPL mark to EARLY ACCESS TO) and below (03/04 to the green card)." Type
     bumped one step from L30's mobile halving — labels `--step--1`
     (0.89rem), titles `--step-1` (1.28rem). Vertical space cut three ways:
     (1) `.idea` bottom padding var(--space-3xl) → var(--space-2xs), because
     `.idea__figure` already carries its own bottom bleed and the section's
     default 3xl was doubling it; (2) `.offer-section` top padding down from
     `--space-xl` to var(--space-2xs) since the visible gap is now the sum
     of both; (3) `.offer-section` bottom padding down from `.section--tight`'s
     `--space-2xl` to var(--space-m) so 03/04 does not float miles above the
     green closer. */
  .offer { --cols: 2; --grid-gap: var(--space-s) var(--space-l); }
  /* THE MARK IS CENTRED IN ITS AIR. Owner, 2026-09-19 (evening): the mark
     was floating closer to EARLY ACCESS TO than to the end of the paragraph
     above it. Above-figure space comes from `.idea__grid { gap }` (default
     `--space-2xl`, 5rem); below-figure space is `.idea` padding-block-end
     + `.offer-section` padding-block-start. Both sides brought to ~2.25rem
     each so the mark sits in equal air. */
  .idea__grid { gap: var(--space-l); }
  .idea { padding-block-end: var(--space-2xs); }
  .offer-section { padding-block-start: var(--space-m); padding-block-end: var(--space-l); }
  .offer-section .label { font-size: var(--step--1); }
  .offer__item { padding-top: var(--space-s); }
  .offer__num { font-size: var(--step--1); }
  .offer__title { margin-top: var(--space-2xs); font-size: var(--step-1); }

  /* FOOTER, LEFT / RIGHT — owner, 2026-09-19 (evening): "the logo at the
     centre with the copyright below did not look nice, I want the desktop
     version — logo on the left, copyright on the right." So the mobile
     stacking column that stood here from 2026-09-18 (a ~112px centred mark
     with the copyright under it) is out, and the desktop's own space-between
     split is back — but with the mark small enough that the copyright fits
     beside it. The copyright wraps to two lines on the narrower phones and
     that is expected (owner's suggestion, same message): it now sits on
     the right, right-aligned so its break points read as one block rather
     than as two lines adrift. Mark 88px so a 375px viewport with 20px of
     gutter each side (`.wrap`) leaves ~247px for the copyright's ~250px of
     text at `--step--2` — one row on 375, two on 320. */
  .foot { padding-block: var(--space-l); }
  .foot__inner { flex-wrap: nowrap; gap: var(--space-s); }
  .foot img { width: 104px; flex: 0 0 auto; }
  .foot small { text-align: right; flex: 0 1 auto; min-width: 0; }
  .foot__cr-break { display: inline; }
}

/* ---------- short desktops — ONE RAMP, NOT THREE DESIGNS ----------
   Replaces the two `max-height` steps that used to sit here (780px and 660px)
   with height-fluid interpolation. Rewritten 2026-09-06; design-rules L16.

   WHY. The steps made the first screen four fixed designs selected by
   viewport height. Inside a band nothing moved; at a band edge everything
   moved at once — at 1440 wide, one pixel either side of 780, the logo went
   260 -> 187.2px (-28%), the masthead's top padding 50.8 -> 24px and the
   headline 65.30 -> 50.96px (-22%). Two browsers on one monitor land in
   different bands whenever their furniture differs by the couple of pixels
   straddling a boundary; that is the reported "Chrome vs Opera" defect.
   Moving the threshold could not fix it — measured, the 780 step fired 102px
   EARLY with the webfont loaded and 65px LATE without it, so no single number
   was right, and any number leaves a cliff. Continuity is the class fix.

   THE SHAPE. One height-driven scalar, `--rise`, and all five properties
   expressed as `clamp(short, short + k * --rise, tall)`. They therefore ride
   one ramp and cannot drift apart, which five hand-solved clamps could not
   guarantee. `--rise` is a LENGTH, not the unitless 0..1 factor that would
   read more naturally, because a unitless factor needs either length/length
   division or `tan(atan2())` and FIREFOX 141 SUPPORTS NEITHER — it drops the
   whole declaration, which measured as the property falling back to `auto`.
   `number * length` is the one form all three engines compute identically.

   THE ANCHORS. `tall` is, in every case, the expression the page already
   used above 780 — untouched, so at 780px of viewport height and above the
   rendering is exactly the one the owner approved, including the owner's own
   ~800px viewport. Each `k` is the property's largest tall-minus-short gap
   over ALL widths from 720 up, divided by the 220px span, so the ramp reaches
   its tall value at or before 780 at every width and the clamp holds it
   there — a k fitted at 1440 alone left the logo 7.5px short of its approved
   size at 1368 wide. Below 560 the ramp is flat at the short values, which
   are the ones the old 660 step used, so 541-560 renders exactly as before.

   `vh`, not `svh`, on purpose: the steps this replaces keyed on `max-height`,
   which is the layout viewport, and the tall expressions it interpolates from
   are already written in `vh`. Matching them is what keeps the tall end
   bit-identical. */
@media (min-width: 720px) and (min-height: 541px) {
  :root {
    /* THE ONE SCALAR: 0 at 560px of viewport height, then 1:1 with it. */
    --rise: calc(100vh - 560px);
  }

  /* SCOPED TO `.hero`, and that scoping is load-bearing. The steps this
     replaces were written as bare `.masthead` / `.masthead__logo img`, so
     they also styled the CONFIRMATION PAGE's masthead, which is a different
     composition with its own `max-height` steps below. Left unscoped, the
     ramp made that page's mark and its top padding grow below 781 and pushed
     its share state off the fold — measured, the overflow band widened from
     {560, 600} to {560, 600, 640, 680, 700} and 1440x600 went from -5.5px to
     -22.6px. The confirmation page keeps its old stepped behaviour verbatim,
     restored under `.welcome` after this block.

     `min-height: 541px` on this query is the other half of that lesson. The
     landscape rule below re-lays the hero into two columns at 540 and under,
     and it is written with bare `.masthead` selectors — one class. Scoping
     the ramp to `.hero .masthead` made it TWO classes, so it started winning
     against a rule that had beaten it on source order the day before, and
     the landscape masthead went from 4px of padding and a 99px mark to 24px
     and 120px. Specificity, not order, is what a scope changes. Bounding the
     query to the range the stacked hero actually owns is the fix. */

  /* k = 26.70 / 220 — the gap to 6.5vh at the 780 anchor */
  .hero .masthead {
    padding-block-start: clamp(
      var(--space-m),
      calc(var(--space-m) + 0.12136 * var(--rise)),
      clamp(var(--space-m), 6.5vh, var(--space-3xl)));
  }
  /* k = 0.02 exactly, NOT the 4/220 the anchors imply: the tall value here is
     `2vh`, and a shallower ramp would still be under it above 780 and would
     quietly take over from the approved value (-1.45px at 1800 tall). The
     ramp must out-climb its own ceiling. */
  .hero .masthead__inner {
    gap: clamp(
      var(--space-xs),
      calc(var(--space-xs) + 0.02 * var(--rise)),
      clamp(var(--space-s), 2vh, var(--space-l)));
  }
  /* k = 109.47 / 220, the worst gap being at 1368 wide, not at 1440 */
  .hero .masthead__logo img {
    --logo-w: clamp(
      clamp(120px, 11vw, 158px),
      calc(clamp(120px, 11vw, 158px) + 0.4976 * var(--rise)),
      clamp(150px, 19vw, 260px));
  }
  /* k = 25.536 / 220 — step-5 minus step-3, at its widest, which is 1440 */
  .hero__title {
    font-size: clamp(
      var(--step-3),
      calc(var(--step-3) + 0.11607 * var(--rise)),
      var(--step-5));
  }
  /* k = 24.36 / 220 — the gap to 6.2vh at the 780 anchor */
  .hero__join {
    margin-block-start: clamp(
      var(--space-m),
      calc(var(--space-m) + 0.11073 * var(--rise)),
      clamp(var(--space-l), 6.2vh, var(--space-2xl)));
  }
}

/* ---------- WHERE THE FIRST SCREEN CANNOT HOLD THE LARGER MARK ----------
   Added 2026-09-18 with `--logo-scale`. The 30% is delivered wherever the
   fold can carry it, which — measured, 511 viewports — is every phone, every
   tablet, and every desktop or laptop window 900px wide with 546px of height
   or more. These two rules are about the two places it cannot, and each one
   TAPERS rather than steps, so no viewport is a cliff edge for the next.

   NEITHER SHORTFALL IS THE MARK'S DOING; both are the hero already spending
   every pixel it has.

   HOW THE CEILINGS WERE FITTED. Not by arithmetic off the anchors — by
   bisecting `--logo-scale` against the real render at each viewport, the
   hero's own height against `innerHeight`, and reading off the largest EXTRA
   width in pixels the fold still allows. Each ramp below is then fitted
   under the WORST COLUMN of that measurement, not a representative one. The
   `min()` against the asked-for width is what stops a ramp overshooting 1.3
   once there is room; without it the ramps keep climbing and the mark grows
   past what was asked for.

   1. 720–899px WIDE. Between 900 and 880px of width THE HEADLINE RE-WRAPS
      FROM TWO LINES TO FOUR — measured at 640px tall, 96.5px of headline
      becomes 188.6px at the same 43.7px font size. That 92px comes out of
      the hero's own budget, so the slack under `100svh` collapses from 109px
      to 17px, and the mark's extra 30% needs 28px of height. There are 17.
      Nothing here can conjure the other 11, and pushing the button or the
      scroll cue past the fold is the one regression this page may not make
      (design-rules L5, L12 — the first screen carries the route to the
      sign-up).
      The worst column is 899px wide, and its allowance is CONVEX, not
      linear: 0.1px of extra at 660px tall, 5.5 at 680, 17.9 at 720, 35.6 at
      740, and the whole 51.2 by 760. A line drawn from 660 to the top of
      that overshoots at 680 — fitted from the bottom it is too steep early,
      fitted from the top it never arrives. So the ramp starts LATE and
      climbs fast: nothing below 710px of height, the full extra by 770, and
      under the measurement at every row between — 8.5px against an allowed
      17.9 at 720, 25.6 against 35.6 at 740, 42.7 against 51.2 at 760.
      Below 710px tall the mark is simply the size it was; this band has no
      room to give and saying so is better than shipping a broken fold.
      THE WIDTH EDGE AT 900 IS A STEP AND IS MEANT TO BE. Everywhere else
      this file replaced steps with ramps (L16), because a step in the middle
      of the common range makes two browsers on one monitor render
      differently. This one sits exactly on the headline's own two-line /
      four-line boundary, a 92px jump in the same direction — a
      discontinuity already there, and four times the size of this one.

   2. 900px WIDE AND UP, AT 541–545px OF HEIGHT. A four-pixel sliver of
      viewport height, one pixel above the landscape rule that re-lays the
      hero into two columns at 540. The `--rise` ramp is already at its floor
      there and the measured allowance falls from 47.4px of extra at 545 to
      42.7 at 541. Without this the mark overflowed the fold by 3.3px at
      1440–1920 wide — small, and a regression all the same, and the kind
      that is only ever found by sweeping the sizes that FAIL rather than the
      ones that pass. Above 545 the ramp is past the full 30% and the `min()`
      holds it there, so this rule does nothing at any ordinary height. */
@media (min-width: 720px) and (max-width: 899px) and (min-height: 541px) {
  .hero .masthead__logo img {
    width: min(
      calc(var(--logo-w) * var(--logo-scale, 1)),
      calc(var(--logo-w) + clamp(0px, calc(0.853 * 100vh - 605.6px), 60px)));
  }
}
@media (min-width: 900px) and (min-height: 541px) {
  .hero .masthead__logo img {
    width: min(
      calc(var(--logo-w) * var(--logo-scale, 1)),
      calc(var(--logo-w) + clamp(0px, calc(1.6 * 100vh - 824.6px), 90px)));
  }
}

/* The confirmation page's masthead keeps the STEPS, verbatim, because its
   composition was tuned against them and it is not what this pass measured.
   These are the same three declarations the block above replaced for the
   hero — nothing here is new, it is the old rule given the narrower scope it
   should always have had. */
@media (min-width: 720px) and (max-height: 780px) and (min-height: 541px) {
  .welcome .masthead { padding-block-start: var(--space-m); }
  .welcome .masthead__inner { gap: var(--space-xs); }
  .welcome .masthead__logo img { --logo-w: clamp(130px, 13vw, 190px); }
}
@media (min-width: 720px) and (max-height: 660px) and (min-height: 541px) {
  .welcome .masthead__logo img { --logo-w: clamp(120px, 11vw, 158px); }
}

/* short phones */
@media (max-width: 719px) and (max-height: 700px) {
  .masthead { padding-block-start: var(--space-s); }
  .masthead__inner { gap: var(--space-2xs); }
  .masthead__logo img { --logo-w: clamp(120px, 34vw, 176px); }
  .hero__title { font-size: var(--step-3); }
  .hero__join { margin-block-start: var(--space-m); }
  .hero__cue-line { height: 20px; }
}
@media (max-width: 719px) and (max-height: 600px) {
  .masthead { padding-block-start: var(--space-2xs); }
  .masthead__logo img { --logo-w: clamp(104px, 30vw, 150px); }
  .hero__body { padding-block: var(--space-2xs); }
  .hero__join { margin-block-start: var(--space-s); gap: var(--space-s); }
  .hero__cue { gap: var(--space-2xs); padding-block: 0; }
  .hero__cue-line { height: 14px; }
}

/* ---------- the confirmation page has to fit too ----------
   The client's second deliverable asks a question; both answers must be on
   screen with it. Keyed at heights that exist. */

@media (max-width: 719px) {
  .welcome__body { padding-block: var(--space-s); }
  .welcome h1 { font-size: var(--step-3); }
  .welcome .lead, .welcome .story > p { font-size: var(--step--1); }
  .welcome [data-state="share"] { --stack-gap: var(--space-s); }
  .story { --stack-gap: var(--space-2xs); }
  .story__q { margin-block-end: var(--space-2xs); }
  .story__actions { margin-block-start: var(--space-s); }
  .tick { width: 32px; height: 32px; }
}
@media (max-width: 359px) {
  .story__actions { --cluster-gap: var(--space-2xs); }
  .story__actions > .btn { padding-inline: var(--space-s); }
  .story__actions > .btn--ghost:last-child { margin-inline-end: calc(-1 * var(--space-s)); }
}
@media (min-width: 720px) and (max-height: 1100px) {
  .welcome__body { padding-block: var(--space-m); }
  .welcome h1 { font-size: var(--step-4); }
  .welcome .lead, .welcome .story > p { font-size: var(--step-0); }
  .welcome .lead { max-width: 56ch; }
  .welcome [data-state="share"] { --stack-gap: var(--space-m); }
  .story { --stack-gap: var(--space-s); }
  .story__q { margin-block-end: var(--space-xs); }
  .story__actions { margin-block-start: var(--space-s); }
  .tick { width: 40px; height: 40px; }
}
@media (min-width: 720px) and (max-height: 860px) {
  .welcome__body { padding-block: var(--space-s); }
  .welcome h1 { font-size: var(--step-3); }
  .welcome [data-state="share"] { --stack-gap: var(--space-s); }
  .tick { width: 34px; height: 34px; }
}
@media (max-width: 719px) and (max-height: 680px) {
  .welcome .tick { display: none; }
  .welcome__body { padding-block: var(--space-2xs); }
  .welcome .masthead { padding-block: var(--space-2xs); }
  .welcome .masthead__logo img { --logo-w: 78px; }
  .welcome h1 { font-size: var(--step-2); }
  .welcome [data-state="share"] { --stack-gap: var(--space-xs); }
  .story__actions { margin-block-start: var(--space-xs); }
}

/* ---------- landscape phone — short and wide ----------
   Below 540px of height the hero cannot stack; the headline and the way in
   sit side by side. The confirmation page splits the same way. */
@media (max-height: 540px) and (min-width: 560px) {
  .masthead { padding-block-start: var(--space-3xs); }
  .masthead__inner { flex-direction: row; gap: var(--space-m); }
  .masthead__logo img { --logo-w: clamp(96px, 11vw, 120px); }
  .hero { padding-bottom: var(--space-3xs); }
  .hero__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    column-gap: clamp(var(--space-m), 4vw, var(--space-2xl));
    row-gap: var(--space-2xs);
    padding-block: var(--space-2xs);
    text-align: left;
  }
  .hero__body::before, .hero__body::after { display: none; }
  .hero__title { grid-column: 1; grid-row: 1; font-size: var(--step-2); max-width: none; }
  .hero__join { grid-column: 2; grid-row: 1; margin-block-start: 0; align-items: flex-start; text-align: left; gap: var(--space-s); }
  .hero__prompt { margin-inline: 0; }
  .hero__cue { padding-block: 0; gap: var(--space-3xs); }
  .hero__cue-line { height: 14px; }

  .welcome .tick { display: none; }
  .welcome__body { padding-block: var(--space-2xs); }
  .welcome .wrap--narrow { max-width: var(--container); }
  .welcome h1 { font-size: var(--step-2); }
  .welcome .lead, .welcome .story > p { font-size: var(--step--1); }
  .welcome [data-state="share"].is-active {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: clamp(var(--space-m), 4vw, var(--space-2xl));
    align-items: center;
    text-align: left;
  }
  .welcome [data-state="share"].is-active > * { margin-block-start: 0; }
  .welcome [data-state="share"] h1,
  .welcome [data-state="share"] .lead,
  .welcome [data-state="share"] .story > p { margin-inline: 0 !important; max-width: none; }
  .story { --stack-gap: var(--space-2xs); }
  .story__q { margin-block-end: var(--space-3xs); }
  .story__field { height: clamp(56px, 14vh, 120px); }
  .story__actions { margin-block-start: var(--space-2xs); }
}
@media (max-height: 540px) and (min-width: 560px) and (max-width: 719px) {
  .hero__title { font-size: var(--step-1); }
  .welcome h1 { font-size: var(--step-1); }
  .welcome__body > .wrap { max-width: none; }
  .welcome [data-state="share"].is-active { grid-template-columns: 0.85fr 1.15fr; }
  .welcome .lead, .welcome .story > p { font-size: var(--step--2); }
  .story__q { font-size: var(--step-0); margin-block-end: var(--space-3xs); }
  .story__field { height: 44px; }
  .story__actions { --cluster-gap: var(--space-3xs); margin-block-start: var(--space-3xs); }
  .story__actions > .btn { padding-inline: var(--space-s); }
}
