/* ============================================================================
   PADEL RALLY — take.css
   The cinematic take: one sticky stage, five photographic layers, live HTML
   glass on every device. Spec: see the repository docs.

   Two compositions from one DOM:
     static  — the default. The stage is a hero panel (court + copy); every
               beat flows below as a device-kit figure with its caption.
               Serves no-JS, reduced motion, short viewports and 320px phones.
     pinned  — gated at the bottom of this file. The stage sticks, the hand
               and wrist cutouts appear, beats stack inside the glass, and
               scroll picks the state.
   ========================================================================== */

/* ── 1. TOKENS ─────────────────────────────────────────────────────────────── */
:root {
  --e-inout: cubic-bezier(.77, 0, .175, 1);

  /* Hand cutout — trimmed master 1242×4258. The phone display, as a share of
     that box (measured on the master; see generated/README.md). */
  --hand-ar: 1242 / 4258;
  /* Measured to the glass itself, not the body: the phone's side rails read ~26 px lighter
     than the screen, so the display is the body inset by one bezel. The earlier rect was
     13 px wide on each side, which let the HTML screen bleed onto the bezel and cover the
     fingertips that wrap the phone's edge. */
  --hand-gx: 17.754%; --hand-gy: 0.622%; --hand-gw: 58.213%; --hand-gh: 36.144%;

  /* Wrist cutout — trimmed master 5056×2820 (plate P5). The watch display glass. */
  --wrist-ar: 5056 / 2820;
  --wrist-fx: 38.726%; --wrist-fy: 30.496%; --wrist-fw: 17.168%; --wrist-fh: 32.340%;
  --face-ar: 868 / 912;
  --face-roll: -2.8deg;                /* the photographed watch sits rolled; the face follows it */

  /* Courtside plate — the phone display as a share of the plate. */
  --bench-ar: 2752 / 1536;
  --bench-gx: 39.83%; --bench-gy: 44.53%; --bench-gw: 35.47%; --bench-gh: 30.21%;

  /* Design widths the kit is rasterised at; the glass is rectified with zoom. */
  --kit-w: 274px;
  --board-w: 576px;
  --face-w: 128px;

  /* The first (static) panel: one viewport, floored in landscape so short landscape phones
     still hold the hero copy under the masthead. */
  --panel: 100svh;
}
@media (orientation: landscape) { :root { --panel: max(100svh, 40rem); } }
@media (orientation: portrait) {
  :root {
    --bench-ar: 1536 / 2752;
    --bench-gx: 23.44%; --bench-gy: 53.78%; --bench-gw: 62.5%; --bench-gh: 15.99%;
  }
}

/* ── 2. STAGE AND LAYERS (shared) ──────────────────────────────────────────── */
.take { position: relative; }
/* Static: the first viewport is the hero panel (court + copy), so the stage pads
   the beats down by one viewport. Padding, not margin — a margin here would
   collapse through the take and push the whole page down. */
.stage { position: relative; min-height: 100svh; padding-top: var(--panel); background: var(--ink); }

.court, .court picture, .court img { position: absolute; inset: 0; }
.court { bottom: auto; height: var(--panel); }   /* the static composition's court stops at the first panel */
.court img { width: 100%; height: 100%; object-fit: cover; }
.court__p1 { animation: courtIn 600ms var(--e-out) both; }   /* from first paint, not from JS */
.court__p3 { opacity: 0; }
@keyframes courtIn { from { opacity: 0; } to { opacity: 1; } }

/* The asleep screen under every live glass: black with a little sheen, so a
   glass that is off never shows plate pixels. */
.off-glass {
  position: absolute;
  background: linear-gradient(160deg, rgba(255,255,255,.07), rgba(255,255,255,0) 46%), #050505;
}

.hand, .wrist, .cover { position: absolute; }
.hand img, .wrist img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.hand .off-glass {
  left: var(--hand-gx); top: var(--hand-gy); width: var(--hand-gw); height: var(--hand-gh);
  border-radius: calc(var(--gw, 0px) * 0.09);
}
.cover .off-glass {
  left: var(--bench-gx); top: var(--bench-gy); width: var(--bench-gw); height: var(--bench-gh);
  border-radius: calc(var(--bw, 0px) * 0.045);
}

/* Hero copy — absolute, out of flow, first in source so the h1 and CTAs read before the take. */
.hero {
  position: absolute; inset: 0 auto auto 0; width: 100%; height: var(--panel);
  display: flex; align-items: center;
  padding: calc(var(--head) + 1.5rem) 6vw 2rem;
  z-index: 3; pointer-events: none;
}
.hero > * { pointer-events: auto; }
.hero__inner { display: grid; gap: var(--s3); width: min(40vw, 36rem); }
.hero__logo { width: min(340px, 60%); }
.hero__logo img { width: 100%; }
.hero__lede { max-width: 32ch; }
.hero__ctas { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }
/* Portrait hero, in both compositions: full width, top-aligned under the masthead. */
@media (max-height: 699px) and (orientation: landscape) { .hero .d-hero { font-size: min(var(--t-hero), 12vh); } }
@media (orientation: portrait) {
  .hero { padding: calc(var(--head) + 1rem) 1.25rem 0; align-items: flex-start; }
  .hero__inner { width: 100%; gap: var(--s2); }
  .hero .d-hero { font-size: clamp(2.6rem, 13vw, 4rem); }
}

/* ── 3. BEATS — static composition (default) ───────────────────────────────── */
.beats {
  display: grid; gap: var(--s8) var(--s4);
  grid-template-columns: minmax(0, 1fr);
  width: var(--wrap); margin: 0 auto;
  padding-block: var(--s8) var(--s10);
}
@media (min-width: 760px)  { .beats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1160px) { .beats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.beat { margin: 0; display: grid; justify-items: center; gap: var(--s3); align-content: start; }
.beat[data-body="bench"] { grid-column: 1 / -1; }
.beat[data-body="none"]  { grid-column: 1 / -1; padding-block: var(--s6); }

.beat .device { width: min(292px, 100%); }
.beat[data-body="bench"] .device { width: min(36rem, 100%); }
.beat .device__screen { min-height: 552px; }
.beat[data-body="bench"] .device__screen { min-height: 0; aspect-ratio: 2.1; }

.beat figcaption { display: grid; gap: .8rem; justify-items: center; text-align: center; max-width: min(36ch, 100%); }
.cap__eyebrow { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; justify-content: center; }
.cap__title { font-size: var(--t-d2); }
.beat[data-body="none"] .cap__title { font-size: var(--t-d1); max-width: 22ch; }
.cap__text { color: var(--chalk-mid); font-size: var(--t-sm); line-height: 1.45; text-wrap: pretty; }

/* The live pair in the static grid: the board with the watch face beside it.
   The board is authored at --board-w, so the static device is exactly that wide
   (zoomed down on narrow screens) and never stretches. */
.beat .live__pair { display: flex; align-items: center; justify-content: center; gap: clamp(1rem, 3vw, 2.5rem); width: 100%; flex-wrap: wrap; }
.beat .live__pair .device { flex: 0 1 auto; }
.beat[data-body="bench"] .device { width: min(calc(var(--board-w) + 20px), 100%); }
@media (max-width: 640px) {
  .beat[data-body="bench"] .device { width: min(calc(var(--board-w) * .58 + 20px), 100%); }
  .beat[data-body="bench"] .device__screen { zoom: .58; }
}
@media (max-width: 380px) {
  .beat[data-body="bench"] .device { width: min(calc(var(--board-w) * .45 + 20px), 100%); }
  .beat[data-body="bench"] .device__screen { zoom: .45; }
}

/* The watch is a device on an arm, never a bare panel. When the rig cannot pin — no JS,
   reduced motion, or a viewport under the gate below — the stage's plate layers are off, so
   the static grid carries its own copy of the P5 plate and maps the face onto the photographed
   glass with the same tokens the rig uses. The plate keeps its authored width and is cropped by
   the card on narrow screens (faded at both edges), exactly as the pinned layer is, so the face
   itself never shrinks below the size it is authored at.
     --ws-w      the plate width at which the glass measures --face-w: 128 / 0.17168
     face left   50% - --ws-w x (0.5 - --wrist-fx)      = --ws-w x 0.11274 left of centre
     face top    --ws-w x (2820/5056) x --wrist-fy      = --ws-w x 0.17012 down from the top */
.wrist-still {
  --ws-w: calc(var(--face-w) * 5.8248);
  position: relative; flex: 0 1 auto;
  width: min(var(--ws-w), 100%);
  height: calc(var(--ws-w) * .55775);   /* the plate's own height (2820/5056): crop sideways only */
  overflow: hidden;
}
.wrist-still picture { display: block; }
.wrist-still img {
  position: absolute; top: 0; left: 50%;
  width: var(--ws-w); height: auto; max-width: none;
  transform: translateX(-50%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
}
/* The face must never arrive ahead of the arm it sits on, so it is hidden from the first paint
   and take.js reveals it once the plate has decoded. The gate keys on .js, which the inline boot
   script sets while the document is still parsing — keying it on a class take.js adds would leave
   the face showing for as long as that script takes to arrive. Fail-safe in every direction: no
   script at all leaves .js unset; a take.js that never arrives trips the boot script's failsafe,
   which removes .js; a plate that never decodes still reveals; and pinned, the rule further down
   re-shows the face unconditionally, because there the rig owns it. */
.js .wrist-still .face { visibility: hidden; }
.js .wrist-still.is-ready .face { visibility: visible; }
.wrist-still .face {
  position: absolute;
  left: calc(50% - var(--ws-w) * .11274);
  top: calc(var(--ws-w) * .17012);
  transform: rotate(var(--face-roll));
}

/* The watch face — authored at --face-w, on the photographed watch when pinned. */
.face {
  box-sizing: border-box; overflow: hidden;
  width: var(--face-w); aspect-ratio: var(--face-ar);
  display: grid; align-content: center; gap: 5px;
  padding: 10px 8px; text-align: center;
  background: #050505; border-radius: 18%;
  color: var(--chalk);
}
.face > * { min-width: 0; }
.face__top {
  font-family: var(--f-data); font-size: 7px; font-stretch: 78%;
  letter-spacing: .12em; text-transform: uppercase; color: var(--chalk-low);
  display: flex; align-items: center; justify-content: center; gap: 4px;
}
.face__live { color: var(--volt); display: inline-flex; align-items: center; gap: 3px; }
.face__live::before { content: ''; width: 4px; height: 4px; border-radius: 50%; background: var(--volt); }
/* Two equal slots, each centred: the ink then sits symmetrically about the face's centre line
   whichever state the score is in. A single centred flex row cannot do this, because the swap
   reserves the width of its widest state ("Game") and stretches "40" inside it, which pushed
   the pair ~15 px right of centre and opened a hole between the numbers. */
.face__score {
  /* Neither `baseline` nor `flex-end` aligns these reliably: the "us" cell's baseline is
     synthesised through the swap, so the flex box offsets it by ~2.5 px. Instead both cells are
     given one explicit, identical line box and aligned at the top — same font, same size, same
     24 px line, same origin, so the two baselines are the same line by construction. */
  display: flex; align-items: flex-start; gap: 4px;
  width: min(100%, 100px); margin-inline: auto;
  margin-block: 2px 3px;               /* the score is the biggest element: give it room */
}
.face__score > .face__pt { flex: 1 1 0; text-align: center; height: 24px; line-height: 24px; }
/* Both scores are set at one size, so their baselines coincide by construction rather than by
   baseline alignment — which cannot align them reliably here, since the "us" item's baseline is
   synthesised through the swap. Ours reads as ours by colour, which is all a watch needs. */
.face__pt {
  font-family: var(--f-display); font-weight: 800; font-stretch: 108%;
  font-size: 24px; line-height: 1; letter-spacing: -.04em;
  font-variant-numeric: tabular-nums; color: var(--chalk-low);
}
.face__pt--us { color: var(--volt); }
.face__keys { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
.face__key {
  position: relative; overflow: hidden;
  font-family: var(--f-data); font-size: 7px; font-weight: 600; font-stretch: 78%;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 7px 2px; border-radius: 999px;
  background: var(--surface-hi); color: var(--chalk-mid);
}
.face__key--us { background: var(--volt); color: var(--on-volt); }
.face__undo {
  font-family: var(--f-data); font-size: 6.5px; font-stretch: 78%;
  letter-spacing: .14em; text-transform: uppercase; color: var(--chalk-low);
}
/* The swap wrapper must contribute no box of its own: as an inline-grid it synthesised a
   baseline from its box bottom, and even as a plain inline it sat 2.5 px low. `display: contents`
   removes it, so the resting number is inline text directly in the cell — structurally identical
   to the other score, therefore on the same line and the same baseline by construction. The
   second state is taken out of flow over the cell, at the same size and line-height (so its
   baseline is that same line) and condensed so the word occupies the number's slot.
   Centring uses left/right rather than a transform, because swapIn/swapOut animate transform. */
.face .swap { display: contents; }
/* Even with the wrapper gone the "us" cell sits 2.09 px low against an identical-glyph control,
   so the cell carries that measured correction. Expressed in em, it survives any face size. */
.face__pt--us { position: relative; top: -.087em; }
.face .swap > .swap__a { display: inline-block; }   /* visible overflow keeps a real baseline */
.face .swap > .swap__b {
  display: none; opacity: 0;
  position: absolute; left: 0; right: 0; bottom: 0;
  text-align: center; white-space: nowrap;
  font-stretch: 62%; letter-spacing: -.04em;   /* condensed so the word occupies the number's slot */
}
.face__flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; }

/* The courtside board — authored at --board-w. Big numerals, no chrome. */
.board--glass { background: transparent; border: 0; padding: 0; box-shadow: none; border-radius: 0; }
.board--glass .board__screen { border-radius: 0; height: 100%; padding: 14px 28px; gap: 8px; display: grid; align-content: space-between; }
.board--glass .board__set { font-size: 12px; }
.board--glass .board__grid thead th { font-size: 9px; }
.board--glass .board__row th { font-size: 18px; padding: 4px 0; }
.board--glass .board__row td { font-size: 32px; padding: 4px 0 4px 14px; }
.board--glass .board__row .board__pt { font-size: 72px; }
.board--glass .board__foot { font-size: 9px; padding-top: 8px; }
.board--glass .board__pt .swap__b { font-size: .46em; letter-spacing: .1em; text-transform: uppercase; align-self: center; }

/* Landscape "ready" and "complete" faces of the bench glass. */
.bench-ready, .bench-done { height: 100%; padding: 22px 28px; display: grid; align-content: space-between; gap: 10px; }
.bench-ready__head, .bench-done__head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.bench-ready__head .board__set, .bench-done__head .board__set { white-space: nowrap; font-size: 10px; }
.bench-ready__title, .bench-done__title { font-family: var(--f-display); font-weight: 800; font-stretch: 116%; font-size: 26px; letter-spacing: -.02em; text-transform: uppercase; line-height: .95; }
.bench-ready__teams { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; }
.bench-ready__team { font-family: var(--f-display); font-weight: 800; font-stretch: 112%; font-size: 30px; letter-spacing: -.02em; line-height: 1; }
.bench-ready__team + .bench-ready__vs { font-family: var(--f-data); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--chalk-dim); }
.bench-ready__team:last-child { text-align: right; color: var(--chalk-mid); }
.bench-ready__meta, .bench-done__meta { display: flex; justify-content: space-between; gap: 12px; font-family: var(--f-data); font-size: 9.5px; font-stretch: 80%; letter-spacing: .12em; text-transform: uppercase; color: var(--chalk-low); border-top: 1px solid var(--line); padding-top: 10px; }
.bench-done__score { display: flex; align-items: baseline; gap: 14px; }
.bench-done__set { font-family: var(--f-display); font-weight: 800; font-stretch: 112%; font-size: 54px; letter-spacing: -.04em; line-height: 1; color: var(--chalk-mid); font-variant-numeric: tabular-nums; }
.bench-done__set.won { color: var(--volt); }
.bench-done__line { font-size: 17px; font-weight: 600; }
.bench-done__confirm { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.bench-done__cta { font-family: var(--f-data); font-size: 10px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--volt); border: 1px solid var(--volt-line); padding: 8px 12px; }

/* Screens that are new to this take (the kit carries the rest). */
.ui-body > * { min-width: 0; }            /* a wide row shrinks instead of pushing the card past the glass */
.hr { width: 100%; height: 92px; display: block; }
.hr polyline { fill: none; stroke: var(--volt); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
.hr .zone { fill: rgba(255,255,255,.035); }
.ui-metrics--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.feedcard { display: flex; gap: 9px; align-items: flex-start; }
.feedcard__body { display: grid; gap: 5px; min-width: 0; }
.feedcard__line { font-size: 11.5px; line-height: 1.35; }
.feedcard__line b { font-weight: 600; }
.feedcard__react { display: flex; gap: 10px; font-family: var(--f-data); font-size: 7.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--chalk-dim); }
.feedcard__react span:first-child { color: var(--volt); }
.year-card { height: 100%; display: grid; align-content: space-between; padding: 20px 16px 18px; background: linear-gradient(165deg, #1a2a06, var(--ink) 60%); }
.year-card__big { font-family: var(--f-display); font-weight: 900; font-stretch: 118%; font-size: 44px; line-height: .86; letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.year-card__rows { display: grid; gap: 9px; }
.year-card__row { display: flex; justify-content: space-between; align-items: baseline; border-top: 1px solid var(--line); padding-top: 8px; }
.year-card__row b { font-family: var(--f-display); font-weight: 800; font-stretch: 112%; font-size: 17px; }
.ui-cta { text-align: center; border: 1px solid var(--volt); color: var(--volt); border-radius: var(--r-md); padding: 10px; font-family: var(--f-data); font-size: 8px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; }
.ui-cta--armed { background: var(--volt); color: var(--on-volt); }

/* Static composition hides the photographic actors — they exist for the pin. */
.hand, .wrist, .wrist-front, .cover, .court__p3 { display: none; }

/* ── 4. THE PINNED COMPOSITION ─────────────────────────────────────────────── */
/* Gate: JS present, motion allowed, a viewport tall enough to hold a legible
   phone under the masthead, and wide enough that the glass keeps the kit at
   ≥ 0.85 of its design width. Below the gate the static composition stands. */
@media (prefers-reduced-motion: no-preference) and (min-height: 700px) and (min-width: 341px) {

  .js .stage {
    position: sticky; top: 0; height: 100svh; min-height: 0; padding: 0; overflow: hidden;
    isolation: isolate;
  }
  .js .court { inset: 0; height: auto; }
  .js .court__p1, .js .court__p3 { transition: opacity 900ms var(--e-soft); }
  .js .court__p3 { transition-delay: 420ms; }          /* starts at 60 % of the 700 ms hand exit */
  .js .stage[data-live] .court__p3 { opacity: 1; }
  .js .stage[data-live] .court__p1 { visibility: hidden; transition: visibility 0s 1400ms; }
  .js .stage:not([data-live]) .court__p3, .js .stage:not([data-live]) .cover { transition-delay: 0ms; }
  /* The whole court plate stack takes the camera: parallax + push-in (JS, fine pointers only). */
  .js .court { will-change: transform; transform-origin: 50% 62%; }

  .js .hand, .js .wrist, .js .wrist-front, .js .cover, .js .court__p3 { display: block; }

  /* The hand: phone ≈ 62svh tall, clamped so short and tall viewports both keep the kit legible. */
  .js .hand {
    --phone-h: clamp(520px, 62svh, 760px);
    height: calc(var(--phone-h) / 0.3664);
    aspect-ratio: var(--hand-ar);
    left: 56%; top: calc(var(--head) + 1.6svh);
    z-index: 2;
    transition: transform 700ms var(--e-inout);
    will-change: transform;
  }
  .js .stage[data-live] .hand, .js .stage[data-end] .hand { transform: translateY(120svh); }

  /* The courtside cover box mirrors object-fit: cover, so the bench glass sits on the photographed phone. */
  .js .cover {
    left: 50%; top: 50%; translate: -50% -50%;
    width: max(100%, calc(100svh * 1.7917));
    aspect-ratio: var(--bench-ar);
    z-index: 1; pointer-events: none;
    opacity: 0; transition: opacity 900ms var(--e-soft) 420ms;
  }
  .js .stage[data-live] .cover { opacity: 1; }

  /* The wrist: forearm from the left edge, face in the lower-left third, the fist
     cropped by the bottom edge so it never covers the board's team column. */
  /* The wrist is a close-up: it is sized so the watch face lands at --face-max, capped by the
     room between the board and the bottom of the frame, and by 240vw so a phone still shows
     forearm either side of the watch. The face sits 30.5 % down the plate and is 32.3 % tall,
     hence the 0.1701 / 0.18037 constants (those fractions divided by the plate aspect).
     The arm is allowed to cross in front of the board's lower edge — .wrist-front does the
     occluding, so the two no longer have to be held apart. */
  .js .wrist, .js .wrist-front img {
    --face-max: 196px;
    --wrist-w: min(
      calc(var(--face-max) / 0.17168),
      calc((100svh - var(--by, 60svh) - var(--bh, 0px) - 26px) / 0.18037),
      240vw);
  }
  .js .wrist {
    width: var(--wrist-w); aspect-ratio: var(--wrist-ar);
    /* The plate's own left edge is a cut (the forearm runs off it), so it is always pushed
       off-frame; where the plate is wider than the viewport that also centres the watch.
       The -8vw floor also carries the hand clear of the board's points column. */
    left: min(-8vw, calc(50% - 0.4731 * var(--wrist-w)));
    top: calc(var(--by, 60svh) + var(--bh, 0px) + 22px - 0.1701 * var(--wrist-w));
    bottom: auto;
    z-index: 2;                          /* under the beats layer, so the watch face paints on it */
    transform: translateY(var(--wrist-h, 60svh));
    transition: transform 520ms var(--e-soft);
    will-change: transform;
  }
  /* The plate is narrower than a landscape viewport — the room under the board caps how big
     the arm can be — so its side cuts can fall on screen. A 4 % fade at each edge turns the
     cut into falloff against the dark court instead of a visible boundary. */
  .js .wrist img, .js .wrist-front img {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%);
  }

  /* Occlusion matte: the same arm, clipped to the board's glass, painted above it. The arm is
     nearer the camera than the courtside phone, so where they overlap the arm wins — and its
     own alpha edge does the cutting, not a rectangle. Outside the glass the base .wrist layer
     already covers the phone's body, which sits in the court plate below it. */
  .js .wrist-front {
    position: absolute; z-index: 4; overflow: hidden; pointer-events: none;
    left: var(--bx, 0px); top: var(--by, 0px); width: var(--bw, 0px); height: var(--bh, 0px);
    opacity: 0;
  }
  .js .stage[data-live] .wrist-front { opacity: 1; }
  .js .wrist-front img {
    position: absolute; display: block;
    left: calc(var(--wx, 0px) - var(--bx, 0px));
    top: calc(var(--wy, 0px) - var(--by, 0px));
    width: var(--wrist-w); height: auto;
    max-width: none;                     /* the kit's img reset would clamp it to the clip's width */
    transform: translateY(var(--wrist-h, 60svh));
    transition: transform 520ms var(--e-soft);
  }
  .js .stage[data-wrist] .wrist-front img { transform: translateY(0); transition: transform 560ms var(--e-out); }
  .js .stage[data-wrist] .wrist { transform: translateY(0); transition: transform 560ms var(--e-out); }

  /* Beats stack over the stage; each device is placed on its photographed screen. */
  .js .beats {
    position: absolute; inset: 0; display: block;
    width: auto; margin: 0; padding: 0; z-index: 3;
  }
  .js .beat {
    position: absolute; inset: 0; display: block;
    opacity: 0; pointer-events: none;
    transition: opacity 380ms var(--e-soft) 80ms;
  }
  .js .beat.is-on { opacity: 1; pointer-events: auto; z-index: 1; transition: opacity 220ms var(--e-out) 0ms; }

  .js .beat .device {
    position: absolute;
    left: var(--gx); top: var(--gy); width: var(--gw); height: var(--gh);
    padding: 0; border: 0; box-shadow: none; background: #000;
    border-radius: calc(var(--gw) * 0.09);
    overflow: hidden;
    transition: transform 700ms var(--e-inout), opacity 300ms var(--e-out);
  }
  .js .beat .device__notch { display: none; }
  .js .beat .device__screen {
    width: var(--kit-w); min-height: 0;
    height: calc(var(--gh) / var(--zoom, 1));
    zoom: var(--zoom, 1);
    border-radius: 0;
  }

  /* Hand-body devices leave with the hand; bench-body devices sit on the bench. */
  .js .stage[data-live] .beat[data-body="hand"] .device,
  .js .stage[data-end]  .beat[data-body="hand"] .device { transform: translateY(120svh); }
  /* The courtside screen is part of the photographed phone, so it rides the camera with the
     plate. Without this it stayed at the plate's untransformed rect while the plate was pushed
     in and parallaxed, and the board's bottom row drifted ~11 px below the glass. The origin is
     the camera's own (50%, 62%) of the stage, re-expressed against this box; `transition` drops
     transform, which the camera rewrites every frame. */
  .js .beat[data-body="bench"] .device {
    left: var(--bx); top: var(--by); width: var(--bw); height: var(--bh);
    border-radius: calc(var(--bw) * 0.045);
    transform-origin: var(--cam-ox, 50%) var(--cam-oy, 62%);
    transform: var(--cam, none);
    transition: opacity 300ms var(--e-out);
  }
  .js .beat[data-body="bench"] .device__screen {
    width: var(--board-w);
    height: calc(var(--bh) / var(--bzoom, 1));
    zoom: var(--bzoom, 1);
  }

  /* The phone that goes down to the bench leaves with its screen lit; only its caption yields. */
  .js .stage[data-handoff="bench"] .beat.is-leaving { opacity: 1; transition: none; }
  .js .stage[data-handoff="bench"] .beat.is-leaving figcaption { opacity: 0; transition: opacity 220ms var(--e-soft); }
  /* A hand-off: the arriving body's glass stays dark until the plates have dissolved. */
  .js .stage[data-handoff="bench"] .beat[data-body="bench"] .device,
  .js .stage[data-handoff="hand"]  .beat[data-body="hand"]  .device { opacity: 0; }
  /* The hero glass wakes once fonts are in, 180 ms after the hand settles. */
  .js .stage:not(.is-awake) .beat[data-f="0"] .device { opacity: 0; }
  .js .stage.is-awake .beat[data-f="0"] .device { transition-delay: 0ms, 180ms; }   /* transform, opacity — order follows the shorthand */

  /* The watch face lives in beat 8's figure but sits on the wrist, and moves with it. */
  .js .beat .live__pair { display: contents; }
  /* Pinned, the stage's .wrist layer is the plate, so the static carrier steps out of the way
     and the face is placed by the rig exactly as before. */
  .js .beat .wrist-still { display: contents; }
  .js .beat .wrist-still > picture { display: none; }
  .js .beat .wrist-still .face { visibility: visible; }
  .js .beat .face {
    position: absolute;
    left: var(--fx); top: var(--fy); width: var(--fw); height: var(--fh);
    zoom: var(--fzoom, 1);
    border-radius: 18%;
    z-index: 5;
    transform: translateY(calc(var(--wrist-h, 60svh) / var(--fzoom, 1))) rotate(var(--face-roll));
    transition: transform 520ms var(--e-soft);
  }
  /* Last resort only: below ~86 px the labels stop being readable, so the face keeps the score
     and the two keys. Every pinned breakpoint in the matrix now lands well above this. */
  .js .stage.face-small .face { gap: 8px; }
  .js .stage.face-small .face__top, .js .stage.face-small .face__undo { display: none; }
  .js .stage.face-small .face__key { font-size: 0; padding: 9px 2px; }
  .js .stage.face-small .face__pt--us { font-size: 34px; }
  .js .stage.face-small .face__pt { font-size: 24px; }
  .js .stage[data-wrist] .beat .face { transform: translateY(0) rotate(var(--face-roll)); transition: transform 560ms var(--e-out); }

  /* Copy: the on beat's caption in the left column. Content rises as it arrives.
     Beat 0's caption is only for the static grid — the hero block carries its copy. */
  .js .beat[data-f="0"] figcaption { display: none; }
  .js .beat figcaption {
    position: absolute; left: 6vw; top: 46%; transform: translateY(-50%);
    width: min(38vw, 34rem); max-width: none;
    justify-items: start; text-align: left;
  }
  /* Live beats: the bench glass holds the middle of the frame, so the copy sits above it. */
  .js .beat[data-body="bench"] figcaption { top: calc(var(--head) + 7svh); transform: none; }
  /* The hero block fades as soon as the take moves on, so its last line never peeks under the masthead. */
  .js .hero { transition: opacity 300ms var(--e-soft); }
  .js .take:has(> .stage[data-state]:not([data-hero])) > .hero { opacity: 0; pointer-events: none; }
  .js .cap { display: grid; gap: .9rem; transform: translateY(10px); transition: transform 480ms var(--e-out); }
  .js .beat.is-on .cap { transform: none; }
  /* A hand-off's arriving copy waits for the dissolve midpoint so the eye is on the type
     through the cut — in both directions. */
  .js .stage[data-handoff] .beat.is-on .cap { animation: capHold 300ms var(--e-out) 720ms both; transition-delay: 720ms; }
  .js .beat .ui-body { transform: translateY(10px); transition: transform 480ms var(--e-out); }
  .js .beat.is-on .ui-body, .js .beat.is-leaving .ui-body { transform: none; }
  .js .cap__eyebrow { justify-content: start; }
  .js .cap__title { font-size: var(--t-d1); }
  .js .beat[data-body="none"] figcaption { width: min(60vw, 52rem); }
  .js .beat[data-body="none"] .cap__title { font-size: var(--t-d1); }

  /* The end: the glass sleeps, the hand goes, the court darkens and holds the line. */
  .js .stage::after {
    content: ''; position: absolute; inset: 0; background: #000; opacity: 0;
    pointer-events: none; z-index: 1; transition: opacity 900ms var(--e-soft);
  }
  .js .stage[data-end]::after { opacity: .55; }
  .js .stage[data-end] .beat .device { opacity: 0; transition: opacity 120ms var(--e-soft), transform 700ms var(--e-inout); }

  /* The rig's honesty about swaps: the second state exists only when the take pins. */
  .js .beat .swap > .swap__b, .js .face .swap > .swap__b { display: block; }

  /* ── 5. THE POINT — cause, then effect ──────────────────────────────────── */
  /* Wrist in (560) → dwell (≥ 500) → key press at 1150 with a flash → the wrist
     reacts at 1210 → the watch's own score at 1250 → the board at 1450 (500 ms
     swaps with a bloom). Keyed on data-point, which the driver sets with the wrist
     and holds 520 ms past the exit so the fills persist under the fade; the jolt
     stays on data-wrist because the wrist itself leaves on a transform transition. */
  .js .stage[data-point] .face__key--us { animation: keyTap 140ms var(--e-out) 1150ms both; }
  .js .stage[data-point] .face__key--us .face__flash { animation: keyFlash 140ms var(--e-out) 1150ms both; }
  .js .stage[data-wrist] .wrist { animation: wristJolt 90ms var(--e-out) 1210ms; }
  .js .stage[data-point] .face .swap > .swap__a { animation: swapOut 500ms var(--e-out) 1250ms both; }
  .js .stage[data-point] .face .swap > .swap__b { animation: swapIn  500ms var(--e-out) 1250ms both; }
  .js .stage[data-point] .beat .board .swap > .swap__a { animation: swapOut 500ms var(--e-out) var(--sd, 1450ms) both; }
  .js .stage[data-point] .beat .board .swap > .swap__b { animation: swapIn  500ms var(--e-out) var(--sd, 1450ms) both; }
  .js .stage[data-point] .beat .board__pt--live { animation: bloom 600ms var(--e-out) 1450ms both; }

  /* Fitness: the trace draws. */
  .js .beat .hr polyline { clip-path: inset(0 100% 0 0); transition: clip-path 900ms var(--e-out) 200ms; }
  .js .beat.is-on .hr polyline { clip-path: inset(0 0 0 0); }

  /* Staggered arrivals inside a screen. */
  .js .beat .ui-body > * { opacity: 0; transform: translateY(6px); transition: opacity 300ms var(--e-out), transform 360ms var(--e-out); }
  .js .beat.is-on .ui-body > *, .js .beat.is-leaving .ui-body > * { opacity: 1; transform: none; }
  .js .beat.is-on .ui-body > :nth-child(2) { transition-delay: 40ms; }
  .js .beat.is-on .ui-body > :nth-child(3) { transition-delay: 80ms; }
  .js .beat.is-on .ui-body > :nth-child(4) { transition-delay: 120ms; }
  .js .beat.is-on .ui-body > :nth-child(5) { transition-delay: 160ms; }
  .js .beat.is-on .ui-body > :nth-child(6) { transition-delay: 200ms; }

  /* ── PORTRAIT — phones and portrait tablets ─────────────────────────────── */
  @media (orientation: portrait) {
    /* Width-driven phone: body ≈ min(72vw, 310px), top just under the masthead,
       the body running off the bottom edge. Rests low while the hero copy shows. */
    .js .hand {
      --phone-h: auto;
      height: auto; width: min(115vw, 496px);
      left: 50%; margin-left: max(-57.5vw, -248px);
      top: calc(var(--head) + 10px);
    }
    .js .stage:not([data-state]) .hand, .js .stage:not([data-state]) .beat[data-body="hand"] .device,
    .js .stage[data-hero] .hand, .js .stage[data-hero] .beat[data-body="hand"] .device { transform: translateY(58svh); }

    .js .cover { width: max(100%, calc(100svh * 0.5581)); }

    .js .wrist, .js .wrist-front img { --face-max: 190px; }

    /* Copy becomes a bottom band; captions are cut; the tag stays. */
    .js .beat figcaption {
      left: 0; right: 0; top: auto; bottom: 0; transform: none;
      width: auto; padding: 5rem 1.25rem 1.5rem;
      background: linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.88) 45%);
    }
    .js .cap__title { font-size: clamp(1.7rem, 8.5vw, 2.6rem); }
    .js .cap__text { display: none; }
    .js .beat[data-body="none"] figcaption { width: auto; }

    /* Live beats: the line goes under the masthead, the board holds the middle, the wrist the bottom. */
    .js .beat[data-body="bench"] figcaption {
      top: var(--head); bottom: auto; padding: .75rem 1.25rem 4rem;
      background: linear-gradient(180deg, rgba(0,0,0,.85) 0, rgba(0,0,0,.85) 62%, rgba(0,0,0,0));   /* solid under the line, fades above the floodlight */
    }
    .js .beat[data-body="bench"] .cap__title { font-size: clamp(1.4rem, 7vw, 2rem); }
  }
}


/* Animation vocabulary for the point. */
@keyframes capHold  { from { opacity: 0; } }
@keyframes keyTap   { 0% { transform: scale(1); } 40% { transform: scale(.94); } 100% { transform: scale(1); } }
@keyframes keyFlash { 0% { opacity: 0; } 30% { opacity: .55; } 100% { opacity: 0; } }
@keyframes wristJolt { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(2px); } }
@keyframes bloom {
  0%   { text-shadow: 0 0 0 rgba(50,255,40,0); }
  30%  { text-shadow: 0 0 22px rgba(50,255,40,.55); }
  100% { text-shadow: 0 0 0 rgba(50,255,40,0); }
}
