/* Fives — descent.css · the entrance system for the long pages.
   Hand-authored; NO .jsx sibling — safe to edit directly.

   Commission: operations/design-review/fives-entrance-commission-2026-08-19/
   Gate 1 direction 2 "The Chalk Descent" · M1 = the shared beat system +
   /history. M2 (/shot-vault) and M3 (/clubs, /courts) append below.

   Two layers, separable by design (owner requirement):

     CORE  — the .fv-beat grammar below. Pure CSS + reveal.js's existing
             `.is-in` contract. Hidden states are gated on `html.fv-armed`
             (reveal.js's first statement), so a page that ships this markup
             without reveal.js renders statically instead of staying blank.

     MODULE A — the thread (descent.js). Everything keyed on `html.fv-descent`,
             which descent.js adds as ITS first statement. Delete the script
             tag and the page falls back to the CSS `::before` rail with the
             core grammar intact.

   Reduced motion: the block at the foot is the belt; descent.js carries the
   brace (and honours a preference flipped mid-session). */

/* ════════════════════════════════════════════════════════════════
   0 · keyframes
   ════════════════════════════════════════════════════════════════ */

/* the node keeps its own centring transform — the pop composes onto it */
@keyframes fvd-pop    { from { transform: translate(-50%, 0) scale(0.6); }
                        to   { transform: translate(-50%, 0) scale(1); } }
@keyframes fvd-rise   { from { transform: translateY(16px); }
                        to   { transform: translateY(0); } }
@keyframes fvd-return { from { transform: translate(var(--fvd-x, 0px), 16px); }
                        to   { transform: translate(0, 0); } }
@keyframes fvd-fade   { from { opacity: 0; } to { opacity: 1; } }
/* 104, not 100 — same reason as tokens.css's fv-draw: at exactly one full
   pathLength the round linecap of the next dash lands on the path end and
   leaves a permanent 2px speck. */
@keyframes fvd-draw   { from { stroke-dashoffset: 104; } to { stroke-dashoffset: 0; } }
/* the long thread is measured, not normalised: a path stretched down a
   6,000px page and a `pathLength` of 100 give the renderer two different
   answers about how long a dash is, so descent.js hands it the real one. */
@keyframes fvd-draw-thread { from { stroke-dashoffset: var(--fvd-len, 0px); }
                             to   { stroke-dashoffset: 0px; } }

/* ════════════════════════════════════════════════════════════════
   1 · the beat system  ·  .fv-beat  (CORE)
   ════════════════════════════════════════════════════════════════
   Serve then return: the node strikes, the year answers (+90ms), the card
   comes back from its own side of the court (+180ms). Light does not bounce
   (opacity on `settle`); arrivals do (transform on `rebound`); chalk is
   drawn, never faded (strokes on `draw`).

   The side is derived from where the card actually RENDERS, not from the
   class name: at >=880px `.hi-milestone` puts its card in column 2 (right of
   the rule, so it returns from +24px) and `.hi-milestone--right` puts its
   card in column 1 (left of the rule, so it returns from -24px). Below 880
   the timeline is one column and a side is meaningless — rise only. */
.hi-milestone { --fvd-x: 0px; }
@media (min-width: 880px) {
  .hi-milestone          { --fvd-x:  24px; }   /* card renders RIGHT of the rule */
  .hi-milestone--right   { --fvd-x: -24px; }   /* card renders LEFT  of the rule */
}

/* hidden states — armed only (fail-open) */
html.fv-armed .fv-beat .hi-milestone-node {
  opacity: 0;
  transform: translate(-50%, 0) scale(0.6);
}
html.fv-armed .fv-beat .hi-milestone-date {
  opacity: 0;
  transform: translateY(16px);
}
html.fv-armed .fv-beat .hi-milestone-card {
  opacity: 0;
  transform: translate(var(--fvd-x, 0px), 16px);
}

/* the node's pop waits for its chalk strike when module A is live (the beat
   Gate 2 judged); with the module stripped it lands on the beat's downbeat. */
.fv-beat .hi-milestone-node { --fvd-node-d: 0ms; }
html.fv-descent .fv-beat .hi-milestone-node { --fvd-node-d: 220ms; }

html.fv-armed .fv-beat.is-in .hi-milestone-node {
  animation: fvd-pop  var(--fives-t-quick) var(--fives-e-rebound) var(--fvd-node-d) both,
             fvd-fade var(--fives-t-quick) var(--fives-e-settle)  var(--fvd-node-d) both;
}
html.fv-armed .fv-beat.is-in .hi-milestone-date {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) 90ms both,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  90ms both;
}
html.fv-armed .fv-beat.is-in .hi-milestone-card {
  animation: fvd-return var(--fives-t-gentle) var(--fives-e-rebound) 180ms both,
             fvd-fade   var(--fives-t-gentle) var(--fives-e-settle)  180ms both;
}

/* ════════════════════════════════════════════════════════════════
   2 · group stagger  ·  --fv-group  (CORE)
   ════════════════════════════════════════════════════════════════
   tokens.css gives .fv-reveal a 60ms index step, capped at 5. Grids that
   arrive row by row add a second axis: 90ms per row, capped at 4. With no
   --fv-group set the delay is byte-for-byte what tokens.css already ships,
   so this is inert everywhere it isn't asked for. */
.fv-reveal {
  transition-delay: calc(min(var(--fv-i, 0), 5) * 60ms
                       + min(var(--fv-group, 0), 4) * 90ms);
}

/* ── the gallery plates ──
   .hi-shot-card declares its own `transition` (the hover lift), which would
   win over .fv-reveal's — same specificity, later sheet. So the arrival runs
   as a one-shot ANIMATION instead, with fill `backwards` only: it covers the
   stagger delay, then hands back to the resting `.is-in` values and the
   card's hover transition is free again. Group rebound, no frame-draw —
   these are content cards, not empty frames (Gate 2, amendment 3). */
.hi-gallery .hi-shot-card {
  --fvd-d: calc(min(var(--fv-i, 0), 5) * 60ms + min(var(--fv-group, 0), 4) * 90ms);
}
html.fv-armed .hi-gallery .hi-shot-card.fv-reveal.is-in {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) var(--fvd-d) backwards,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  var(--fvd-d) backwards;
}

/* ════════════════════════════════════════════════════════════════
   3 · the quiet ending  ·  the closing rule  (CORE, C-4)
   ════════════════════════════════════════════════════════════════
   One chalk rule at the 200 grade, drawn by court-voice.js's shared hand
   (its <defs> are already on the page — the footer's .fv-link strokes pull
   them in). Fail-open: the mark rests DRAWN, and only the armed page hides
   it to draw it. */
.hi-close-rule {
  max-width: 1180px;
  /* clear of the closing aside's own hairline, which starts ~26px below */
  margin: 0 auto 48px;
  padding: 0 24px;
  color: var(--fives-line);
}
@media (min-width: 760px) { .hi-close-rule { padding: 0 40px; } }
.hi-close-rule .fv-ck { width: 200px; max-width: 60%; height: 12px; }
/* the rule carries .fv-reveal only for the `.is-in` hook — chalk is drawn,
   never faded, so the wrapper itself neither rises nor fades. */
html.fv-armed .hi-close-rule.fv-reveal { opacity: 1; transform: none; }
.hi-close-rule .fv-ck-p { stroke-dashoffset: 0; }
html.fv-armed .hi-close-rule .fv-ck-p { stroke-dashoffset: 104; }
html.fv-armed .hi-close-rule.is-in .fv-ck-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* ════════════════════════════════════════════════════════════════
   4 · the thread  ·  MODULE A  (descent.js)
   ════════════════════════════════════════════════════════════════ */

/* the static rail stands down ONLY once the module has armed itself */
html.fv-descent .hi-timeline::before { opacity: 0; }

.fvd-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.fvd-thread, .fvd-flick {
  position: absolute;
  pointer-events: none;
  z-index: 0;
  overflow: visible;
}
.fvd-thread > svg { display: block; width: 100%; height: 100%; overflow: visible; }

.fvd-p {
  fill: none;
  stroke: var(--fives-muted);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 100 200;
  stroke-dashoffset: 104;
  vector-effect: non-scaling-stroke;
}

/* PRIMARY BINDING — the CSS scroll-driven animation. The overlay div IS the
   thread's box, so its own view timeline expresses the 0.5vh lead exactly:
   p = 0 when the viewport bottom + 0.5vh reaches the thread top  -> cover -50vh
   p = 1 when it reaches the thread bottom -> cover calc(100% - 150vh).
   `linear` is licensed here and nowhere else: scroll progress IS reading
   position. The hand's character comes from the waver and the taper, not
   from a curve laid over the reader's own thumb. */
@supports (animation-timeline: view()) {
  html.fv-descent .fvd-thread { view-timeline-name: --fvdThread; view-timeline-axis: block; }
  html.fv-descent .fvd-thread-p {
    animation: fvd-draw-thread auto linear both;
    animation-timeline: --fvdThread;
    animation-range: cover -50vh cover calc(100% - 150vh);
  }
  /* monotonic guard: on the way back up the JS pins the mark at its
     high-water value rather than letting the line un-draw. */
  html.fv-descent .fvd-thread-p.is-pinned {
    animation-name: none;
    animation-timeline: none;
    stroke-dashoffset: var(--fvd-hw-off, 0);
  }
}
/* FALLBACK — the same math in a rAF-throttled scroll handler writing
   stroke-dashoffset directly. Also forced by ?descent=js. */
html.fv-descent-js .fvd-thread-p { animation-name: none; animation-timeline: none; }

/* the node strike: one 18px chalk mark laid THROUGH the node, under it in
   paint order, so the dot covers the middle and two accent ticks flank it. */
.fvd-tick {
  position: absolute;
  left: 38px; top: 44px;          /* node: left 38px, top 38px, 12px tall */
  width: 22px; height: 12px;
  transform: translate(-50%, -50%);
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}
@media (min-width: 880px) { .fvd-tick { left: 50%; top: 62px; } }
@media (max-width: 759px) { .fvd-tick { display: none; } }
.fvd-tick .fvd-p, .fvd-flick .fvd-p { stroke-width: 2; }
.fvd-tick .fvd-p { stroke: var(--fives-accent); }
html.fv-descent .fv-beat.is-in .fvd-tick .fvd-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* the Today flick — the thread's terminal segment. It starts exactly where
   the long path ends, so the two read as one stroke, and lifts off to the
   right of the cap's accent dot. */
html.fv-descent .hi-timeline-cap.is-in ~ .fvd-flick .fvd-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* ════════════════════════════════════════════════════════════════
   5 · reduced motion — the belt (descent.js carries the brace)
   ════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  html.fv-armed .fv-beat .hi-milestone-node,
  html.fv-armed .fv-beat.is-in .hi-milestone-node {
    animation: none !important;
    opacity: 1 !important;
    transform: translate(-50%, 0) !important;
  }
  html.fv-armed .fv-beat .hi-milestone-date,
  html.fv-armed .fv-beat .hi-milestone-card,
  html.fv-armed .fv-beat.is-in .hi-milestone-date,
  html.fv-armed .fv-beat.is-in .hi-milestone-card,
  html.fv-armed .hi-gallery .hi-shot-card.fv-reveal,
  html.fv-armed .hi-gallery .hi-shot-card.fv-reveal.is-in {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .fvd-p,
  html.fv-armed .hi-close-rule .fv-ck-p,
  html.fv-armed .hi-close-rule.is-in .fv-ck-p {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
  html.fv-descent .hi-timeline::before { opacity: 1 !important; }
}

/* ════════════════════════════════════════════════════════════════
   6 · /shot-vault — the catalogue that demonstrates itself  (M2)
   ════════════════════════════════════════════════════════════════
   Commission M2. The page's own defect was that all 31 card entrances
   fired at MOUNT, before anyone had scrolled, while ShotThumb drew each
   trajectory on its own scroll gate. This puts the two on one beat: the
   card rebounds in (the serve) and its trajectory draws as it lands (the
   return, +240ms — shotvault-app.jsx's BEAT_GAP).

   Why the cards carry `fv-beat` and not `fv-reveal`: tokens.css resolves a
   revealed `.fv-reveal` to `transform: none` at (0,3,1), which outranks
   `.sv-card:hover`'s raise at (0,2,0) — the exact pin constraint 8 forbids.
   `fv-beat` is reveal.js's other contract (same observer, same fast path)
   and carries no transform declaration at all, so the entrance is a
   `backwards`-filled animation that hands the card back to its own hover
   transition the moment it lands. */

.sv-grid .sv-card {
  --sv-cd: calc(min(var(--fv-i, 0), 5) * 60ms);   /* column ripple: 0/60/120 */
  --sv-fd: calc(min(var(--sv-n, 0), 8) * 30ms);   /* the approved reflow ripple */
}

/* INITIAL LOAD — scroll-revealed, hidden under `html.fv-armed` only, so a
   page that loses reveal.js shows all 31 cards statically. */
html.fv-armed .sv-grid .sv-card.fv-beat { opacity: 0; }
html.fv-armed .sv-grid .sv-card.fv-beat.is-in {
  opacity: 1;                                     /* the resting value the
                                                     `backwards` fill returns to */
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) var(--sv-cd) backwards,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  var(--sv-cd) backwards;
}

/* THE APPROVED FILTER REFLOW — untouched in shape (dip to .45 in
   shotvault.css, 30ms step capped at 8, `backwards` fill), upgraded to the
   v2 split: transform on rebound, opacity on settle. Remounted cards do NOT
   wait for a reveal — the reflow is the arrival — so this arm pins opacity
   whether or not `.is-in` has landed, and spells both cases with one
   identical declaration so a late `.is-in` cannot restart the animation. */
html.fv-armed .sv-grid-wrap.is-refiltered .sv-grid .sv-card.fv-beat,
html.fv-armed .sv-grid-wrap.is-refiltered .sv-grid .sv-card.fv-beat.is-in {
  opacity: 1;
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) var(--sv-fd) backwards,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  var(--sv-fd) backwards;
}

/* ── the section header as a beat ──
   Tier rebounds in, the count answers at +90ms, and the chalk underline is
   DRAWN (never faded) at the 120 grade — court-voice.js's own hand and mask,
   inlined by shotvault-app.jsx so it is in the header's first paint. */
.sv-grid-h { position: relative; }
html.fv-armed .sv-grid-h.fv-beat .sv-grid-h-title { opacity: 0; transform: translateY(16px); }
html.fv-armed .sv-grid-h.fv-beat .sv-grid-h-count { opacity: 0; }
html.fv-armed .sv-grid-h.fv-beat.is-in .sv-grid-h-title {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) both,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  both;
}
html.fv-armed .sv-grid-h.fv-beat.is-in .sv-grid-h-count {
  animation: fvd-fade var(--fives-t-gentle) var(--fives-e-settle) 90ms both;
}
/* absolutely positioned — the mark can never move the header (zero CLS) */
.sv-grid-h-rule {
  position: absolute; left: 0; bottom: 2px;
  width: 120px; max-width: 100%; height: 12px;
  color: var(--fives-accent);
  pointer-events: none;
}
.sv-grid-h-rule .fv-ck { width: 120px; max-width: 100%; height: 12px; }
.sv-grid-h .fv-ck-p { stroke-dashoffset: 0; }            /* fail-open: rests drawn */
html.fv-armed .sv-grid-h .fv-ck-p { stroke-dashoffset: 104; }
html.fv-armed .sv-grid-h.is-in .fv-ck-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* ── the quiet ending (C-4) ──
   One rule at the 200 grade above the CTA. The wrapper carries `.fv-reveal`
   for the `.is-in` hook only: chalk is drawn, never faded, so it neither
   rises nor fades. */
.sv-close-rule {
  max-width: 1240px;
  margin: 0 auto;
  padding: 0 24px;
  color: var(--fives-line);
}
@media (min-width: 760px) { .sv-close-rule { padding: 0 40px; } }
.sv-close-rule-mark { display: block; }
.sv-close-rule .fv-ck { width: 200px; max-width: 60%; height: 12px; }
html.fv-armed .sv-close-rule.fv-reveal { opacity: 1; transform: none; }
.sv-close-rule .fv-ck-p { stroke-dashoffset: 0; }
html.fv-armed .sv-close-rule .fv-ck-p { stroke-dashoffset: 104; }
html.fv-armed .sv-close-rule.is-in .fv-ck-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* ════════════════════════════════════════════════════════════════
   7 · reduced motion — the M2 belt
   ════════════════════════════════════════════════════════════════
   NOTE the cards do not get `transform: none !important` here: with the
   animation off there is no transform to cancel, and forcing one would pin
   the very property constraint 8 protects. */
@media (prefers-reduced-motion: reduce) {
  html.fv-armed .sv-grid .sv-card.fv-beat,
  html.fv-armed .sv-grid .sv-card.fv-beat.is-in,
  html.fv-armed .sv-grid-wrap.is-refiltered .sv-grid .sv-card.fv-beat,
  html.fv-armed .sv-grid-wrap.is-refiltered .sv-grid .sv-card.fv-beat.is-in {
    animation: none !important;
    opacity: 1 !important;
  }
  html.fv-armed .sv-grid-h.fv-beat .sv-grid-h-title,
  html.fv-armed .sv-grid-h.fv-beat .sv-grid-h-count,
  html.fv-armed .sv-grid-h.fv-beat.is-in .sv-grid-h-title,
  html.fv-armed .sv-grid-h.fv-beat.is-in .sv-grid-h-count {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* chalk is painted, not drawn — tokens.css's blanket `.fv-ck-p
     { animation: none }` would otherwise leave both marks invisible. */
  .sv-grid-h .fv-ck-p,
  html.fv-armed .sv-grid-h .fv-ck-p,
  html.fv-armed .sv-grid-h.is-in .fv-ck-p,
  .sv-close-rule .fv-ck-p,
  html.fv-armed .sv-close-rule .fv-ck-p,
  html.fv-armed .sv-close-rule.is-in .fv-ck-p {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
}

/* ════════════════════════════════════════════════════════════════
   8 · /clubs + /courts — the register, the gazetteer, the reshuffle  (M3)
   ════════════════════════════════════════════════════════════════
   Commission M3. C-1 holds: this is a motion layer over the two directory
   pages exactly as they are. No card, filter-UI, map, region-header or IA
   redesign — the only new DOM is three inert marks (a gutter thread, a chalk
   underline per group, one closing rule per page) and one aria-hidden span
   that rolls a number.

   Both pages load masthead.js, so the Arrival keeps its headroom: the FIRST
   content group of each page takes the plain register — it beats, but no
   chalk is struck. Strikes begin from the second group (the apps simply do
   not render a mark on group index 0).

   Why the cards carry `fv-beat` and not `fv-reveal`: .cd-club and .cl-court
   both declare their own `transform` hover, and tokens.css resolves a
   revealed `.fv-reveal` to `transform: none` at (0,3,1) — the specificity pin
   constraint 8 forbids. `fv-beat` is reveal.js's other contract (same
   observer, same fast path, no transform declaration), so the entrance runs
   as a `backwards`-filled animation and hands the card straight back to its
   own hover transition on landing. Same reasoning, same shape, as §6's
   .sv-card. */

@keyframes fvd-out { from { opacity: 1; } to { opacity: 0; } }

/* ── 8.1 · the group header as a beat  (both pages) ──────────────
   The region name rebounds in, the (n) count answers at +90ms, and the chalk
   underline is DRAWN at the 120 grade — court-voice.js's own hand and mask,
   inlined by the apps so the mark is in the header's first paint. The mark is
   absolutely positioned, so it can never move the header: zero CLS. */
.cd-group-h { position: relative; }
/* .cl-list-group-h is already `position: sticky`, which is a containing block
   for the mark — left alone on purpose (C-1: no layout changes). */

.cd-group-rule,
.cl-group-rule {
  position: absolute; left: 0; bottom: 2px;
  width: 120px; max-width: 100%; height: 12px;
  color: var(--fives-accent);
  pointer-events: none;
}
.cd-group-rule .fv-ck,
.cl-group-rule .fv-ck { width: 120px; max-width: 100%; height: 12px; }

/* fail-open: the mark RESTS drawn; only an armed page hides it to draw it */
.cd-group-h .fv-ck-p,
.cl-list-group-h .fv-ck-p { stroke-dashoffset: 0; }
html.fv-armed .cd-group-h .fv-ck-p,
html.fv-armed .cl-list-group-h .fv-ck-p { stroke-dashoffset: 104; }
html.fv-armed .cd-group-h.is-in .fv-ck-p,
html.fv-armed .cl-list-group.is-in .cl-list-group-h .fv-ck-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* /clubs — the beat host is the header itself */
html.fv-armed .cd-group-h.fv-beat h2          { opacity: 0; transform: translateY(16px); }
html.fv-armed .cd-group-h.fv-beat .cd-group-n { opacity: 0; }
html.fv-armed .cd-group-h.fv-beat.is-in h2 {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) both,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  both;
}
html.fv-armed .cd-group-h.fv-beat.is-in .cd-group-n {
  animation: fvd-fade var(--fives-t-gentle) var(--fives-e-settle) 90ms both;
}

/* /courts — the beat host is the GROUP, so the country label (shown above the
   first chapter of a split country) arrives on the same downbeat as the
   region name instead of as a separate event. */
html.fv-armed .cl-list-group.fv-beat .cl-list-country,
html.fv-armed .cl-list-group.fv-beat .cl-list-group-h h2 {
  opacity: 0; transform: translateY(16px);
}
html.fv-armed .cl-list-group.fv-beat .cl-list-group-h .cl-group-n { opacity: 0; }
html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-country,
html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-group-h h2 {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) both,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  both;
}
html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-group-h .cl-group-n {
  animation: fvd-fade var(--fives-t-gentle) var(--fives-e-settle) 90ms both;
}

/* ── 8.2 · the cards  (both pages) ───────────────────────────────
   House values: 60ms per index step (tokens.css's own), --fv-i set by the app
   to the index WITHIN the group mod 3 — a 3-column ripple on desktop, a
   natural cadence in one column on the phone. */
.cd-grid .cd-club,
.cl-list .cl-court { --fvd-cd: calc(min(var(--fv-i, 0), 5) * 60ms); }

html.fv-armed .cd-grid .cd-club.fv-beat,
html.fv-armed .cl-list .cl-court.fv-beat { opacity: 0; }
html.fv-armed .cd-grid .cd-club.fv-beat.is-in,
html.fv-armed .cl-list .cl-court.fv-beat.is-in {
  opacity: 1;                                    /* the value `backwards` returns to */
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) var(--fvd-cd) backwards,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  var(--fvd-cd) backwards;
}

/* ── 8.3 · the quiet endings  (C-4) ──────────────────────────────
   One rule at the 200 grade: on /clubs above "How Ask to play works", on
   /courts above the watchlist heading. The wrapper carries .fv-reveal for the
   `.is-in` hook ONLY — chalk is drawn, never faded, so it neither rises nor
   fades. */
.cd-close-rule,
.cl-close-rule {
  max-width: 1240px;
  margin: 0 auto;
  padding: 0 max(24px, var(--safe-left)) 0 max(24px, var(--safe-right));
  color: var(--fives-line);
}
@media (min-width: 760px) {
  .cd-close-rule,
  .cl-close-rule { padding: 0 max(40px, var(--safe-left)) 0 max(40px, var(--safe-right)); }
}
.cd-close-rule-mark,
.cl-close-rule-mark { display: block; }
.cd-close-rule .fv-ck,
.cl-close-rule .fv-ck { width: 200px; max-width: 60%; height: 12px; }
html.fv-armed .cd-close-rule.fv-reveal,
html.fv-armed .cl-close-rule.fv-reveal { opacity: 1; transform: none; }
.cd-close-rule .fv-ck-p,
.cl-close-rule .fv-ck-p { stroke-dashoffset: 0; }
html.fv-armed .cd-close-rule .fv-ck-p,
html.fv-armed .cl-close-rule .fv-ck-p { stroke-dashoffset: 104; }
html.fv-armed .cd-close-rule.is-in .fv-ck-p,
html.fv-armed .cl-close-rule.is-in .fv-ck-p {
  animation: fvd-draw var(--fives-t-standard) var(--fives-e-draw) both;
}

/* ── 8.4 · the marginal spine  ·  MODULE A on /clubs  (removable) ─
   descent.js draws it; everything here is keyed on `fv-descent`, which that
   module adds as its own first statement. Delete the <script> tag and the
   register loses a line and nothing else. It is furniture, one register below
   everything: 1.5px, muted, no taper mask, held back in opacity. Desktop only
   — descent.js hides it below 880px, where there is no gutter to live in. */
.cd-directory { position: relative; }
.fvd-thread--reg .fvd-p {
  stroke-width: 1.5;
  opacity: 0.5;
}
/* the boundary marks sit in the gutter beside each region name. The x offset
   is published by descent.js (the header is in the content box, the thread in
   the padding box), and the draw hook is §4's shared
   `.fv-beat.is-in .fvd-tick` rule — no new rule needed. */
html.fv-descent .cd-group-h .fvd-tick--reg {
  display: none;
  left: var(--fvd-reg-x, -20px);
  top: 12px;
}
@media (min-width: 880px) {
  html.fv-descent .cd-group-h .fvd-tick--reg { display: block; }
}

/* ── 8.5 · the reshuffle  ·  MODULE B  (reshuffle.js, removable) ──
   Everything below is keyed on a class reshuffle.js adds. Delete that file
   and its <script> tag and the filter swap is today's instant cut again.

   The per-card view-transition-name is bound HERE, not inline in the app, and
   only while a same-document swap is in flight. Both pages also run the
   CROSS-document trial (`@view-transition { navigation: auto }` in their own
   stylesheets); a permanently-named card would be lifted out of that
   navigation's `root` snapshot and captured unpaired, which is exactly what
   the C-5 checkpoint must not be shown. The name itself is inline and stable
   per id — `--fv-vt: club-<id>` / `court-<id>`. */
html.fv-reshuffling .cd-grid .cd-club,
html.fv-reshuffling .cl-list .cl-court { view-transition-name: var(--fv-vt); }

/* Once the reader has filtered, the arrival IS the entrance: cards no longer
   wait on a scroll reveal (same ruling as §6's .sv-grid-wrap.is-refiltered).
   Opacity only — never `transform: none`, which would re-create the very pin
   constraint 8 protects on these two cards. */
html.fv-armed.fv-reshuffled .cd-grid .cd-club.fv-beat,
html.fv-armed.fv-reshuffled .cd-grid .cd-club.fv-beat.is-in,
html.fv-armed.fv-reshuffled .cl-list .cl-court.fv-beat,
html.fv-armed.fv-reshuffled .cl-list .cl-court.fv-beat.is-in {
  opacity: 1;
  animation: none;
}
html.fv-armed.fv-reshuffled .cd-group-h.fv-beat h2,
html.fv-armed.fv-reshuffled .cd-group-h.fv-beat .cd-group-n,
html.fv-armed.fv-reshuffled .cl-list-group.fv-beat .cl-list-country,
html.fv-armed.fv-reshuffled .cl-list-group.fv-beat .cl-list-group-h h2,
html.fv-armed.fv-reshuffled .cl-list-group.fv-beat .cl-list-group-h .cl-group-n {
  opacity: 1;
  transform: none;
  animation: none;
}
html.fv-armed.fv-reshuffled .cd-group-h .fv-ck-p,
html.fv-armed.fv-reshuffled .cl-list-group-h .fv-ck-p { stroke-dashoffset: 0; }

/* THE FALLBACK RIPPLE — no startViewTransition (or ?reshuffle=fallback).
   Applied instantly, then the house remount entrance: 30ms per card capped at
   8, transform on rebound, opacity on settle, `backwards` fill. Same
   specificity as the pin above and deliberately AFTER it, so it wins for the
   length of the swap and hands back cleanly. */
.cd-grid .cd-club,
.cl-list .cl-court { --fvd-fd: calc(min(var(--fv-n, 0), 8) * 30ms); }
html.fv-armed.fv-reshuffle-fb .cd-grid .cd-club.fv-beat,
html.fv-armed.fv-reshuffle-fb .cd-grid .cd-club.fv-beat.is-in,
html.fv-armed.fv-reshuffle-fb .cl-list .cl-court.fv-beat,
html.fv-armed.fv-reshuffle-fb .cl-list .cl-court.fv-beat.is-in {
  opacity: 1;
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) var(--fvd-fd) backwards,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  var(--fvd-fd) backwards;
}

/* THE VIEW-TRANSITION CHOREOGRAPHY (spec §3).
   Scoped to `html.fv-reshuffling` throughout, so none of it can reach the
   cross-document navigation between these two pages — that transition names
   only `root` and keeps the duration/easing its own stylesheet already sets.

   `:only-child` is what separates an unpaired snapshot from a survivor: a
   surviving card has BOTH an -old and a -new in its group, an arrival has
   only a -new, a departure only an -old. */
html.fv-reshuffling::view-transition-group(*) {
  animation-duration: var(--fives-t-standard);
  animation-timing-function: var(--fives-e-settle);
}
/* departures — the light goes out where it stands, no travel */
html.fv-reshuffling::view-transition-old(*):only-child {
  animation: fvd-out var(--fives-t-quick) var(--fives-e-settle) both;
}
/* arrivals — 16px rise on rebound, light on settle. Uniform, NOT staggered:
   the ::view-transition tree is rooted on the document, so a card's own
   --fv-i cannot reach its pseudo-element, and per-name delays would mean
   generating up to 88 rules per swap. Noted in the digest. */
html.fv-reshuffling::view-transition-new(*):only-child {
  animation: fvd-rise var(--fives-t-gentle) var(--fives-e-rebound) both,
             fvd-fade var(--fives-t-gentle) var(--fives-e-settle)  both;
}

/* the rolling count's aria-hidden twin. The real number is the .sr-only span
   beside it, which React sets atomically — this one only has to look right. */
.fv-roll { font-variant-numeric: tabular-nums; }

/* ════════════════════════════════════════════════════════════════
   9 · reduced motion — the M3 belt
   ════════════════════════════════════════════════════════════════
   reshuffle.js carries the brace (it reads the preference per call and takes
   the plain path), descent.js carries the thread's. Note the cards get no
   `transform: none !important`: with the animation off there is no transform
   to cancel, and forcing one would pin the property constraint 8 protects. */
@media (prefers-reduced-motion: reduce) {
  html.fv-armed .cd-grid .cd-club.fv-beat,
  html.fv-armed .cd-grid .cd-club.fv-beat.is-in,
  html.fv-armed .cl-list .cl-court.fv-beat,
  html.fv-armed .cl-list .cl-court.fv-beat.is-in,
  html.fv-armed.fv-reshuffle-fb .cd-grid .cd-club.fv-beat,
  html.fv-armed.fv-reshuffle-fb .cd-grid .cd-club.fv-beat.is-in,
  html.fv-armed.fv-reshuffle-fb .cl-list .cl-court.fv-beat,
  html.fv-armed.fv-reshuffle-fb .cl-list .cl-court.fv-beat.is-in {
    animation: none !important;
    opacity: 1 !important;
  }
  html.fv-armed .cd-group-h.fv-beat h2,
  html.fv-armed .cd-group-h.fv-beat .cd-group-n,
  html.fv-armed .cd-group-h.fv-beat.is-in h2,
  html.fv-armed .cd-group-h.fv-beat.is-in .cd-group-n,
  html.fv-armed .cl-list-group.fv-beat .cl-list-country,
  html.fv-armed .cl-list-group.fv-beat .cl-list-group-h h2,
  html.fv-armed .cl-list-group.fv-beat .cl-list-group-h .cl-group-n,
  html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-country,
  html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-group-h h2,
  html.fv-armed .cl-list-group.fv-beat.is-in .cl-list-group-h .cl-group-n {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* chalk is painted, not drawn — tokens.css's blanket `.fv-ck-p
     { animation: none }` would otherwise leave every mark invisible. */
  .cd-group-h .fv-ck-p,
  .cl-list-group-h .fv-ck-p,
  .cd-close-rule .fv-ck-p,
  .cl-close-rule .fv-ck-p,
  html.fv-armed .cd-group-h .fv-ck-p,
  html.fv-armed .cl-list-group-h .fv-ck-p,
  html.fv-armed .cd-group-h.is-in .fv-ck-p,
  html.fv-armed .cl-list-group.is-in .cl-list-group-h .fv-ck-p,
  html.fv-armed .cd-close-rule .fv-ck-p,
  html.fv-armed .cl-close-rule .fv-ck-p,
  html.fv-armed .cd-close-rule.is-in .fv-ck-p,
  html.fv-armed .cl-close-rule.is-in .fv-ck-p {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
  /* the ::view-transition tree is out of reach of tokens.css's blanket `*`
     rule, so the choreography is declined outright here as well as in JS. */
  html.fv-reshuffling::view-transition-group(*),
  html.fv-reshuffling::view-transition-old(*),
  html.fv-reshuffling::view-transition-new(*) { animation: none !important; }
}
