/* =====================================================================
   הח״כ ה-121 — prototype, s1 round.
   Sits ON TOP of ../prototype/hachach.css. Everything here is either
   (a) screen-specific layout the handoff stylesheet deliberately does
   not carry, (b) one of the two motion tokens the brief supersedes, or
   (c) a documented workaround for a defect in the handoff file.
   No colour is declared here that is not already a token.
   ===================================================================== */

@font-face{
  font-family:'SimplerPro';
  src:url('fonts/SimplerPro_HLAR-Black.woff2') format('woff2');
  font-weight:900; font-style:normal; font-display:block;
}
/* 13b · THE REGULAR IS DECLARED AND LOADED, AND NOTHING IS WIRED TO IT YET.
   Until now the family shipped one face, so every `font-weight` under 900
   that named SimplerPro was answered by the Black — there was no lighter
   face to pick. There is now, at 400.
   THIS IS INERT ON EVERY EXISTING SCREEN, and that is checked rather than
   assumed: every rule in this file and in hachach.css that reads
   --font-display sets font-weight:900 in the same declaration, so nothing
   currently resolves to the new face. Body copy is untouched for a
   different reason — --font-ui does not name SimplerPro at all, it is
   system-ui — which is the finding item 13 reports.
   font-display:swap, not the Black's `block`: the Black carries the title
   and a swap there would flash a system face through the display type,
   but nothing is set in this face yet, so swap costs nothing and is the
   safer default for the body copy it exists to become. */
@font-face{
  font-family:'SimplerPro';
  src:url('fonts/SimplerPro_HLAR-Regular.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
}

/* ---------------------------------------------------------------------
   THE TWO OVERRIDDEN TOKENS, and why.
   hachach.css ships --t-flip:260ms and --t-finale:620ms. Both are below
   the figures the brief calls MEASURED: the flip wants 300-500ms (the
   Doherty band, sheet §1.4) and the finale wants ~850ms (Balatro weight,
   sheet §8b [M]). Overridden here, in one place, so the handoff file
   stays the handoff file.
   --------------------------------------------------------------------- */
:root{
  /* ---- 21 · THE BODY TOKEN, AND WHY IT IS NOT --font-ui ---------------
     --font-ui is not a body-prose token. hachach.css defines it once and
     this file's very first rule is `body{ font-family:var(--font-ui) }`,
     so it is the app's DEFAULT face — on the intro alone 25 of 35 painted
     elements resolve through it, and its other readers are a text input
     (.prof-field), a chip (.gchip), a sticker label (.bopt-lbl) and the
     share card. Repointing it at SimplerPro would have re-set the whole
     app, so item 21's second branch applies: a separate token, wired to
     the named elements only.
     The system stack is kept behind SimplerPro exactly as it was, so a
     failed font load degrades to today's rendering rather than to serif. */
  --font-body: 'SimplerPro', system-ui, -apple-system, 'Helvetica Neue', sans-serif;

  /* ---- 22 · THE BODY INK, LIFTED OFF --ink-faint ----------------------
     #BEB9AC measured 4.41 against the lightest dot of the grid (#4D4C48),
     under the 4.5 AA floor for body text. This is +3 per channel — the
     smallest lift that clears it with a margin rather than by 0.007.
     It is a NEW token rather than a change to --ink-faint, because
     --ink-faint has seven readers and three of them (.b5-a2 .b5count,
     .b5link.is-missing, .eg-stat__sub) are not in item 21's set —
     .eg-stat__sub sits on a card, where the ground is not the dot grid
     and the ratio was never in question. */
  --ink-body: #C1BCAF;

  --t-flip:   380ms;   /* was 260ms — mid of the measured 300-500 band   */
  --t-finale: 850ms;   /* was 620ms — the round's one held beat          */

  /* ---- DERIVED. Baselines from the brief, tuned by hand on device. --- */
  /* §1.2 THE ANSWER-FIRST HOLD. The brief's derived baseline was
     250-300ms. On device that reads as latency, not as a held beat: the
     player's choice does not get time to sit alone before the truth
     arrives. --t-hold-long is the tuned figure; the switch in the spike
     bar plays both. The FLIP is untouched at 380ms — lengthening the
     hold does not touch the cascade-vs-finale contrast, because the hold
     is the pause BEFORE the verdict, not the verdict's own duration. */
  --t-hold-short:  275ms;   /* the brief's derived baseline              */
  --t-hold-long:   640ms;   /* tuned: the choice reads as HELD           */
  --t-hold:        var(--t-hold-long);

  --t-draw:        225ms;  /* pile -> front, ease-out, deck not slide    */
  --t-exit:        180ms;  /* the spent card leaving                     */
  --t-ov-in:       260ms;  /* beat-2 overlay, blur ramps with it         */
  --t-ov-collapse: 220ms;  /* beat-3 dismiss, INTO the card beneath      */
  --t-snapback:    160ms;  /* release below the drag threshold           */
  --t-resolve:     450ms;  /* the pinned claim resolving. A moment.      */
  --t-coin:        400ms;  /* one coin award counting up                 */

  /* §D THE OVERLAY CONTENT SWAP. This token went missing last pass — the
     edit that was meant to add it silently matched nothing — so the
     shorthand `transition:transform var(--t-ov-swap) var(--e-settle)`
     resolved with NO duration, which makes the whole declaration invalid
     and leaves transition-duration at its 0s initial value. T.ovSwap read
     the same empty token and became 0ms, so the JS wait was zero too.
     Both halves were instant and the class toggle I checked looked fine. */
  --t-ov-swap:     360ms;
  /* §4 the chosen vote landing on the chair. Long enough to read as an
     object arriving and settling, short enough that the hold after it is
     still the beat's pause rather than a wait. */
  /* B2-4 · THE SLAP IS ITS OWN BEAT. --t-ask-delay is measured from the
     moment the card is in place, and it is the number the brief asked to
     be tuned. The card turn is --t-card-flip 450ms on --e-settle, which
     is asymptotic: the card is visually stopped at about 390ms and the
     last 60ms is sub-pixel. 480ms puts the slap ~90ms after the eye has
     read the card as arrived — clearly a second event, still inside the
     window where the eye is on that corner. It is at the low end of the
     brief's 400-600ms band on purpose: at 600 the pause reads as a
     hesitation rather than as a beat, and the cascade's per-card tempo is
     ~850ms end to end. (Lion's 5ms is a third of a frame at 60Hz — the
     slap and the card would arrive on the same paint.) */
  --t-ask-delay:   480ms;
  --t-ask-in:      300ms;
  --t-b2-seat:     520ms;

  /* §1 · THE LOADING BEAT, and the whole thing is budgeted to land under
     2.5s door to door: 260 fade + 200 bar-in + 1600 fill + 180 hold =
     2240ms from the tap to the destination. The chair's travel runs
     inside the fade rather than after it — the screen empties and the
     object grows as one movement, not two. */
  --t-load-fade:   260ms;
  --t-load-rise:   520ms;
  --t-load-barin:  200ms;
  --t-load-fill:  1600ms;
  --t-load-hold:   180ms;

  /* §1b · THE LOADING SCREEN'S EXIT. The chair rushes the camera and the
     destination is already underneath when it dissolves.
       0   -> 150   the bar fades
       150 -> 350   the chair holds alone
       350 -> 700   the chair launches, 1x -> 10x, ease-IN
       525 -> 700   ... its second half, over which it fades to nothing
       350 -> 530   the destination fades up from scale 1.04
     700ms bar-full to settled. Runs once per load.

     THE FADE STARTS AT THE HALFWAY POINT AND ENDS WITH THE SCALE, and
     that is the correction. At 60% the chair had already filled the
     viewport at full opacity and sat there ~35ms before anything
     happened to it: a flat opaque frame, then a dissolve, which reads
     as two events rather than one. The cover and the fade now begin on
     the same frame — 50% of an ease-in zoom is where the chair passes
     full-screen — so there is never a moment of solid chair holding
     still. 350ms rather than 420 for the same reason: the halves have
     to feel like one gesture.
     The destination's fade drops to 180ms so it is fully up long before
     the chair goes translucent. That ordering is the assertion now. */
  --t-lx-bar:       150ms;
  --t-lx-hold:      200ms;
  --t-lx-zoom:      350ms;
  --t-lx-fade-at:   175ms;   /* 50% of the zoom */
  --t-lx-fade:      175ms;   /* and it ends when the zoom does */
  --t-lx-dest:      180ms;

  /* §S-1 · THE FINALE'S CHRONOLOGY. The whole beat is these eight
     numbers, and the order they compose is the design:
       r1  0.0 count -> 2.4 settled -> 2.8 flight -> 3.5 landed
           -> 3.9 coins -> 4.7 reading + buttons
       e3  0.0 outcome -> 1.2 flight -> 1.9 landed -> 2.3 coins
           -> 3.1 reading + buttons
     --t-f5-flare is INSIDE --t-f5-count, not added to it: the count
     stops dead for 400ms on the frame 61 is crossed and the run is
     shortened to match, so the settle still lands at 2.4s. */
  --t-f5-count:    2400ms;
  --t-f5-flare:     400ms;
  --t-f5-prose:     800ms;   /* the twin's peak, before the flight     */
  --t-f5-gap:       400ms;   /* between two beats of the sequence      */
  --t-f5-flight:    700ms;
  --t-f5-coin-hold: 500ms;   /* the sticker alone before it flies      */
  --t-f5-coin-out:  300ms;
  --t-f5-in:        300ms;   /* a block arriving                       */
  --t-f5-bnr-out:   250ms;   /* the emptied banner leaving              */
  --t-f5-recentre:  400ms;   /* the settled stack's one move            */
  /* ITEM 8 · THE BOARD'S MOVE IS ITS OWN NUMBER NOW. It used to be the
     stack's re-centre at 400ms, fired after the panels had already
     landed; the board is the object that moves and the panels follow it,
     so the duration is named for the board and --t-f5-recentre is left
     describing nothing else that runs. */
  --t-f5-board:     320ms;   /* the board rises to its final position   */
  --t-f5-panel:      80ms;   /* one step of the panels' stagger         */
  /* ITEM 11 · the 121st vote, in the order it happens. */
  --t-f5-hold:      500ms;   /* a · the record, alone and readable      */
  --t-f5-tick-at:   120ms;   /* c · after the token has settled         */
  --t-f5-tick:      220ms;   /* d · the +1, the colour and the bar      */

  /* §T · THE TACHLES -> CASCADE TRANSITION. One continuous move made of
     six overlapping steps; the offsets are when each STARTS, measured
     from the tap, and the durations are how long each takes. They
     overlap on purpose — the pair assembles while the letters are still
     travelling — so the whole thing reads as one gesture and not as six.
       0    -> 340   letters leave the button
       180  -> 480   the avatar joins them
       480  -> 740   the callout resolves into the banner
       740  -> 1120  the banner travels to its pin   <- the one to follow
       900  -> 1300  the next screen enters behind
       1300          the tap affordance appears
     Total 1720ms. */
  --t-tc-letters:    340ms;
  --t-tc-av-at:      180ms;
  --t-tc-av:         300ms;
  --t-tc-resolve-at: 480ms;
  --t-tc-resolve:    260ms;
  --t-tc-travel-at:  740ms;
  --t-tc-travel:     380ms;
  --t-tc-next-at:    900ms;
  --t-tc-next:       400ms;
  --t-tc-tap-at:    1300ms;
  --t-tc-end:       1720ms;

  /* A6 · how long the claim's stamp holds alone before the explanation
     arrives under it. Long enough to read one word and register the
     colour, short enough that it is a beat and not a wait. */
  --t-claim-hold:  700ms;
  /* §1.1 step 2 · THE BEAT BETWEEN COMMIT AND STAMP. The answer is
     registered and nothing moves. It is the pause that lets the commit
     land as its own event before the verdict arrives on top of it — with
     no gap the stamp reads as part of the tap. */
  --t-claim-beat: 400ms;
  /* ITEM 2 · the claim's reflow at the answer, played rather than snapped.
     260ms sits inside --t-claim-beat's 400ms hold, so the move has landed
     before claimReveal() measures the claim's box for the panel cap. */
  --t-claim-lift: 260ms;
  /* §6c · the seat fill. 2.15s of travel plus a 340ms stop on the seat
     that crosses 61 — about 2.5s end to end, at the top of the brief's
     1.8-2.5s window because the majority beat is time the eye spends on
     one square rather than on the fill. */
  --t-seat-fill:  2150ms;
  --t-seat-cross:  340ms;
  /* §0 · the two gaps that separate the reveal's three movements. Unequal
     on purpose: an even rhythm reads as a loading sequence, an uneven one
     reads as staging. */
  /* ITEM 47B · 120 -> 140ms. The stamp lands ALONE and is held before the
     verdict arrives: the stamp is the Knesset's answer, the pill is
     whether the player called it, and the gap is what makes it cause then
     consequence rather than one compound arrival. */
  --t-mark-gap:  140ms;
  --t-panel-gap: 160ms;
  /* A-1 · THE PEEL. Three phases, all CSS.
     --t-peel      the sheet lifting and curling off the party line. 620ms
                   is slow enough to read as a physical action and short
                   enough that a player peeling five cards in a cascade is
                   not waiting on it; the ease starts fast and settles,
                   which is how a stuck edge releases.
     --t-peel-out  the discard. Shorter than the peel and on an
                   ACCELERATING curve — the peel is something the player
                   does, the fall is something gravity does.
     --t-peel-land the party line's settle, run against the discard so the
                   beat belongs to the reveal rather than to the exit. */
  --t-peel:      620ms;
  --t-peel-out:  460ms;
  --t-peel-land: 240ms;
  /* the first curve was cubic-bezier(.32,.72,.36,1) and it was wrong for
     this: front-loaded, so 27% of the duration had already peeled ~70% of
     the sheet and the lift read as a snap. A peel unsticks slowly, runs
     steady, then releases — slow in, slow out, nearly linear between. */
  --e-peel:      cubic-bezier(.55,.08,.35,1);
  --e-fall:      cubic-bezier(.42,0,.86,.62);

  /* §A the deck. A card is turned over, never swapped: --t-card-flip is
     the turn, --t-card-exit the resolved card being swiped off. */
  --t-card-flip:   450ms;
  --t-card-exit:   320ms;

  /* ITEM 7 · CONTACT ON AN MK CARD. hachach.css's --t-stamp-drop (190ms)
     still describes the CLAIM stamp's fall, which this item does not
     touch; the cascade's fall is now 200ms and everything keyed to the
     moment of contact — the jolt, the 25ms buzz, the ink rupture — reads
     this instead. The code's own rule is that the buzz and the hit are
     one event or neither, so they cannot be allowed to drift apart. */
  --t-stamp-drop-mk: 200ms;

  /* ITEM 47 · THE LANDING IS ONE MOTION AND ONE DURATION. Both stamps —
     the claim's at 156px and the MK card's at 190px — run the same
     keyframes for the same 360ms. Only the box differs. */
  --t-stamp-land:    360ms;

  /* §F the strip, played rather than stated: lock -> hold -> the MK's
     token appears in the PLAYER'S slot -> the fill travels to the truth,
     distance-proportional on one easing -> the stamp lands after it. */
  --t-gx-lock:     200ms;
  --t-gx-hold:     350ms;
  --t-gx-appear:   140ms;
  --t-gx-travel-1: 420ms;   /* one slot of disagreement                 */
  --t-gx-travel-2: 650ms;   /* two — felt as bigger, not just longer    */
  --t-gx-settle:   220ms;   /* agreement: nowhere to travel             */
  --t-gx-stamp-lag: 80ms;

  --e-in:          cubic-bezier(.55,.06,.9,.5);   /* leaving, not arriving */
  --e-out:         cubic-bezier(.16,.84,.44,1);

  --gate-hint:  4000ms;    /* §1.6 idle -> instruction line appears      */
  --gate-grow:  8000ms;    /* §1.6 idle -> it grows. NEVER an error.     */

  --card-scale: 1;         /* set from JS so a 620px card fits a short
                              phone without ever introducing a scroll    */

  /* the guess-vs-reality strip, inside the card at its foot. 88px before
     the caption row came out. */
  --gx-h: 71px;
  --gx-bottom: 8px;        /* the strip's inset off the card's foot        */

  /* ---- the map ----------------------------------------------------
     The disc and its extrusion are the v16 board's own numbers, off the
     NODE and PathMap artboards. THE RING IS NOT, any more, and this is the
     one measurement in the file that departs from the board — see the
     block by .node-ico for the arithmetic that forced it. */
/* EVERY RENDERED PART IS 15% LARGER THAN THE BOARD'S. The previous attempt
     grew --node-box 116 -> 132 and left the disc at 76 and the depth at 7, so
     the bounding box moved and the node did not. These are the parts that
     actually draw, and all of them moved together:
       disc          76   -> 87px      depth        7  -> 8px
       ring radius   63   -> 59.5px    ring stroke  6  -> 10px   (see 1.3)
       icon average  44   -> 51px      badges       21 -> 24px
       label         14   -> 16px      status       11 -> 13px
     The ring radius went DOWN while everything else went up, and that is
     1.4 rather than a contradiction: the cream tile and the 14px overhang
     are gone, so the clearance that forced r=63 is gone with them. The ring
     is back to hugging the face+base composite by the board's own margin,
     scaled 15% like everything else — 6px of clearance becomes 7px. */
  --node-box:      130px;   /* .ringnode, the whole node incl. the ring   */
  --node-box-h:    130px;
  --node-face:     87px;    /* the pressable top face — board 76 x 1.15   */
  --node-depth:    8px;     /* its extrusion. 9.2% of the face — the
                               brief's "~10% of diameter". Board.         */
  --node-face-x:   21.5px;  /* (130 - 87) / 2                             */
  --node-face-y:   17.5px;  /* centres face+base on the ring at cy 65     */
  /* THE RING. Inner edge = the composite's half-height (87+8)/2 = 47.5,
     plus the board's 6px clearance scaled to 7 = 54.5. Half the stroke on
     top of that is the centreline: 54.5 + 5 = 59.5. */
  --ring-r:        59.5px;
  /* §1.3 A DELIBERATE HEAVY ARC, not a hairline. 6px was 7.9% of the
     board's disc; 15% of it would be 6.9px and would read exactly as thin
     as before against a 15% larger node. 10px is 11.5% of the 87px disc
     and is the weight the segments need to hold a 360px screen. It is also
     what makes the ring read as a track with part of it filled rather than
     as two arcs near a disc. */
  --ring-w:        10px;
  /* the icon's larger dimension, AVERAGED over the eight. Each one is
     this times its own manifest node_scale, so retuning the set is one
     number here — see the node_mass note in tools/make_manifest.py.
     51/87 = 58.6% of the disc, the same proportion 44/76 was. */
  --node-ico-avg:  51px;
  --node-gap:      199px;   /* centre to centre, up the path              */
  /* §1.5 THESE TWO PADS ARE THE BLEED. The ribbon is drawn the full height
     of the path now, not from the first node to the last, so what used to
     be empty charcoal above the top node and below the bottom one is where
     the road runs off the screen. --node-pad-top is 110 rather than 84 so
     that 45px of ribbon shows clear of the top node's ring before the
     edge; the bottom already had 132. */
  --node-pad-top:  110px;
  --node-pad-bot:  132px;   /* the name and status under the last node    */

  /* ---- motion the map and the award add ----------------------------- */
  --t-node-press:  80ms;    /* board: transform+box-shadow, linear        */
  --t-screen:      300ms;   /* map -> round. Under the brief's 350ms cap. */
  --t-coin-fly:    450ms;   /* one token, spawn to chip                   */
  --t-coin-stagger: 40ms;   /* between tokens                            */
  /* ITEM 14 · the ALLOCATION's flight, which is a different trip from the
     award's above: a short hop from the balance line to a topic row
     rather than a long haul to the HUD chip, so it gets its own four
     numbers rather than being folded into those two. */
  --t-eg-fly:      420ms;   /* one token, balance line to topic row       */
  --t-eg-fly-step:  25ms;   /* between tokens                             */
  --t-eg-fly-fade: 100ms;   /*   ... of which this is the fade, at the end */
  --t-eg-pulse:    200ms;   /* the balance's own acknowledgement           */
  --t-map-in:      260ms;   /* the map settling under the round leaving   */
}
:root[data-hold="short"]{ --t-hold: var(--t-hold-short); }

*{ box-sizing:border-box; }

/* =====================================================================
   THE STAGE IS FIXED. NO SCROLL, EITHER AXIS, ON ANY BEAT.
   The app is a fixed stage, not a document. html and body are pinned to
   the visual viewport and cannot scroll, rubber-band or overscroll; the
   stage is sized in DYNAMIC viewport units so it follows Safari's chrome
   collapsing instead of assuming a height. Exactly two surfaces in the
   whole app may scroll — the path map and character personalisation —
   and they opt in with .scrolls, which is also the only place
   touch-action is handed back.
   ===================================================================== */
html, body{
  margin:0; padding:0;
  position:fixed; inset:0;
  width:100%; height:100%;
  overflow:hidden;
  overscroll-behavior:none;
  touch-action:none;                 /* the body never pans. .scrolls opts back in. */
  -webkit-text-size-adjust:100%;
}
body{
  background:#6E6C68; color:var(--ink);
  font-family:var(--font-ui); font-variant-numeric:tabular-nums;
  -webkit-font-smoothing:antialiased;
  display:flex; justify-content:center;
}
/* the only two surfaces that may ever scroll */
.scrolls{ overflow-y:auto; touch-action:pan-y; overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch; }

.defs{ position:absolute; width:0; height:0; }

.stage{
  position:relative; width:100%; max-width:390px;
  /* --vh is set from window.innerHeight for Safari versions without dvh;
     dvh wins wherever it exists. Either way the stage IS the viewport. */
  height:calc(var(--vh, 1vh) * 100);
  overflow:hidden; isolation:isolate;
  display:flex; flex-direction:column;
  padding:calc(10px + env(safe-area-inset-top))
          calc(16px + env(safe-area-inset-right))
          calc(12px + env(safe-area-inset-bottom))
          calc(16px + env(safe-area-inset-left));
  background:#403E3A;
  box-shadow:0 3px 20px rgba(0,0,0,.4);
}
/* §3.2 · THE MAP GETS A GROUND OF ITS OWN, warm and energetic. It is on
   .stage rather than on .sc-map so it covers the whole viewport including
   the strips under the HUD and below the jump button — see §5.3. The
   round and the intro are untouched: only [data-screen="map"] matches.
   THE DOT GRID CARRIES OVER IT. .stage::before is inset:0 above this and
   is not touched, so the same 4px/7px two-layer grid the round stands on
   runs across the gradient — which is what keeps the two screens the same
   product. Its dark layer does most of the work on a light ground. */
.stage[data-screen="map"]{
  background:
    radial-gradient(120% 80% at 50% 108%, #F2B33C 0%, #E4732B 38%, #C4402E 70%, #8E2A2E 100%),
    #C4402E;
}
@supports (height: 100dvh){ .stage{ height:100dvh; } }
/* §5.3, SECOND CAUSE. max-width:390px is a DESKTOP PREVIEW FRAME — it
   keeps the prototype phone-shaped in a browser window — but it applies on
   real phones too, so a 430px device ran the app at 390 with 20px of body
   grey down each side. That is the same "does not fill the viewport"
   symptom on the other axis, and it affects every screen, not just the
   map. On anything phone-sized the cap comes off; above 480px the frame
   stays, because there it is doing the job it was written for. */
@media (max-width: 480px){
  .stage{ max-width:none; }
}

.stage::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background-image:
    radial-gradient(rgba(255,255,255,.07) .5px,transparent .6px),
    radial-gradient(rgba(0,0,0,.34) .5px,transparent .6px);
  background-size:4px 4px,7px 7px; background-position:0 0,2px 3px;
}

/* =====================================================================
   HUD. Coins left, avatar right. Sits ABOVE the beat-2 overlay so the
   coin count stays readable while the round is blurred out.
   ===================================================================== */
.hud{
  position:relative; z-index:12; flex:none;
  display:flex; align-items:center; gap:var(--sp-2);
  /* §3c the chyron sits 12px under the HUD, not 32. 8px of HUD padding
     plus a 22px helper box was making that gap the widest space on the
     screen. The helper still reserves its box on every beat — the card
     cannot change size — it is just the size of its own line now. */
  padding-bottom:0;
}
/* §B the topic title. flex:1 so it centres between the chip and the
   avatar whatever their widths, and truncates rather than wrapping the
   HUD onto a second line. */
/* ITEM 28 · VARIANT C, ported from explorations/v22/hud-title.html.
   The round's title stops being bare 15px ink on the dot grid and becomes
   a white pill with a leading art slot.

   GEOMETRY COMES FROM THE COIN PILL AS IT MEASURES IN THIS FILE, not from
   the board's --pill-h:35px. The board redefines .h-a for its own page;
   here .h-a is 36px tall (5px + a 17px line box + 6px), radius 999px —
   see the §3 note below, this file already matches the shipped app's coin
   chip — with a 2px keyline at .55 and a 3px extrusion. All four are
   restated here rather than inherited, because .h-a's height is a
   CONSEQUENCE of 17px type and this pill carries 15px type: matching by
   padding would have made it shorter, and the brief forbids resizing the
   title. So the height is explicit and the other three are copied.

   flex:0 1 auto, not flex:1 — the pill hugs its content and shrinks only
   when .hud-mid runs out of room, so a short title gets a short pill
   instead of a full-width bar with the text floating in the middle. */
.hud-topic{
  flex:0 1 auto; min-width:0; max-width:100%;
  display:inline-flex; align-items:center; gap:8px;
  height:36px;
  padding-inline:7px 13px;
  border-radius:999px;
  background:var(--paper); color:var(--ink);
  font-family:var(--font-display); font-weight:900; font-size:15px; line-height:1;
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
}
/* the TITLE is what truncates, not the pill: the pill never grows past
   the room .hud-mid gives it, the type never shrinks and never wraps.
   A long title losing its tail is the accepted cost of variant C. */
.hud-topic__t{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* ITEM 28 · THE ART SLOT, placeholder until the real per-issue art lands.
   It is the die-cut treatment the rest of the system uses — charcoal
   fill, a 2px white edge, the keyline outside it — at the board's 20x20.
   THE HOOK IS data-issue-icon AND IT CARRIES THE ISSUE ID. A per-issue
   rule can drop art in later without touching the markup or this rule:
       .hud-topic__slot[data-issue-icon="r1"]{ background-image:url(...) }
   and every issue without one keeps this shared fallback. background is
   written as a shorthand with center/cover so such a rule only has to
   supply the image. */
.hud-topic__slot{
  flex:none; width:20px; height:20px; border-radius:4px;
  background:#2B2926 center/cover no-repeat;
  border:2px solid var(--edge);
  box-shadow:0 0 0 1.5px var(--keyline);
}
.hud-coins{ direction:ltr; }
.hud-coins .coin-glyph{ color:var(--primary); font-size:13px; line-height:1; }
.hud-coins .num{ direction:ltr; unicode-bidi:isolate; }
.hud-coins.is-awarding{ animation:coin-bump var(--t-coin) var(--e-out); }
@keyframes coin-bump{ 0%,100%{transform:none} 35%{transform:translateY(-3px) scale(1.04)} }
/* ---- T13 · THE MAP AVATAR: A PERMANENT STROKE AND A ONE-SHOT BEACON.
   THE GOLD IS --f5-gold, and it is the token the item asks for: the
   finale board's 121st-vote marker paints .f5av's ring out of it and
   .f5n.is-mine's numeral to match. It is declared once, at :root, in the
   §5 block — aliased there rather than pointed at --primary precisely so
   that "the marker gold" is addressable as one name, which is what makes
   this reuse a reference and not a coincidence. On this branch its value
   is #FFD60A.
   THE RING IS .f5av's RING, at its small size: 2.5px of gold and 4px of
   keyline, the same two numbers the strip and the plate use. The avatar
   is now the same object in both places — it is the player, and the
   player is marked the same way on the map as on the board.
   border-radius is set because the ring is a box-shadow and the SVG's
   own round shell does not give the BUTTON a shape to spread from. */
.hud-you{
  width:40px; height:40px; border-radius:50%;
  box-shadow:0 0 0 2.5px var(--f5-gold), 0 0 0 4px rgba(0,0,0,.5);
}
/* THE BEACON IS THE GOLD, NOT THE FINALE'S CREAM. --f5-glow is
   245,227,170 — a warm yellow-cream — and it is reserved for the S-1
   board; this is 255,214,10, the marker gold decomposed to a triple
   because a spread shadow needs alpha and the token is a hex. The two are
   not interchangeable and are not meant to look alike: one is a soft
   warm wash on a dark board, this is a hard saturated ring on a 40px
   sticker, and it stops.
   IT PULSES OUT OF THE STROKE IT WILL LEAVE BEHIND. The animation's
   floor IS the resting box-shadow above, so when the class comes off
   nothing jumps: the beacon stops on the frame it is already on and the
   ring is simply what remains.
   THE CLASS IS ONLY EVER ON THE MAP — showScreen() puts it there and
   takes it off — and the selector says so a second time, so a beacon can
   never be left running on a screen this rule did not intend. */
:root{ --av-beacon:255,214,10; }
@keyframes av-beacon{
  0%,100%{ box-shadow:0 0 0 2.5px var(--f5-gold), 0 0 0 4px rgba(0,0,0,.5),
                      0 0 10px 1px rgba(var(--av-beacon),.30),
                      0 0 22px 4px rgba(var(--av-beacon),.13); }
  50%    { box-shadow:0 0 0 2.5px var(--f5-gold), 0 0 0 4px rgba(0,0,0,.5),
                      0 0 16px 3px rgba(var(--av-beacon),.62),
                      0 0 34px 9px rgba(var(--av-beacon),.28); }
}
.stage[data-screen="map"] .hud-you.is-beacon{
  animation:av-beacon 2000ms ease-in-out infinite;
}
/* the same contract §5's board keeps: no motion, and it holds at the
   midpoint rather than at the floor, so the beacon is still legible as a
   beacon to somebody who has switched motion off. */
@media (prefers-reduced-motion: reduce){
  .stage[data-screen="map"] .hud-you.is-beacon{
    animation:none;
    box-shadow:0 0 0 2.5px var(--f5-gold), 0 0 0 4px rgba(0,0,0,.5),
               0 0 16px 3px rgba(var(--av-beacon),.62),
               0 0 34px 9px rgba(var(--av-beacon),.28);
  }
}
/* A4 · the right slot holds whichever of the two is on this screen, and
   holds the SAME 40px box either way so the row never shifts when the
   avatar swaps for the ✕. */
.hud-right{ flex:none; width:40px; height:40px; display:grid; place-items:center; }
.hud-right > [hidden]{ display:none; }
/* ITEM 28 · THE CLOSE IS A CIRCLE, diameter = the title pill's height, so
   the row reads as two pills and a disc cut to one measure. Its keyline
   and extrusion are .ib-b's own and are untouched.
   THE 40px SLOT AROUND IT IS NOT CHANGED — .hud-right still reserves
   40x40, which is what holds the strip at today's height whether the slot
   is showing the 40px avatar or this 36px disc. See .hud-right. */
.hud-x{
  width:36px; height:36px; border-radius:50%;
  font-size:17px; line-height:1;
}

/* =====================================================================
   B5-1 · THE EXIT CONFIRM, BUILT AND CENTRED
   §3.3. The board drew a sheet at the foot; this is centred in BOTH axes,
   because a bottom sheet is the shape of an options menu and this is a
   destructive confirm. It borrows .stmodal's sticker outright — same
   white die-cut, same keyline, same extrusion, same tilt — so the round
   has ONE modal shape and not one for content and another for confirms.
   ===================================================================== */
.exitsheet{
  position:absolute; inset:0; z-index:70; display:grid; place-items:center;
  padding:var(--sp-4); background:rgba(20,18,15,.74);
  opacity:0; transition:opacity var(--t-ov-in) linear;
}
.exitsheet.is-in{ opacity:1; }
.exitsheet.is-out{ opacity:0; }
.exitsheet__box{
  position:relative; width:100%; max-width:300px;
  background:var(--paper); color:var(--ink);
  border:5px solid var(--edge); border-radius:18px;
  box-shadow:0 0 0 2px var(--keyline), 0 8px 0 rgba(0,0,0,.4);
  padding:var(--sp-4);
  display:flex; flex-direction:column; gap:var(--sp-3);
  /* the sticker is LAID DOWN, not printed square. 1.2 degrees the other
     way from .stmodal so the two are not the same object twice. */
  transform:rotate(1.2deg) scale(.9);
  transition:transform var(--t-flip) var(--e-land);
}
.exitsheet.is-in .exitsheet__box{ transform:rotate(1.2deg) scale(1); }
/* §3.3 the ✕ — a third way to stay, and the one that costs nothing to
   reach because it is where the ✕ that opened this already was. Inset so
   it clears the die-cut border rather than sitting on it. */
/* the exit confirm's ✕ is the SAME COMPONENT as the modals' — see the
   shared rules under .stmodal__x. Physically right, not inline-end:
   inset-inline-end resolves LEFT under dir=rtl, which would put it on the
   opposite side from the HUD ✕ that opened it. */
/* §3.3 THE QUESTION IS BLACK AND IT IS A QUESTION. It used to be one
   string carrying the question and the consequence at once, at
   --ink-faint's weight, which made the thing being agreed to read as
   part of the thing being asked. Padded on the end so it clears the ✕. */
.exitsheet__q{
  margin:0; padding-right:34px;   /* clears the ✕, which is physically right */
  font-family:var(--font-display); font-weight:900;
  font-size:19px; line-height:1.18; color:var(--ink); text-align:start;
}
.exitsheet__note{
  margin:-4px 0 0; font-size:var(--fs-meta); font-weight:700;
  line-height:1.4; color:var(--ink-soft); text-align:start;
}
/* §1b · EQUAL HEIGHTS AND A REAL GAP. .p-c is min-height:56px and .r-b is
   52px, so the yellow was 4px taller than the outline and the pair read as
   primary-plus-afterthought rather than as two choices. I kept 56 — the
   PRIMARY's height is the one tuned against every other primary in the
   app, and shrinking it here would make this the one 52px primary in the
   build; raising the outline costs nothing.
   The gap goes 8 -> 14px: at 8 the two boxes were nearly flush and read as
   a segmented control, which is the wrong affordance for a destructive
   confirm where the two options are opposites. */
.exitsheet__row{ display:flex; flex-direction:column; gap:14px; }
/* min-height is not what was making them unequal — both already had 56 and
   both already exceeded it. The heights come from the TYPE: .p-c is 25px
   (--fs-action) and .r-b is 17px (--fs-body), which put them at 70 and 61.
   The type sizes are system tokens and are not this modal's to change, so
   the fix is an explicit shared height with the label centred in it. */
.exitsheet__row > *{
  min-height:70px;
  display:flex; align-items:center; justify-content:center;
  padding-block:0;
}
/* R-B IS BUILT FOR THE DARK GROUND. hachach.css draws it as
   `color:var(--paper); background:transparent; box-shadow:inset 0 0 0 2.5px
   var(--paper)` — cream type in a cream outline, which is correct on the
   charcoal stage and completely invisible on a cream sticker: להישאר
   disappeared and the modal read as having one action and a large empty
   panel under it. Re-inked here rather than in hachach.css, which is the
   locked handoff sheet and not ours to change. */
.exitsheet .r-b{
  color:var(--ink); box-shadow:inset 0 0 0 2.5px rgba(19,19,16,.38);
}
.exitsheet .r-b:active{ box-shadow:inset 0 0 0 2.5px rgba(19,19,16,.65); }

/* §C THE ONE HELPER LINE. Not a pill: plain type on the ground, low
   contrast, between the HUD and the chyron. Its box is reserved on every
   beat — is-empty hides the text, never the space — so turning it on for
   the first MK card cannot change the card's size. */
/* A8 · 1.5x. --fs-micro is 11px and that is what the MK question was set
   in; it is now 17px with a box to match. The box is still RESERVED on
   every beat — is-empty hides the text, never the space — so the card is
   the same size on the claim, the overlay and every cascade card. */
.helper{
  /* margin, not margin-bottom: it is a <p>, and the UA's default 1em top
     margin was quietly taking another 13px off the card. */
  flex:none; min-height:18px; margin:5px 0 0;
  font-size:17px; font-weight:900; line-height:1.05;
  font-family:var(--font-display);
  color:#D8D2C4; text-align:center; opacity:1;
}
.helper.is-empty{ visibility:hidden; }

.round{ position:relative; z-index:1; flex:1; min-height:0;
  display:flex; flex-direction:column; padding-top:26px; }

.beat{ flex:1; min-height:0; display:flex; flex-direction:column; position:relative; }

/* =====================================================================
   NUMERALS · §7. Western numerals in an LTR run inside the RTL flow.
   ===================================================================== */
.num,.n{ direction:ltr; unicode-bidi:isolate; font-family:var(--font-display);
  font-weight:900; }

/* =====================================================================
   PLACEHOLDER · copy Tamar has not written.
   The spike bar hides these so the round can be played clean. They are
   HIDDEN, never deleted: the marker is what makes a missing string
   visible, and the layout has to hold without them because that is what
   shipping looks like. Hiding targets the HOST element, so a paragraph
   that exists only to carry a placeholder leaves no empty line behind.
   ===================================================================== */
.ph{
  display:inline-block; background-color:var(--primary);
  background-image:repeating-linear-gradient(45deg,#FFE03D 0 8px,var(--primary) 8px 16px);
  color:var(--ink); border:2px dashed var(--ink); border-radius:4px;
  padding:2px 8px 3px; font-size:var(--fs-meta); font-weight:700; line-height:1.4;
}
/* THE RULE MATCHED THE HOST BUT NOT THE MARKER, WHICH IS A SHIPPING BUG.
   ph() emits `<span class="ph">…</span>` — a CLASS. `data-ph` is a separate,
   optional ATTRIBUTE put on the HOST when hiding the marker alone would
   leave an empty line behind (.b1hint, .b3go). Only two of the five ph()
   call sites carry that attribute, so with DEV.ph false the other three
   went on painting a yellow hazard stripe in production: the finale's
   result line on all eight no-tally issues, and the cascade's guess label
   on every card of every issue that has one.
   Both halves are needed and they do different jobs: [data-ph] removes the
   whole line, .ph removes just the marker. */
body.no-ph [data-ph],
body.no-ph .ph{ display:none !important; }


/* =====================================================================
   THE CARD STACK
   .cardwrap is the only thing that scales, so a 620px card fits a short
   phone. It does NOT clip: the verdict stamp hangs outside the card and
   .mf-b carries overflow:hidden, which is what clipped it before.
   ===================================================================== */
/* .stack is the only thing that scales, so a 620px card plus its axis
   strip fits a short phone without ever introducing a scroll.
   sizeStage() sets BOTH the transform and an explicit height, because a
   transform does not shrink the box the flex column reserves — leaving
   the layout claiming 620px while the eye saw 500. */
/* §3 THE DECK SITS HIGHER. The beat centres the stack, so a bottom
   margin lifts it by half its own value: 20px here moves the deck up
   10px and leaves that much more air under it. sizeStage() subtracts
   this margin from the card's budget — it is the stack's own margin, so
   the `others` sum never sees it, and uncounted it would push the card's
   foot past the stage on a short viewport. */
.stack{
  display:flex; flex-direction:column; align-items:center; flex:none;
  transform-origin:top center; margin-bottom:20px;
}
/* .cardwrap does NOT clip: the verdict stamp hangs outside the card, and
   .mf-b carries overflow:hidden, which is what was cutting it off. */
/* §A THE DECK. .cardwrap is the deck's own box and the 3D context the
   cards turn inside: perspective HAS to sit on the parent, not on the
   card, or every card gets its own vanishing point and the turn reads
   flat. Fixed height because the cards are stacked absolutely on top of
   each other now — the claim card, the active card and the face-down
   next card all occupy it at once. */
.cardwrap{
  position:relative; width:var(--card-w); height:var(--card-h); flex:none;
  perspective:1400px;
}
/* one card: a back and a front on one element, turned in 3D.
   THE TRANSFORM LISTS MATCH between states on purpose. Interpolating
   `translate+rotate` against a bare `rotateY` falls back to matrix
   decomposition, which squashes the card instead of turning it. */
.deckcard{
  position:absolute; inset:0; transform-style:preserve-3d;
  transform:translate(2px,-7px) rotate(.9deg) rotateY(0deg);
  transition:transform var(--t-card-flip) var(--e-settle);
}
.deckcard.is-current{
  z-index:2;
  transform:translate(0px,0px) rotate(0deg) rotateY(180deg);
}
.deckcard.is-next{ z-index:1; }
/* the resolved card is swiped off — ease-IN, because it is leaving */
.deckcard.is-leaving{
  transform:translate(-420px,8px) rotate(-13deg) rotateY(180deg); opacity:.2;
  transition:transform var(--t-card-exit) var(--e-in),
             opacity   var(--t-card-exit) linear;
}
.deckcard > .cardback,
.deckcard > .mf-b{
  position:absolute; inset:0; margin:0;
  backface-visibility:hidden; -webkit-backface-visibility:hidden;
}
/* the front is pre-turned, so the card shows its back at rest and its
   face at rotateY(180deg). It is in the DOM from the deal but never
   visible until then — which is what stopped an MK's face showing
   through the beat-2 blur a whole beat before the cascade. */
.deckcard > .mf-b{ transform:rotateY(180deg); }

/* §6 THE JOLT WAS UN-TURNING THE CARD. hachach.css lands the stamp with
   `.mf-b.is-stamped{ animation:d2-jolt ... both }`, and d2-jolt's frames
   set `transform:translateY(...)` — a WHOLE transform, which replaced the
   front face's own rotateY(180deg). With fill:both it then held that
   value forever. The front's net rotation went from 360 to 180, its
   backface turned to the viewer, backface-visibility hid it, and what
   showed through was the face-down card underneath: the MK vanished and
   the player saw a card back the instant the stamp landed.
   The jolt is kept — it is the card taking the hit — but it now carries
   the face's rotation with it. */
/* ITEM 7 · 3px -> 5px, AND THE DELAY IS NOW THE NEW FALL. The jolt has to
   fire on the frame the disc hits, and the fall grew 190 -> 200ms, so the
   delay is written out rather than left on --t-stamp-drop, which still
   describes hachach.css's 190ms fall for anything else using it. */
.deckcard > .mf-b.is-stamped{
  animation:d2-jolt-face 120ms var(--e-land) 200ms both;
}
@keyframes d2-jolt-face{
  0%,100%{ transform:rotateY(180deg) translateY(0) }
  40%    { transform:rotateY(180deg) translateY(5px) }
}

/* §1 THE CARD BACK. One illustration, identical on every back and on
   every pile card, at FULL STRENGTH — the previous treatment greyed it,
   dropped it to 20% and multiplied it into the kraft, which was far too
   faint to read as a card back at all.
   BOTTOM-ANCHORED AND COVERED. The art is 1536x2752 (0.558) against a
   340x620 card (0.548), so `cover` crops a sliver — and anchoring the
   bottom takes that sliver off the EMPTY SKY at the top, keeping the
   building and the flags at its foot, which is the part worth seeing. */
.cardback{
  background:var(--cardback-art) bottom center / cover no-repeat, #C4B48F;
  border:var(--card-border) solid var(--edge-warm);
  box-shadow:0 0 0 2px var(--keyline), var(--shadow-card);
  overflow:hidden;
}
/* the claim card sits on top of the deck it belongs to */
.b1card{ z-index:3; }

/* =====================================================================
   BEAT 1 · THE CLAIM · B1-B developed
   ONLY THE TOP CARD MOVES. The stage, the pile and the ground are never
   transformed — the drag is on .b1card and nothing else.
   ===================================================================== */
.b1{ align-items:center; justify-content:center; }

.b1card{
  display:flex; flex-direction:column; align-items:center;
  padding:44px var(--sp-4) 84px;
  touch-action:none;                 /* the card owns the gesture */
  will-change:transform;
}
.b1card.is-dragging{ transition:none; }
.b1card.is-snapping{ transition:transform var(--t-snapback) var(--e-settle); }
/* §3 · THE CARD IS THROWN, AND IT IS THE DECK'S OWN THROW. It used to
   travel on --t-swipe 220ms with --e-out — a DECELERATING curve, which is
   the ease for something arriving — while fading 1 -> 0 linearly across
   the whole flight, so it was half transparent by the time it had crossed
   half the screen. That reads as vanishing, not as being thrown.
   It now uses exactly what .deckcard.is-leaving uses when a resolved MK
   card is swiped off in the cascade: --t-card-exit 320ms on --e-in, the
   accelerating curve, and it fades only to .2 and only over the last
   100ms, so the object is opaque for two thirds of its travel and leaves
   the frame rather than dissolving in it. One throw in the round, not
   two. */
.b1card.is-leaving{ transition:transform var(--t-card-exit) var(--e-in),
                                opacity 100ms linear 220ms; }

/* §1.3 · THE GRAPHIC USES THE ROOM, AND IT IS THE THING THAT YIELDS.
   The card is 620px and the art had a flat 210px floor with the claim
   taking whatever was left, so on a short claim (s1, 49 chars) a third of
   the card was empty. It is now flexible with a generous floor and a cap,
   so it grows into the space a short claim leaves and shrinks when the
   claim needs the room — `flex:1 1 auto; min-height:0` on the art and
   `flex:none` on the claim is what makes the TEXT win the fight. Checked
   against e3, the longest claim in the active set at 190 characters. */
/* the BOX grows into whatever the claim leaves, the IMAGE is capped by its
   own declared size — so the graphic is centred in the space rather than
   the space collecting as a gap between the claim and the buttons */
.b1art{
  flex:1 1 auto; min-height:0;
  display:grid; place-items:center; align-self:stretch;
}
/* THE IMG IS CAPPED, NOT RE-SIZED. claimArt() writes width/height
   attributes on the <img> — 300x180 for s1's art, 128-scaled for the
   topic fallback — and those attributes ARE the layout size. Setting
   `width:auto; height:auto` here (which the first version did) threw them
   away and the image rendered at its natural 900x539, four times its
   slot, overflowing the card and being clipped by .mf-b's overflow.
   max-* only: the attributes still size it, this only ever shrinks it. */
/* THE RATIO IS NOT NEGOTIABLE, AND max-height ALONE BREAKS IT. The
   attributes set the used WIDTH; max-height then clamped the HEIGHT
   without the width following, so the reveal state stretched the
   graphic horizontally on ALL ELEVEN active issues — 1.32x on g1/g3 to
   1.52x on r1. It was not a rounding artefact, it was every card.
   `width:auto` is NOT the fix and was tried once already: on a replaced
   element it discards the attributes and falls back to the INTRINSIC
   size, which rendered s1 at 900x539 and overflowed the card.
   object-fit:contain is. The box may still be constrained by a cap, but
   the pixels inside it letterbox instead of distorting, so no future
   asset or cap can reintroduce the squish. */
.b1art img{
  max-width:100%; max-height:var(--b1art-cap, 340px); height:auto;
  object-fit:contain;
}
/* WHEN THE PANEL RISES THE ART GIVES WAY. The panel covers the card's
   lower portion, and the one thing that must survive it is the claim the
   explanation is explaining. The art is capped hard here so the claim
   floats up clear of the panel's top edge; the answer row is spent and
   goes entirely, which is where most of the room comes from. */
/* ON REVEAL THE ART BOX STOPS GROWING. Left at flex:1 it absorbed the room
   the hidden answer row freed and pushed the claim straight back under the
   panel — the box grew even though the image inside it could not. */
.b1card.is-revealing .b1art{ flex:0 0 auto; }
/* THE 126px REVEAL CAP IS GONE. Capping the art was what forced the
   choice between distorting it and shrinking it, and the artwork is
   never the thing that gives. Every active issue's graphic is 126-191px
   tall and 101-298px wide, all inside the 340px default cap and the
   container's width, so with this removed NO max-* binds and the art
   renders at exactly its declared size — identical before the answer and
   in the reveal. The room comes out of the claim's leading instead; see
   .b1card.is-revealing .b1claim. */
.b1card.is-revealing .b1ans{ display:none; }
.b1card.is-revealing{ padding-bottom:var(--sp-3); }
.b1card.is-revealing .b1claim{ flex:none; }
.b1art img{ display:block; filter:url(#dcw) drop-shadow(0 5px 0 rgba(0,0,0,.30)); }

/* §1.3 · THE TEXT WINS, SO THE TEXT SETS ITS OWN SIZE. The claims in the
   active set run from 49 characters (s1) to 190 (e3) — nearly 4x — and one
   font size cannot serve both: 29px is right for s1 and gives e3 eight
   lines that overflow the card. Three steps, keyed off the string length
   in beat1(), so the graphic yields to the text rather than the text being
   clipped. The steps are measured, not guessed: at 393px, e3 at 29px is
   336px of claim, at 20px it is 232 — which is what lets the panel clear
   it. See the panel cap in claimReveal(). */
.b1claim{
  flex:none; display:grid; place-items:center; margin-top:10px;
  font-family:var(--font-display); font-weight:900;
  font-size:var(--b1claim-fs, var(--fs-claim));
  line-height:1.16; color:var(--ink); text-align:center; text-wrap:balance;
}
.b1card--mid  { --b1claim-fs:24px; }
.b1card--long { --b1claim-fs:20px; }

/* the two answers. IDENTICAL — same class, same size, same weight, same
   ink. אמת and שקר are never told apart by anything but their letters.

   §2.2 THE BUTTON IS THE DRAG'S READOUT. The answer the gesture is
   choosing fills solid --ink on the same ramp as the preview pill, so it
   is fully solid at the commit threshold and partially filled below it.
   Only --fill moves. It changes no geometry and it is never set on the
   other button, so the pair is identical in size and weight at every
   point of the drag — which is the whole reason the feedback could not
   be "shrink the one you are not choosing".                            */
.b1ans{ position:absolute; z-index:7; bottom:var(--sp-3); inset-inline:var(--sp-3); }
.b1ans .v-a{
  --fill:0;
  min-height:60px; font-size:var(--fs-action);
  position:relative; overflow:hidden;      /* clips the fill to the radius */
}
/* THE FILL IS A WIPE, NOT A CROSS-FADE. Fading the background to --ink
   while fading the label to --paper puts mid-grey text on a mid-grey
   ground at --fill:.5 and the word vanishes at exactly the moment the
   player is deciding. The wipe carries a hard edge instead: every pixel
   is either ink-on-paper or paper-on-ink, so both halves stay at full
   contrast at every value. The two layers share one clip so the dark
   ground and the light lettering can never drift apart.
   RTL: the fill grows from the inline-start edge, which is the right. */
.b1ans .v-a::before,
.b1ans .v-a::after{
  position:absolute; inset:0; pointer-events:none;
  clip-path:inset(0 0 0 calc((1 - var(--fill)) * 100%));
}
.b1ans .v-a::before{ content:""; background:var(--ink); }
/* the label's light copy. data-label is the same string as the button's
   own text; the pseudo-element inherits the font, and the padding is
   symmetric, so the two copies land on the same baseline. */
.b1ans .v-a::after{
  content:attr(data-label);
  display:grid; place-items:center; color:var(--paper);
}

/* ---- the swipe, §2.2 · every gesture has a button twin -------------
   THE REVEAL APPEARS ON THE SIDE THE CARD IS MOVING TOWARD. Written in
   PHYSICAL left/right on purpose: the logical properties invert under
   dir=rtl, which is exactly how this ended up mirrored. The mapping
   switch changes which ANSWER a direction means, never which side the
   reveal comes from.                                                  */
/* ---- THE REVEAL, and why it is ON the card rather than behind it.
   Measured: dragging left exposes 0px of ground on the left and 56-152px
   on the right, because the card is 340px wide inside 25px of ground —
   its leading edge leaves the stage before any gap can open there. A
   panel parked behind the card on the travel side therefore cannot ever
   become visible, at any drag distance. It is not a z-order or an RTL
   bug; the geometry forbids it at this card width.
   So the reveal is the Tinder/Reigns move instead: a wash on the card's
   LEADING EDGE, which is visible from the first few pixels of travel, is
   unambiguously on the side the card is going, and travels with it.
   Same ink both directions — it names the word, never which one is the
   "good" answer. The alternative, if the panel behind is wanted, is to
   shrink the card during the drag so ground opens on the leading side. */
.b1target{
  position:absolute; z-index:6; top:0; bottom:0; width:62%;
  display:flex; align-items:center; opacity:0; pointer-events:none;
  border-radius:2px;
}
.b1target--left{
  left:0; justify-content:flex-start; padding-inline-start:20px;
  /* §2 A SHADOW, NOT A LEAK. This was --kraft-deep, a pale beige, which
     read as light escaping from behind the card instead of the card
     lifting away from the deck. Deep warm brown-black: the gap a card
     opens when it moves off is dark. */
  background:linear-gradient(to right, rgba(14,10,6,.90) 0%,
             rgba(14,10,6,.52) 58%, rgba(14,10,6,0) 100%);
}
.b1target--right{
  right:0; justify-content:flex-end; padding-inline-end:20px;
  background:linear-gradient(to left, rgba(14,10,6,.90) 0%,
             rgba(14,10,6,.52) 58%, rgba(14,10,6,0) 100%);
}
/* THE WASH CARRIES NO WORD. It used to hold the answer at 38px across the
   card face, which — with the pill above it and the button below it —
   put the same word on screen three times during a single drag. The wash
   is the DIRECTION; the pill and the button carry the word. */

/* §2.1 Reigns: the label fades in ACROSS the drag, before release.
   It lives INSIDE the card so it travels with it — as a sibling it
   floated loose on screen while the card moved out from under it. */
/* IT IS A STAMP, NOT A BUTTON. As a paper pill with --r-pill corners, a
   white die-cut edge and an extruded shadow it was built out of exactly
   the parts a control is built from, so the one thing on the card that
   is NOT pressable looked the most pressable. Now it takes the committed
   button's own treatment — solid --ink, --paper lettering — with every
   affordance removed: no radius, no die-cut, no extrusion, nothing that
   implies a press. It sits ON the card surface and hangs slightly over
   the top edge, the way a stamp lands on top of a thing rather than
   being part of it. The opacity ramp is unchanged: it is still the
   drag's readout and still arrives on the same k as the button fill. */
.b1prev{
  position:absolute; z-index:9; top:-9px; opacity:0;
  display:flex; align-items:center;
  background:var(--ink); border:0; border-radius:0;
  padding:7px 16px 9px; pointer-events:none;
  box-shadow:none;
}
.b1prev b{ font-family:var(--font-display); font-weight:900; font-size:22px;
  color:var(--paper); }

/* out of flow so unwritten copy must not be
   able to cost the card 40px when the placeholders are switched on */
/* 21 · in the set and switched, though it renders nothing today: it is a
   data-ph placeholder and body.no-ph hides it. Wired now so it comes back
   in the body face rather than in the app default. */
.b1hint{ position:absolute; left:0; right:0; bottom:0; z-index:10;
  font-family:var(--font-body); font-weight:400;
  text-align:center; margin:0; pointer-events:none; }

/* =====================================================================
   BEAT 2 · YOUR OWN VOTE · B2-CHAIR-FULL
   A full-height blurred overlay, not a card: this is an interruption,
   a meta question, not a step in the same plane.
   THE OVERLAY IS A CHILD OF .stage, NOT OF THE BEAT, so the blur reaches
   every edge including the safe areas. Anchored to the beat it stopped
   at the round's padding and the dot-grid ground showed through.
   ===================================================================== */
.ov--stage{
  position:absolute; inset:0; z-index:9;
  /* THE TRACK IS CAPPED AT THE PADDED BOX, and that one declaration is the
     whole beat-2 clipping fix. An implicit `display:grid` track is sized to
     MAX-CONTENT, and .b2votes below asks for --card-w (340px, hachach.css)
     — so on any stage narrower than 372px the track grew to 340, the pane
     and the vote row grew with it, and the 12px of overflow all landed on
     the RTL end: at 360 the נגד chip sat 4px from the screen edge instead
     of 16 and its 4.6px keyline was cut. minmax(0,1fr) makes the track the
     content box, so --card-w's own max-width:100% can finally bite.
     Nothing changes at 375 or 390, where 340 already fitted. */
  display:grid; place-items:center; grid-template-columns:minmax(0,1fr);
  padding:calc(18px + env(safe-area-inset-top)) 16px
          calc(16px + env(safe-area-inset-bottom));
  /* softer than beat 3's: what is behind it — the answer just given and
     the cards ahead — is meant to be recognisable as those things */
  -webkit-backdrop-filter:blur(9px) saturate(.9);
  backdrop-filter:blur(9px) saturate(.9);
  background:rgba(28,26,23,.52);
  animation:ov-in var(--t-ov-in) var(--e-settle) both;
}
/* ONE SURFACE, TWO CONTENTS. Both panes occupy the same grid cell of the
   same .ov, so the backdrop is painted once and never re-rendered: at the
   commit the vote pane travels UP and out while the bill pane arrives
   from BELOW, and the blur behind them does not move, blink or restart.
   Two overlays doing this were two blurs and the seam read as a load. */
.ovpane{
  /* the same cap one level down: .ovpane is itself a grid whose item is
     .ov-inner, so without this the max-content track re-inflates to 340
     under a correctly-sized parent and the row overflows anyway. Both
     levels or neither. */
  grid-area:1 / 1; width:100%; display:grid; place-items:center;
  grid-template-columns:minmax(0,1fr);
  transition:transform var(--t-ov-swap) var(--e-settle),
             opacity   var(--t-ov-swap) var(--e-settle);
}
.ovpane--vote.is-above{ transform:translateY(-44px); opacity:0; pointer-events:none; }
.ovpane--bill.is-below{ transform:translateY( 44px); opacity:0; pointer-events:none; }

/* ITEM 38 · --ovgap NAMES THE COLUMN GAP so the two rules around .b2q can
   subtract it. The gap itself is unchanged and still applies between every
   child; variant D's 26-above / 12-below are absolute figures, and the only
   way to state an absolute gap inside a flex column whose gap is vh-based
   is to cancel the gap out on the one pair that needs it. Same trick as the
   board, which calls the variable --gap. */
.ov-inner{ display:flex; flex-direction:column; align-items:center;
  --ovgap:clamp(8px, 1.6vh, 16px);
  gap:var(--ovgap); width:100%; max-width:358px; }
/* align-items:center on a flex column shrink-wraps every child, so the
   vote row was 219px of a 326px line. The board writes width:100% here. */
.ov-inner .v-a-row{ width:100%; }
/* THE VOTE ROW IS THE CARD'S WIDTH, NOT THE OVERLAY'S. At the overlay's
   358px the three chips were wider and taller than anything on the claim
   card, so beat 2 read as a bigger, more important surface than the deck
   it interrupts. Same width as the card, same min-height and same type as
   the claim card's two answers. */
/* the parent selector is load-bearing: `.ov-inner .v-a-row{width:100%}`
   above is (0,2,0) and a bare .b2votes would lose to it silently. */
/* §4 AIR BETWEEN THE QUESTION AND THE ANSWERS. .ov-inner's own gap is
   clamp(8px,1.6vh,16px) and it applies between every child, which left
   the vote row sitting as close to the bill title as the title sits to
   the question — one undifferentiated block. This is on top of the gap,
   so the space before the buttons roughly doubles while the copy above
   stays as tight as it was. */
/* ITEM 38 · THE GAP BELOW THE QUESTION IS 12px FLAT. It was the column gap
   plus clamp(10px,2.2vh,22px) — 32px at 844 — which put the question nearer
   the title line above it (13.5) than the buttons below it. It is the
   question the buttons answer, so it groups with them: 26 above, 12 below.
   The margin goes negative at tall viewports (12 - 13.5 = -1.5) and that is
   correct — it is cancelling the column gap, not pulling the row up. */
.ov-inner .b2votes{
  width:var(--card-w); max-width:100%;
  margin-top:calc(12px - var(--ovgap));
}
.b2votes .v-a{ min-height:60px; font-size:var(--fs-action); }
/* §4 THE SEAT. The chair and the confirmation share one positioning
   context so the confirmation can sit ON the chair rather than under it.
   THE CHAIR IS NEVER CROPPED — it is the game's emblem and a cut one
   reads as a bug — so it is height-capped against the viewport and the
   cap is what shrinks on a short phone, never the crop. */
/* ITEM 36 · THE SEAT RESERVES THE TAPE'S DEPTH, AND THEN SOME. The tape is
   absolutely positioned so it contributes no height of its own; this
   margin is the only thing holding .b2head off it.
   THE BOARD'S 40px IS NOT ENOUGH — measured, the line lands at -0.9px,
   i.e. printing 1px INTO the tape rather than under it. 61px puts the gap
   at 20.1px. The extra over a plain 16 is paid to the tape's own 3px
   extrusion, which sits below its box and eats into any gap measured to
   it: 20.1 measured reads as ~17 of actual air, which clears the beat's
   own 12.9-16px column rhythm instead of competing with it. */
.b2seat{ position:relative; flex:none; display:grid; place-items:center;
  margin-bottom:61px; }
/* ITEM 36 · VARIANT E, ported from explorations/v23/b2-headline.html.
   .b2onchair is retired: the headline is no longer type on the cushion,
   it is a KRAFT TAPE running edge to edge behind the chair's foot.

   WHY IT IS STILL A CHILD OF .b2seat. The tape is positioned off the
   seat's own box — top:calc(100% - 5px) drops its top edge 5px above the
   seat's bottom, so the foot's die-cut overlaps the tape's top and the
   thing reads as passing BEHIND the chair rather than under it. Anchoring
   it to the pane instead would have to re-derive the chair's foot on every
   viewport, which is exactly what the board avoided.
   THE OVERRUN IS THE POINT. 440px fixed against a 390 or 360 stage, so the
   tape leaves the screen on both sides and has no visible ends — the torn
   clip-path is for the top and bottom edges, not the ends. Nothing scrolls:
   html and body are position:fixed with overflow:hidden, and .stage clips.
   THE GAP BELOW IT IS OURS, NOT THE BOARD'S. See .b2seat's margin. */
.b2tape{
  position:absolute; z-index:0; left:50%; top:calc(100% - 5px);
  width:440px; margin:0;
  transform:translateX(-50%) rotate(-1.5deg);
  padding:14px 0 13px;
  background:var(--card-surface); color:var(--ink);
  font-family:var(--font-display); font-weight:900;
  font-size:26px; line-height:1; white-space:nowrap; text-align:center;
  box-shadow:0 3px 0 rgba(0,0,0,.30), inset 0 0 0 1px rgba(0,0,0,.10);
  clip-path:polygon(0 0,100% 0,99.4% 18%,100% 36%,99.3% 54%,100% 72%,99.4% 90%,99.7% 100%,
                    0 100%,.6% 84%,0 66%,.7% 48%,0 30%,.5% 12%);
  pointer-events:none;
}
/* the chair paints IN FRONT of the tape — that overlap is the whole
   illusion. .b2tape is z-index:0, so the chair only needs to be raised
   into the same stacking context to sit above it. */
.b2chair{ position:relative; z-index:1; }
.b2chair{
  display:block; flex:none;
  /* was clamp(96px, 23vh, 196px) — at that size it read as an icon beside
     the question instead of the thing the question is about */
  width:auto; height:clamp(168px, 40vh, 330px); aspect-ratio:168 / 196;
  object-fit:contain;
  filter:url(#dcw) drop-shadow(0 5px 0 rgba(0,0,0,.34));
  transition:transform var(--t-b2-seat) var(--e-settle);
}
/* the confirmation lands on the seat itself, ~62% down the chair, which
   is where the seat pan is in the illustration */
.b2taken{
  position:absolute; left:50%; top:62%; margin:0; z-index:2;
  display:flex; align-items:baseline; gap:var(--sp-2); white-space:nowrap;
  padding:8px 20px 11px;
  background:var(--paper); color:var(--ink);
  box-shadow:0 0 0 3px var(--edge), 0 0 0 5px var(--keyline), 0 5px 0 rgba(0,0,0,.34);
  font-family:var(--font-display); font-weight:900;
  font-size:clamp(21px, 4.2vh, 32px); line-height:1;
  opacity:0; pointer-events:none;
  transform:translate(-50%,-50%) scale(.82);
  transition:opacity var(--t-b2-seat) var(--e-settle),
             transform var(--t-b2-seat) var(--e-land);
}
.ovpane--vote.is-taken .b2taken{ opacity:1; transform:translate(-50%,-50%) scale(1); }
/* the chair settles under the weight of being taken */
.ovpane--vote.is-taken .b2chair{ transform:translateY(4px) scale(1.02); }
/* the two not chosen RECEDE, they are not deleted: the round never hides
   an option it offered, and beat 5 still has to be able to talk about
   what was on the table. */
.ovpane--vote.is-taken .b2votes .v-a{
  opacity:.3; transform:scale(.94);
  transition:opacity var(--t-b2-seat) linear, transform var(--t-b2-seat) var(--e-settle);
}
.ovpane--vote.is-taken .b2votes .v-a.is-chosen{ opacity:1; transform:none; }
/* §3.2 THE QUESTION'S LEADING. It had no line-height at all, so it ran
   at the UA's `normal` — which for SimplerPro 900 resolves to 1.32 — and
   at 22-29px that is 8px of air between two lines of a two-line question.
   1.08 is the display face's own set width: the prompt now reads as one
   block rather than as two sentences. Before 1.32 (normal), after 1.08.
   text-wrap:balance keeps the two lines near-equal so the tighter leading
   does not leave a one-word second line hanging under a full first. */
/* §2 · THE FRAMING LINE. Chrome, not card content, and deliberately NOT
   the chyron's own treatment — the chyron is the player's pinned VOTE and
   a second light-and-neon band would say the same thing twice. This is the
   inverse construction: a dark plate with a bright rule down its leading
   edge, so it reads as framing that belongs to the beat rather than as a
   caption on the question below it.
   WEIGHT SITS BETWEEN ITS NEIGHBOURS on purpose — larger and brighter than
   the bill link (15px at 78% white) and clearly under the prompt
   (22-29px display), because it introduces the question rather than being
   it. Two lines at 360px; measured, not assumed. */
/* ITEM 30 · .b2frame IS GONE. The translucent strip with the cyan leading
   edge is what this composition replaces: it was a second headline above
   the question, and the 121st-MK conceit it carried is now in the vote
   question itself. The rule is removed rather than left as a tombstone
   because nothing emits the class any more.

   THE BEAT HAS ONE LOUD OBJECT. Headline, bill button and תכלס are all
   reading-size; .b2q is the only display line, and it sits directly above
   the buttons it is asking about. Two competing headlines is exactly what
   the banner was. */
.b2head{
  margin:0; max-width:32ch;
  font-size:17px; font-weight:700; line-height:1.5;
  color:#D8D2C4; text-align:center; text-wrap:pretty;
}
/* ITEM 34b · THE TAP TARGET IS THE TITLE, INSIDE THE SENTENCE. It is a
   real button, not a styled span, so it carries the control's semantics —
   but it has to sit in the run of text without breaking it, hence the
   inline box, the inherited type and the baseline alignment. The underline
   and the 78% white are what .b2bill--link used to carry; that rule is
   retired and this replaces it. */
.b2title-link{
  appearance:none; border:0; background:none; cursor:pointer;
  font:inherit; font-weight:900; color:#FBF7EE;
  text-decoration:underline; text-underline-offset:3px;
  padding:0; margin:0; display:inline;
}

/* ITEM 38 · VARIANT D, from explorations/v24/b2-question.html.
   FLAT 30px, NOT A CLAMP. The clamp resolved 26px at 844 and bottomed out
   at its 21px floor by 640, so the question shrank while the tape stayed
   26 — and this beat repeats 22 times, where a size that moves between
   rounds reads as a rendering wobble rather than as design. One size.
   30px OUTRANKING THE 26px TAPE IS THE POINT, not an oversight: the tape
   asks the framing question, this line asks the one the buttons answer.
   NO PANEL, TINT, STROKE OR OUTLINE — board variants E and F were built
   and rejected; F reads as a fourth button above the three. */
.b2q{ font-family:var(--font-display); font-weight:900;
  font-size:30px; line-height:1.1; text-wrap:balance;
  color:#FBF7EE; text-align:center; margin:0;
  /* v26 · 26 -> 38. The 12px is room between the law-title link and the
     question, and it is on the MARGIN rather than on padding for a
     reason that is not cosmetic: .b2q is .b2q__tab's containing block,
     and an absolutely positioned tab at bottom:100% anchors to the
     PADDING box's top edge. padding-top:12px would leave the tab where
     it is and push the text 12px out from under it, silently turning the
     decided +11px descent into -1px. Measured on the board: the margin
     moves the head-to-question gap 23.5 -> 35.5 and leaves the
     question-to-buttons gap at 6.5, the line counts at 2/3 and the line
     widths identical to the pixel. */
  margin-top:calc(38px - var(--ovgap)); max-width:100%;
  /* the tab's containing block. Changes no box. */
  position:relative; }

/* ---- v26 · THE תכלס TAB, ON THE QUESTION.
   Decided on explorations/v26/tachles-tab.html; it replaces the tab that
   was briefly built on the kraft tape, which is gone.
   SHAPE GRAMMAR IS .ask-st__i's, THE MK CARD'S BLACK TAB — same #2E2A26
   neutral ink, same white die-cut, same keyline outside it, same radius,
   same display Black — with every dimension multiplied by 0.8 and
   nothing rounded to a nicer number: 16.5 -> 13.2px type, 8/13/9 ->
   6.4/10.4/7.2 padding, 3 -> 2.4 border, 2 -> 1.6 keyline, 12 -> 9.6
   radius, 4 -> 3.2 extrusion. line-height stays 1.06, a ratio that
   already scales. Rendered box 58.43 x 35.44.
   +4deg, NOT -4. About the bottom-right corner the far end then rides UP
   and the tab's lowest ink IS the anchor corner, which is what makes the
   descent below a single number instead of a corner-dependent one. It
   also lifts the tab off .b2head: at -4deg it dipped into the title's
   band and eight of the sixteen titles collided with it.
   THE +11px DESCENT IS THE WHOLE DECISION. Measured against the
   question's real ink: SimplerPro Black's ל carries 21.72px of ascent at
   30px and the tallest body letter beside it 16.95, so the ascender the
   tab may rest on is 4.77px tall and nothing more. +11 puts the tab
   1.72px into it with 3.05px still clear of the letter bodies. +8 never
   reached the ascender; +14 cleared the bodies by 0.05px, which is a
   rounding artefact and not a tolerance; +20 cut 5.95px into them.
   translateY is 5.5 less than the descent because the glyph rect's top
   sits 5.5px above .b2q's own border box.
   THE HORIZONTAL IS NO LONGER A PROPERTY OF THE STRING. It was: anchored
   to the column's start edge the tab hit 11-54px of the first word on
   ten of the sixteen and nothing at all on six. It is now anchored to
   line 1's own box — see placeQTab() in proto.js for why that needs a
   runtime read and what it costs. */
.b2q__tab{
  /* v26f · left is written by placeQTab(), which puts it at line 1's
     centre biased 30% of that line's width toward the start edge. The
     0 here is a floor, not a position: it is overwritten in the same
     tick the pane is appended, before a frame is painted.
     translateX(-50%) is what makes `left` mean the tab's CENTRE, so the
     placement never needs the tab's own width. It is listed BEFORE the
     rotate, so it applies in the parent's frame after the rotation —
     the rotate still pivots on transform-origin 100% 100% and the +11px
     descent is untouched by the horizontal move. */
  position:absolute; z-index:2; left:0; right:auto; bottom:100%;
  transform:translateX(-50%) translateY(5.5px) rotate(4deg);
  transform-origin:100% 100%;
  display:block; white-space:nowrap; pointer-events:none;
  background:#2E2A26; color:#F4F1E8;
  font-family:var(--font-display); font-weight:900;
  font-size:13.2px; line-height:1.06;
  padding:6.4px 10.4px 7.2px; border-radius:9.6px;
  border:2.4px solid var(--edge);
  box-shadow:0 0 0 1.6px var(--keyline), 0 3.2px 0 rgba(0,0,0,.34);
}
/* ---- v26 · THE PROMPT'S TAIL, T-a.
   display:block is the requirement, not a preference: the tail is its
   own line at every width and must never run back inline. 21px Black
   against the claim's 30px Black — the board drew a 22/400 and an 18/400
   beside it and this is the one that stays in the display voice.
   The 6px is the declared gap; measured ink to ink it reads as 12.0px
   with the leading either side, baseline to baseline 31.0. */
.b2q__tail{
  /* v27 PART 1 · 6px -> 0. The tail is part of the question and has to
     group with the claim, not float between the claim and the buttons.
     It read as floating because the two gaps matched: 11.99px ink-to-ink
     above it against 6.5px below. MEASURE INK, NOT BOXES — the 30px
     claim's line box hangs ~5.5px of leading below its last glyph and
     the 21px tail carries its own above, so more than half of that 12px
     was leading and only 6 of it was the margin. Removing the margin
     entirely lands the ink gap at the ~6px asked for, which is now
     visibly tighter than the 6.5px below rather than double it.
     Neither font size moved; both are decided. */
  display:block; margin-top:0;
  font-family:var(--font-display); font-weight:900;
  font-size:21px; line-height:1.15;
}
/* ITEM 34c · .b2bill is retired — the bill name is no longer its own
   line; the tap target is the title inside .b2head. See .b2title-link. */
/* §3.2 .b2consent IS GONE from the markup — "את התוצאה נגלה בסוף ›" was
   a promise about a beat five screens away, printed under the question
   being asked now. The rule is kept as a tombstone rather than deleted so
   the next person searching for the class finds out where it went. */
.ov--out{ animation:ov-out var(--t-ov-in) var(--e-settle) both; }
@keyframes ov-out{ from{opacity:1} to{opacity:0} }

/* =====================================================================
   BEAT 3 · THE BILL · B3-OVERLAY
   One line, over the MK card. What shows through is the card and nothing
   else. Dismiss COLLAPSES INTO the card beneath.
   ===================================================================== */
/* the bill pane's own column. The surface is .ov--stage's; this is only
   the content that arrives on it. */
.b3inner{ gap:var(--sp-4); text-align:center; }
.ov--stage.is-dismissable{ cursor:pointer; }
/* T5 · BEAT 3, DOWN TO THE QUESTION. .b3say and its two marked runs
   (.b3say__d the date, .b3say__t the bill name) go the way ITEM 31 sent
   .b3title, .b3date and .b3go: the beat is one line now, and the line is
   the question. Deleted rather than left behind a retired class, because
   the only element on the screen should not have a stylesheet describing
   a paragraph that is no longer built.
   .b3inner keeps its gap and its centring — it is a one-child column
   today and the gap costs nothing, but it is also where anything the beat
   grows back would land. */
.b3ask{
  margin:0; font-family:var(--font-display); font-weight:900;
  font-size:26px; line-height:1.2; color:#F6F1E2; text-wrap:balance;
}
.ov--collapse{ animation:ov-collapse var(--t-ov-collapse) var(--e-out) both;
  transform-origin:50% 62%; }
@keyframes ov-collapse{
  from{ opacity:1; transform:scale(1);
        -webkit-backdrop-filter:blur(16px) saturate(.9); backdrop-filter:blur(16px) saturate(.9) }
  to  { opacity:0; transform:scale(.86);
        -webkit-backdrop-filter:blur(0) saturate(1); backdrop-filter:blur(0) saturate(1) }
}

/* =====================================================================
   BEAT 4 · THE MK CASCADE · B4-D developed, CHIPS-RING, D2 at 190px
   ===================================================================== */
.b4{ align-items:center; justify-content:center; }

/* draw/spend are gone: a card is TURNED OVER to arrive (.deckcard) and
   SWIPED OFF to leave (.deckcard.is-leaving). Nothing fades in place. */

.mf-b__port{ filter:drop-shadow(0 2px 0 rgba(0,0,0,.18)); }

/* ---- THE PARTY GOES ON ITS OWN LINE.
   hachach.css styles .mf-b__id's <h2> and <p> both display:inline-block,
   so on a short name they share a line and the party reads as a subtitle
   beside it. Every board frame draws the party UNDER the name.
   §1.4b the label itself stays — hiding it dumps the complexity on a
   17-year-old as noise, and the fix for a boring round is curation. */
.mf-b__id h2{ display:block; }
.mf-b__id p { display:inline-block; margin-top:4px; }
/* the .mf-b__basis rule went with the line itself — see the note at the
   verdict. Nothing else in the sheet targeted it. */

/* the initials badge. Never another politician's face. Set on the set's
   own eyeline — 37.7% down the box — so it occupies the card as a
   portrait does. */
.mf-b__badge{
  position:absolute; left:50%; top:37.7%; translate:-50% -50%;
  width:170px; height:170px; border-radius:50%;
  display:grid; place-items:center;
  background:var(--paper); color:var(--ink);
  font-family:var(--font-display); font-weight:900; font-size:56px;
  box-shadow:0 0 0 4px var(--edge), 0 0 0 6px var(--keyline);
}

.mf-b__foot .v-a{ font-size:19px; }

/* ---- the guess-vs-reality axis. The payload of the beat, and it is
   INSIDE the card again, at the foot, where the board draws it.
   It is absolutely positioned, so it contributes nothing to the card's
   box: the card is 340x620 and the strip arriving at the verdict cannot
   re-scale it. The reserved box that used to hold its footprint below the
   card is gone with it — reserving space out there is what was making the
   card smaller.

   IT CROSSES THE LOWEST FACES IN THE SET, and that is known and accepted
   for this build. Face boxes measured per portrait (card-local px, skin
   mask + beard density, validated against the render):
     ben_gvir  y_max 508 · netanyahu 450 · lapid 516
     gantz     y_max 551 · michaeli  578 · deri  553
   The strip's top edge sits at y=541, so it lands on Michaeli's neck by
   37px and on Deri's beard by 12px. Unresolved, on the record. */
.gx{
  position:absolute; z-index:8; bottom:var(--gx-bottom); inset-inline:var(--sp-3);
  height:var(--gx-h);
  animation:gx-in var(--t-hold) var(--e-settle) both;
  background:rgba(216,201,168,.92); border-radius:12px; padding:7px 9px 5px;
  box-shadow:0 0 0 2px rgba(0,0,0,.4);
}
@keyframes gx-in{ from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} }
.gx-track{
  position:relative; height:38px; margin:0 4px;
  background:rgba(34,31,23,.13); border-radius:19px;
  box-shadow:inset 0 0 0 1.5px rgba(34,31,23,.22);
}
/* the fill. It grows FROM the player's stop toward the MK's, anchored on
   the edge the growth starts at — see runAxis() for why that anchor is
   what makes it read right-to-left when it runs that way. */
/* §3.1 · THE GAP CARRIES THE DISTANCE. It was rgba(34,31,23,.26) — a flat
   grey that said only "there is a gap here" while the length of it
   already said that.
   ONE HUE, THREE STRENGTHS, and that is deliberate: the strengths are the
   only variable, so the three states cannot be misread as three different
   kinds of thing. The hue is --verdict-surprise, which is already the
   system's "you were surprised" colour, so a wide gap says the same word
   the stamp does rather than introducing a fourth verdict colour.
   SYMMETRY IS STRUCTURAL. runAxis() keys the class off Math.abs(), so
   בעד->נגד and נגד->בעד are the same class and there is no branch that
   could drift. Verified in the report against all 9 guess/vote pairs.
   DISTANCE 0 NEVER PAINTS A FILL — there is nowhere to travel — so
   agreement is carried by a lime tint on the TRACK instead, which is the
   "neutral/positive, no alarm" end of the scale. */
.gx-fill{
  position:absolute; top:0; bottom:0; width:0; border-radius:19px;
  background:var(--gx-gap, rgba(34,31,23,.26)); pointer-events:none;
  transition:background var(--t-flip) linear;
}
.gx--d1{ --gx-gap:color-mix(in srgb, var(--verdict-surprise) 30%, transparent); }
.gx--d2{ --gx-gap:color-mix(in srgb, var(--verdict-surprise) 62%, transparent); }
.gx--d0 .gx-track,
.gx.is-agreed .gx-track{
  background:color-mix(in srgb, var(--verdict-correct) 22%, rgba(34,31,23,.26));
}
.gx-m{ position:absolute; top:50%; translate:50% -50%; width:38px; height:38px; display:block;
  transition:right var(--t-flip) var(--e-settle); z-index:2; }
/* the player's token drops into its slot rather than being there */
.gx-you{ transition:right var(--t-flip) var(--e-settle),
                    transform var(--t-gx-lock) var(--e-land),
                    opacity var(--t-gx-lock) linear; }
.gx-you.is-landing{ transform:translateY(-14px) scale(1.12); opacity:0; }
/* the MK's token arrives in the PLAYER'S slot before it travels */
.gx-mk{ transition:right var(--t-flip) var(--e-settle),
                   opacity var(--t-gx-appear) linear,
                   transform var(--t-gx-appear) var(--e-land); }
.gx-mk.is-hidden{ opacity:0; transform:scale(.72); }
/* agreement: the two share one slot and settle instead of travelling */
.gx.is-agreed .gx-track{ animation:gx-settle var(--t-gx-settle) var(--e-land); }
@keyframes gx-settle{ 0%,100%{transform:none} 45%{transform:scale(1.015)} }
/* A8 · the neutral "your guess" mark. A punch-hole: paper ring, dark
   centre, NO hue — the verdict stamp beside it is the only coloured thing
   in the beat and this must never compete with it or be mistaken for it.
   Its label sits above the token rather than inside: 38px cannot hold
   three Hebrew words, and the strip's own height is fixed. PLACEHOLDER,
   marked as such — B4 replaces the whole mark. */
.gx-punch{
  display:block; width:38px; height:38px; border-radius:50%;
  background:var(--paper);
  box-shadow:inset 0 0 0 6px var(--ground), 0 0 0 2px rgba(0,0,0,.45),
             0 2px 0 rgba(0,0,0,.3);
}
.gx-punch__lab{
  position:absolute; bottom:calc(100% + 3px); left:50%; transform:translateX(-50%);
  white-space:nowrap; font-size:9px; line-height:1;
}
.gx-punch__lab .ph{ padding:1px 5px 2px; font-size:9px; border-width:1px; }
.gx-port{ display:block; width:34px; height:46px; object-fit:contain; object-position:50% 0; }
.gx-badge{ display:grid; place-items:center; width:38px; height:38px; border-radius:50%;
  background:var(--paper); font-family:var(--font-display); font-weight:900; font-size:14px;
  box-shadow:0 0 0 2px var(--keyline); }
/* ---- THE PAIRED PLAYER TOKEN, and the bug it was.
   When the guess and the vote land on the SAME stop the player's token is
   lifted clear of the MK's so both read. The lift was translateY(-30px)
   on a 38px token centred in a 38px track with 7px of strip padding above
   it: 30px up put its top edge 16.6px ABOVE .gx's own top edge, so after
   the verdict the avatar floated over the strip, outside it, attached to
   nothing. The lift has to fit the headroom that exists.
   A pure vertical stack cannot be made to fit: the MK's portrait already
   overflows the track to -4px, so clearing it needs a 26px lift and the
   strip has 7px. The paired token becomes a BADGE on the shared stop
   instead — up and toward the inline-start, small, above the portrait in
   z-order, and read as separate by the die-cut edge it already carries.
   It costs no height, and it stays inside both the track and the slot at
   every stop and every card scale. */
.gx-m.is-paired.gx-you{
  translate:50% -50%; transform:translate(13px,-8px) scale(.58); z-index:2;
}
.gx-stops{ display:flex; margin-top:5px; }
.gx-stops i{ flex:1; text-align:center; font-style:normal; font-size:var(--fs-meta);
  font-weight:700; color:var(--ink-soft); }

/* ---- D2, THE VERDICT STAMP · ONE PLANE, ON TOP ---------------------
   It paints OVER everything the card carries — portrait, name plate, the
   axis strip at the foot — and it is flat while it does it: no drop
   shadow against the card, no ring, nothing that reads as a layer
   sitting between the card and the viewer. hachach.css gives .d2 a
   1px black shadow for exactly that depth cue; it is removed here.

   Parented to .cardwrap rather than .mf-b, which carries overflow:hidden
   and would cut the overhang, with a z-index above the card so it clears
   the strip's z-index 8 inside it.

   IT LANDS ON THE FACE ON ALL SIX, and that is accepted for this build
   rather than solved. top:430 puts the disc's foot exactly on the card's
   bottom edge — the largest inside-the-card placement — and the share of
   the disc covering skin or beard is then:
     netanyahu 4% · lapid 12% · ben_gvir 18% · gantz 35% · deri 42% ·
     michaeli 69%
   No placement that keeps a 190px disc wholly on the card clears anyone:
   the faces run y[115,450..578] and the card is 620, so there is no 190px
   band inside it that misses them. Dropping to top:470, which hangs 40px
   below the card, would clear netanyahu and ben_gvir — but the overhang
   is not reserved anywhere (reserving it would shrink the card) and it
   clips ~19px off the stage at 375x667. Card size wins. ON THE RECORD,
   UNRESOLVED. */
.d2{
  /* one number for the disc. The word's size and the ink's CSS-px scale
     are both derived from it, so they cannot drift out of step with it. */
  --d2-size:190px;
  width:var(--d2-size); height:var(--d2-size);
  top:430px; left:-22px; bottom:auto; right:auto;
  z-index:14;
  box-shadow:none;
}
.d2__art{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; fill:none; }
.d2__art circle{ fill:none; stroke:currentColor; }
/* §1 THE CENTRE WORD IS HTML. No bidi property fixes SVG text in WebKit
   — direction, unicode-bidi and every combination of them render Hebrew
   left-to-right there — so the word does not go through SVG's text
   shaping at all. HTML text is reordered correctly by every engine, and
   the source strings stay in logical order in the file.

   IT IS STILL THE STAMP'S OWN MARK, not type layered on a graphic:
   · it lives INSIDE .d2, so d2-land's scale — and any rotation the disc
     is ever given — carries it; disc and word are one object;
   · `color` is unset, so it takes the disc's currentColor (--d2-on), the
     identical ink the circles are stroked with;
   · the display face at 900, as the ring had;
   · filter:url(#ink-h) is #ink rescaled from viewBox units to CSS px, so
     the displacement, the dry-brush holes and the blur land at the same
     rendered grain — and inkBleed() drives both maps off one clock, so
     the word ruptures at contact with the disc rather than after it.
   WHAT IS LOST: nothing measurable. The one difference is that the word
   is filtered in its own pass instead of inside the disc's <g>, so the
   noise field is sampled from its own origin — the grain does not
   continue unbroken from circle to letter. At 32px on a 190px disc that
   is not visible; it would be if the word ever overlapped a circle.

   .d2__ring is GONE. Curved text is the one case HTML cannot take over
   without hand-placing glyphs in visual order, which is the reversal
   being refused here. The ring stays as the two stroked circles. */
.d2__word{
  position:absolute; inset:0;
  display:grid; place-items:center;
  font-family:var(--font-display); font-weight:900;
  font-size:calc(var(--d2-size) * .17);      /* the 17 viewBox units it was */
  letter-spacing:.01em; line-height:1;
  filter:url(#ink-h);
  /* the disc is role="img" with an aria-label; this is the same word
     again, and pointer-events would otherwise put it over the card */
  pointer-events:none;
}

/* EVERY BACK IS THE SAME BACK — the pile's cards carry the identical
   artwork to .cardback, at the same strength, so the deck reads as one
   deck. hachach.css gives .pile i a grid-on-kraft background; this
   replaces it outright rather than layering over it.
   The offsets start one step deeper than they did, because the face-down
   .deckcard now occupies the slot the first pile back used to. */
.pile i{
  background:var(--cardback-art) bottom center / cover no-repeat, #C4B48F;
}
.pile i:nth-last-child(1){ transform:translate(3px,-17px) rotate(1.7deg); }
.pile i:nth-last-child(2){ transform:translate(2px,-22px) rotate(1.4deg); }
.pile i:nth-last-child(3){ transform:translate(2px,-32px) rotate(1.85deg); }
.pile i:nth-last-child(4){ transform:translate(3px,-41px) rotate(1.2deg); }

/* =====================================================================
   BEAT 5 · THE REVEAL · B5-A1 (count leads) / B5-A2 (count quiet)
   Strict order: resolution + count, then the shape of the guess, then
   the explanation, then sources as ONE line.
   §8 NEVER: >1 number at the emotional peak. The tally counts up alone;
   the score and the coins arrive after it has settled.
   IT DOES NOT SCROLL. Only the map and character personalisation ever
   do. fitBeat() scales this block down on a short phone instead.
   ===================================================================== */
/* the top pad is what a short phone can least afford: at 640px of height
   fitBeat() is already scaling the whole beat, and 44px of air above the
   board is 44px the grid pays for. Fluid, so the tall phones keep it. */
.b5{ padding-top:clamp(14px, 5vh, 44px); gap:var(--sp-2); justify-content:flex-start; }
.b5 > *{ flex:none; }
.b5fit{ transform-origin:top center; }

.b5panel{
  background:rgba(0,0,0,.16); box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.10);
  width:calc(100% + 32px); margin:0 -16px; padding:16px var(--sp-4) 14px;
  display:flex; flex-direction:column; gap:var(--sp-2);
}
/* =====================================================================
   THE SEAT GRID · v20 option 4
   ===================================================================== */
/* §S-1 · THE BAR'S TWO FILLS, AND A CONFLICT ON THE RECORD.
   The spec sets בעד to #B6E521 and נגד to #FF7A6B and says they "carry no
   correctness meaning — they are a quantity, not a verdict."

   #B6E521 IS --verdict-correct. It is the claim chip, the cascade stamp
   and the cascade gap, and the rule written at the top of this file and
   in the handoff stylesheet is that colour codes correctness and NEVER
   vote direction — "nothing else may borrow either." The previous finale
   pass removed a green/red pair from exactly this bar for exactly this
   reason: a 17-year-old reads green as good, so the beat was quietly
   saying that voting for a bill is the right answer. By beat 5 the
   player has spent a whole round learning lime = you were right.

   BUILT AS SPECIFIED, because it is a design call and it is Lion's, but
   both palettes are here and the switch is ?f5bar=spec|neutral so the
   two can be compared on a device. `neutral` is the pair the seat grid
   derived and verified: #4E6BFF and #E8DCC0, separated on LUMINANCE
   (0.194 vs 0.721 — a channel no dichromacy touches), dE2000 53.0
   normal / 58.3 deuteranopia / 56.3 protanopia / 43.2 tritanopia, and
   neither carries valence. */
:root{
  --f5-for: #B6E521;
  --f5-ag:  #FF7A6B;
  --f5-rem: #2A2822;          /* the unfilled remainder of 120          */
}
[data-f5bar="neutral"]{
  --f5-for: #4E6BFF;
  --f5-ag:  #E8DCC0;
}

/* the seat grid's stylesheet went with seatBoard() — .b5board, .b5seats,
   .seat, .seat--me, .seat.is-cross, .b5me__lab, .b5tal and the
   --seat-* tokens. See the note at §S-1 in proto.js for why. */

.b5-a2 .b5count{ font-size:22px; color:var(--ink-faint); }
.b5res{
  font-family:var(--font-display); font-weight:900; font-size:44px; line-height:1;
  background:#000; color:#fff; border:var(--card-border) solid var(--edge);
  padding:2px 16px 6px;
}
.b5-a2 .b5res, .b5-nocount .b5res{ font-size:56px; }
.b5outcome{ font-size:12.5px; font-weight:700; color:#8E897C; margin:0; }

/* §1.4c THE 121st VOTE. Compared to the OUTCOME, never to a party or a
   named MK. Usually nothing changes — that is the lesson. */
.b5you{
  display:flex; align-items:center; gap:var(--sp-2);
  font-size:var(--fs-body); font-weight:700; color:#EFECE4;
  background:rgba(255,255,255,.06); border-radius:var(--r-chip);
  padding:9px var(--sp-3);
}
.b5you .as-d{ width:32px; height:32px; flex:none; }
.b5you .num{ color:var(--primary); }

/* §1.8 the SHAPE of the guess, narrated before the score.
   Describes the pattern; never characterises the guesser. */
.b5shape{ font-size:15px; font-weight:700; line-height:1.45; color:#D8D2C4; margin:0; }

.b5exp{ font-size:14.5px; font-weight:700; line-height:1.5; color:#D8D2C4; margin:0; }

/* §1.1 the unfinished sentence, completed. Verb + count, nothing added. */
.b5sentence{
  font-size:14.5px; font-weight:700; line-height:1.45; color:#BEB9AC;
  border-inline-start:2px solid rgba(255,214,10,.5); padding-inline-start:10px; margin:0;
}

.gt{ background:rgba(55,196,255,.22); border-bottom:2px dotted #9FD9F2;
  color:#EFECE4; padding:0 3px; font-weight:900; cursor:pointer; }
/* §3.1 .gdef IS GONE. Tapping a term opened a plain white panel inline,
   under the term, inside the paragraph — so the sentence the player was
   reading reflowed around it as it opened and again as it shut. The term
   now opens .stmodal, the same sticker the law does. Tombstone kept so
   the next person searching for the class finds out where it went. */
/* sources: ONE line. Not a block, not a card, not chips. */
.b5src{ font-size:11.5px; font-weight:700; color:#8E897C; margin:0; }
.b5src a{ color:#8E897C; }

.b5stage{ opacity:0; transform:translateY(6px);
  transition:opacity var(--t-flip) var(--e-settle), transform var(--t-flip) var(--e-settle); }
.b5stage.is-in{ opacity:1; transform:none; }

.b5go{ margin-top:var(--sp-3); }

/* ---- THE PINNED CLAIM · a chyron ------------------------------------
   WHAT THIS REPLACES. hachach.css's .pinned is a pill: absolutely
   positioned, --r-chip radius, --shadow-press, the same shape and weight
   as the .h-a coin chip in the HUD opposite it. Two problems, and the
   second is a defect. It read as a STATUS CHIP — a pip the round had
   awarded — when it is the player's held commitment and has to read as a
   declaration. And being positioned over the play area it sat ON the
   card: measured on the beat-4 frame it covered the first letter of the
   MK's name, so איתמר בן-גביר rendered as יתמר בן-גביר.

   A BAND INSTEAD, IN FLOW. .chyron is a sibling of .hud and .round rather
   than a child of either, so "it never overlaps the card" is structural:
   the round begins where the band ends, at every viewport, and no z-index
   is doing the work. Horizontal, flat, unrotated — the shape reports what
   the player said rather than posturing it.

   IT HOLDS ITS BOX ON BEAT 1. There is nothing to pin before the claim is
   answered, so it renders empty — but it keeps its height. Collapsing it
   would make .round 52px taller on beat 1 than on beats 2-5, and
   sizeStage() would hand the card a different scale on either side of the
   answer. visibility, not display, for exactly that reason.             */
/* THE SLOT KEEPS THE FLOW; THE BANNER LEFT IT. .chyron-slot carries the
   exact box .chyron used to occupy inside #scRound — same min-height,
   same margins — so the round still begins where the band ends and the
   card is the same size on every beat. It holds nothing and paints
   nothing. See the note in index.html for why the banner had to go. */
.chyron-slot{ flex:none; min-height:44px; margin-top:8px; margin-bottom:var(--sp-2); }

/* z-index 11: ABOVE .ov--stage's 9, so the banner flies over the beat-2
   blur rather than under it, and BELOW .hud's 12, so it still passes
   beneath the coin counter it has always sat under. Positioned from
   #chyronSlot's measured box by placeChyron(). */
.chyron{
  position:absolute; z-index:11; pointer-events:none;
  display:flex; align-items:center; gap:var(--sp-2);
  /* §2 · AT LEAST 8px OFF THE HUD, at every width. The band was flush
     against the HUD row — .hud has its own bottom padding but the chyron
     had no top margin, so on the beats where the band is filled the two
     read as one stacked object. margin-top, not a gap on the parent,
     because .sc-round's other children must not gain the same space. */
  min-height:44px;
  color:var(--ink);
}
/* THE BOX IS RESERVED IN ALL THREE STATES. Only the contents change,
   never the height, so the card is the same size whichever state is
   switched on and whichever beat is showing. */
.chyron.is-empty{ visibility:hidden; }

/* ---- band · the full-width chyron -----------------------------------
   §3b REAL TRANSPARENCY. It was a 62% paper film, which over the dark
   ground composites to an almost solid mid-grey — a grey bar with a blur
   nobody can see behind it. A LOW-ALPHA DARK film instead: the ground and
   whatever card is under it genuinely read through, the blur does visible
   work, and the type goes light because the band is now dark.
   §3a the hard ink block down the leading edge is gone with the ::before
   that drew it. §3d so is the avatar — the band carries the answer. */
/* §2 · THE BAND GOES LIGHT, AND THE TYPE GOES DARK WITH IT.
   It was a 30% dark film with light type. Light type on a low-alpha film
   only works while the ground stays dark, and §3.2 puts a warm
   yellow/red gradient behind the map — so the same band would have had to
   carry light type over a light ground. A LIGHT film with INK type is the
   combination that survives both, which is why the re-ink goes this
   direction rather than the other.
   The blur stays: at 72% the ground still reads through it, and the band
   is a film over the round rather than a panel on top of it.

   §2 · THE NEON, AND WHY THIS HUE.
   Every other candidate in the palette is spoken for:
     lime #B6E521 / magenta #FF3BC0  are the verdict pair. A verdict hue on
       the player's own vote banner says their vote was graded, which is
       the exact thing §0 forbids anywhere near the tachles award.
     yellow #FFD60A  is P-C, "the only yellow in the system", and §3.2
       turns the map ground warm yellow/red — a yellow glow would compete
       with the primary button and then disappear against the map.
     the six live topic hues  name a topic, and this band is the player's.
   #37C4FF is already in the system twice — it is the glossary accent
   (.gt's dotted underline) and internal_sec's topic hue — so it
   introduces no new brand colour. internal_sec has no active issue, so
   its node never draws, and §3.2 takes topic hues off the nodes anyway;
   the association is theoretical rather than visible. It is also the only
   palette hue that stays legible against BOTH the charcoal round chrome
   and the warm map gradient, being roughly complementary to the latter.

   A LIT SIGN, NOT A BLOOM. One 2px stroke at full strength, then two
   spreads at 55% and 22% — enough that the edge reads as emitting rather
   than as drawn, and short enough that it does not haze the type inside
   it or fight the sticker language around it. No animation: a pulsing
   chyron would compete with §3.3's next-up node, which is the one thing
   on screen that IS allowed to pulse. */
/* =====================================================================
   THE ROUND BANNER · B-5 sticker + B-1's neon
   .chyron is the RESERVED SLOT and nothing else now — no fill, no edge, no
   width. The object is .bnr inside it, sized to its own contents, which is
   what removes the 67% void the full-width band carried. Both banner
   states are the same construction: only the fill and the skew differ.
   ===================================================================== */
[data-chyron="band"] .chyron{
  background:none; box-shadow:none; padding-inline:0;
  backdrop-filter:none; -webkit-backdrop-filter:none;
  justify-content:flex-start;
}
[data-chyron="band"] .chyron-line{ color:var(--ink); }
[data-chyron="band"] .chyron-line .ph{ color:var(--ink); }
/* ---- .bnr · ONE STICKER, TWO STATES ---------------------------------
   B-5's construction: sized to contents, white die-cut, hard keyline
   outside it, flat extrusion, set down slightly off-square.
   B-1's ACCENT, AND ONLY AS A GLOW. The neon does not replace the
   keyline and is not a second hard rule — there is no `0 0 0 Npx neon`
   term anywhere here. It is two blurred, spread shadows painted BEHIND
   the keyline and the extrusion, so the stacking from the middle out is:
       fill -> 3px white die-cut -> 2px keyline -> soft cyan halo
   The white cut stays the object's edge; the cyan is light coming off it.
   Order in the box-shadow list is load-bearing — the first entry paints on
   top, so the keyline and the extrusion must be listed before the glows or
   the halo washes over them. */
.bnr{
  --neon:#37C4FF;
  display:inline-flex; align-items:center; gap:8px;
  /* PADDING IS OPTICALLY BALANCED, NOT NUMERICALLY. The leading side holds
     a 28px circle and the trailing side holds Hebrew. A circle touches its
     boundary at one point and curves away, so it reads as having more room
     than flat type at the same distance; Hebrew letters like ד, ר and ם
     end on a straight vertical stroke that sits flush. Equal numbers
     therefore look wrong in exactly the way the brief describes — the word
     crowding the edge while the avatar floats.
     TUNED BY MEASUREMENT, NOT BY EYE ALONE. The first attempt swung too
     far the other way — 8px behind the avatar and 17px behind the word
     put the real ink gaps at 5.25px and 16.5px, a 3x difference, because
     the avatar's own 2px ring already eats into its padding while the
     Hebrew sits flush against its own. Rendered at DPR 4 and measured
     from the last inked column to the inner edge, 11px / 12px lands the
     ink at 8.2px and 11.4px — and those two READ as equal, because the
     circle recedes from its tangent point while the flat stroke of ד
     does not. Numerically 3px apart, optically level. */
  padding:6px 6px 7px 6px;
  padding-inline-start:11px;
  padding-inline-end:12px;
  border:3px solid var(--edge);
  border-radius:13px;
  color:var(--ink);
  box-shadow:
    0 0 0 2px var(--keyline),
    0 3px 0 rgba(0,0,0,.34),
    0 0 11px 2px color-mix(in srgb, var(--neon) 52%, transparent),
    0 0 26px 6px color-mix(in srgb, var(--neon) 20%, transparent);
}
/* the two states differ in FILL, in how they are set down, and in whether
   they carry the accent — see below. Same object, same edge. */
.bnr--vote{ background:var(--paper); transform:rotate(1.2deg); }
.cmark.bnr{ background:var(--cm); transform:rotate(-1.6deg); border-radius:999px; }

/* THE GLOW IS THE PINNED VOTE'S, NOT THE CHIP'S, and that is a meaning
   distinction rather than a colour fix. The pinned vote persists for four
   beats and is the 121st-MK conceit while it is on screen, so it earns
   the accent; the correctness chip is on screen for a moment and already
   carries a strong colour of its own. Cyan over magenta also stopped
   reading as light and started reading as a third colour.
   THE HALO IS NEVER TINTED TO THE CHIP'S HUE. That would make the accent
   mean two things, and it would put a lime glow beside a right answer and
   a magenta one beside a wrong answer — which is the accent creeping into
   coding correctness, which it may never do. So it is removed, not
   recoloured. */
.cmark.bnr{
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
}

/* ?neon=off draws the same sticker with the glow removed, so the two can
   be compared on a device rather than argued about. */
[data-neon="off"] .bnr{
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
}

/* THE AVATAR IS RE-INKED BY ITS EDGE, not by repainting AV3. Its ground
   circle is #C9BFA6 cream, which on a cream sticker loses its silhouette
   entirely; a dark keyline restores the boundary without touching the
   inline SVG's fills — which the map node and the HUD also draw. */
[data-chyron="band"] .chyron-av{
  border-radius:50%;
  box-shadow:0 0 0 2px rgba(19,19,16,.72);
}

/* ---- note · a scrap left on the table -------------------------------
   Not a band at all: a small square of paper resting at the leading side
   of the same reserved box, carrying the ANSWER WORD and nothing else —
   no avatar, no lead-in. Low-key by construction: small, off to one side,
   and the only thing in the row. */
[data-chyron="note"] .chyron{
  background:none; box-shadow:none; padding:0;
  justify-content:flex-start;
}
[data-chyron="note"] .chyron-line .ph{ display:none; }
[data-chyron="note"] .chyron-line{
  width:40px; height:40px; display:grid; place-items:center;
  background:var(--paper); color:var(--ink);
  font-size:15px; line-height:1;
  transform:rotate(-2.5deg);          /* set down by hand, not placed    */
  box-shadow:0 2px 0 rgba(0,0,0,.28), 0 0 0 1px rgba(0,0,0,.18);
}

/* ---- off · nothing shown, box still reserved ---------------------- */
[data-chyron="off"] .chyron{ visibility:hidden; }

.chyron-line{
  display:flex; align-items:baseline; gap:var(--sp-2); min-width:0;
  font-family:var(--font-display); font-weight:900; font-size:20px;
  line-height:1; color:var(--ink);
}
/* THE WORD IS NEVER COLOURED BY DIRECTION. אמת and שקר take exactly the
   same ink, the same weight and the same size — the band says what was
   said, and says nothing about whether it was right. */
.chyron-line b{ font:inherit; color:var(--ink); }


/* =====================================================================
   THE THREE SCREENS · intro -> map -> round
   The stage carries data-screen and the screens are `hidden`; only one is
   in the layout at a time, so nothing off-screen can charge the visible
   screen for height. The HUD is OUTSIDE all three — see index.html.
   ===================================================================== */
.screen{ flex:1; min-height:0; display:flex; flex-direction:column; position:relative; z-index:1; }
.screen[hidden]{ display:none; }
/* the intro has nothing to count and nobody to be yet */
.stage[data-screen="intro"] .hud{ display:none; }

/* §5 ON THE MAP THE HUD IS OUT OF FLOW, and that is what makes the map
   pass BEHIND it rather than start below it. It is the same row, the same
   three chips, with no band and no background of its own — the stage's
   charcoal and dot grid run under the chips unbroken, and the path scrolls
   through that gap. Taking it out of flow is also what hands .sc-map the
   whole stage: the screen is the only flex child left, so it fills the
   column without a negative margin anywhere.
   In the round it stays IN FLOW: the card is measured against the height
   the HUD leaves, and floating it there would put the card under it. */
.stage[data-screen="map"] .hud{
  position:absolute; z-index:30;
  top:calc(10px + env(safe-area-inset-top));
  left:calc(16px + env(safe-area-inset-left));
  right:calc(16px + env(safe-area-inset-right));
  background:none;
}
/* the HUD's centre slot. Exactly one of the two children is ever in it,
   and the slot holds the same box either way so the row never jumps. */
.hud-mid{ flex:1; min-width:0; display:flex; justify-content:center; align-items:center; }
.hud-progress[hidden], .hud-topic[hidden]{ display:none; }

/* §3 THE COIN CHIP GOES BACK TO A FULL PILL — deliberately, and it is now
   the only 999px corner in the prototype. See the report: what hachach.css
   CALLS H-A is the 14px chip (--r-chip), and 999px is the SHIPPED app's
   coin chip (styles/home.css:9). The token is not touched, because
   --r-chip is also .v-a, .ib-b, .p-c and the chyron; this is scoped to the
   two chips that carry a NUMBER in the HUD row, so the pair still reads as
   one family and the grammar chips are left where the board put them. */
.hud-coins, .hud-progress{ border-radius:999px; }
.hud-progress{ direction:ltr; font-size:var(--fs-body); }

/* =====================================================================
   1 · INT-D · THE INTRO
   One primary action, and it goes straight to the map. There is no
   character step: §4.1 kills creation-as-first-step outright and the
   default avatar is assigned at boot. The flowmap's Intro -> Create -> Map
   is superseded; a stub would be a screen we know is wrong.
   ===================================================================== */
/* EVERY VERTICAL NUMBER HERE IS TIED TO VIEWPORT HEIGHT, and it has to
   be: the intro is the one screen with no card to scale, so if the
   composite does not shrink it clips — the stage is overflow:hidden and
   would simply eat the CTA. Margins are all explicit for the same reason;
   the UA's 1em <p> margin is 26px under a 26px sub-head and was quietly
   spending a fifth of the slack on a 667px phone. */
/* A1 · ONE COMPOSED GROUP, SHIFTED UP. With the lede pill and the note
   gone, the four things left — title, chair, tagline, CTA — are centred as
   a block rather than spread between the top and the bottom edges. The CTA
   keeps a small auto margin so it settles at the foot of the group instead
   of being pushed to the foot of the screen. */
.sc-intro{ align-items:center; text-align:center;
  justify-content:center; padding-top:clamp(6px, 1.6vh, 14px); }
/* the plaza the building stands on */
.sc-intro::before{
  content:""; position:absolute; left:-16px; right:-16px; bottom:-12px; height:62%;
  z-index:0; pointer-events:none;
  background:linear-gradient(to top,#6E6242 0%,#514A38 38%,rgba(64,62,58,0) 100%);
}
.i-tag{ flex:none; position:relative; z-index:5; margin:clamp(6px,1.5vh,16px) 0; }
/* THE TITLE AND THE CHAIR ARE ONE COMPOSITE STICKER. The chair sits ON the
   letters and both carry the same white die-cut edge — the overlap of the
   two cuts is the whole idea, and it only works because neither is boxed. */
.i-comp{ position:relative; z-index:5; display:flex; flex-direction:column;
  align-items:center; margin-top:6px; flex:none; }
.i-title{ display:flex; flex-direction:column; align-items:center; gap:2px; position:relative; z-index:4; }
/* the chair sits ON the letters — the two die-cut edges overlapping IS
   the composite — so the negative margin is the whole point of it. Sized
   off viewport height for the same reason the title is. */
/* ITEM 42 · THE SHADOW'S THREE NUMBERS ARE REGISTERED PROPERTIES, and
   they have to be. filter cannot be tweened here: the chain starts with
   url(#dcw), and a filter list containing a url() reference interpolates
   DISCRETELY — the shadow would jump from 4px to 10px at the midpoint
   instead of tracking the chair, which is the one thing this effect is
   made of. Registering the numbers gives them a type, so THEY tween and
   the filter is simply recomputed from them each frame.
   Initial values are the resting shadow exactly as it was, so anything
   that does not run the float still paints the shadow it always had. */
@property --i-chair-sy{ syntax:'<length>'; inherits:false; initial-value:4px; }
@property --i-chair-sb{ syntax:'<length>'; inherits:false; initial-value:0px; }
@property --i-chair-sa{ syntax:'<number>'; inherits:false; initial-value:0.3; }

.i-chair{ display:block; position:relative; z-index:3;
  margin-top:clamp(-64px, -7.5vh, -40px);
  width:auto; height:clamp(150px, 36vh, 324px); aspect-ratio:278 / 324;
  filter:url(#dcw) drop-shadow(0 var(--i-chair-sy) var(--i-chair-sb)
                               rgba(0,0,0,var(--i-chair-sa))); }

/* =====================================================================
   ITEM 42 · THE INTRO CHAIR FLOATS
   THE SHADOW STAYS ON THE GROUND. That is the whole effect and it is why
   the offset counter-animates: the chair rises 6px and the shadow's own
   offset grows 4 -> 10px, so its painted position never moves. A shadow
   that rises with the object reads as the sticker being nudged; a shadow
   that stays put reads as the object leaving the ground. It also softens
   as it goes — blur 0 -> 2px, alpha .30 -> .22 — because a thing further
   from its ground casts a larger, weaker shadow.
   The 2px of blur is the one soft moment in a hard-shadow language, and
   it is deliberate: it is transient, it is 2px, and without it the
   shadow reads as painted on rather than cast.
   5500ms AND IT MUST NOT BE 3500 OR A MULTIPLE OF IT. The CTA breathes
   on 3500 (item 10); at 5500 the two realign only every 38.5s, so they
   never settle into a shared beat and read as one mechanism. This is its
   own literal, deliberately NOT the CTA's token.
   `translate`, NOT `transform`. The loading beat drives this same chair
   with an inline `transform` and a transition on it, and keeping the
   float on a different property means the two can never clobber one
   another whatever the ordering.
   AMBIENT, SO IT NEVER STOPS — except that the loading beat MEASURES
   this chair to compute its launch, and a floating chair would be
   measured 0-6px off. loadingBeat() sets sc.dataset.loading before it
   measures, so :not([data-loading]) grounds the chair on that same tick,
   before the rect is read. No JS was needed and none was added.
   ===================================================================== */
.sc-intro:not([data-loading]) .i-chair{
  animation:i-chair-float 5500ms ease-in-out infinite both;
}
@keyframes i-chair-float{
  0%,100%{ translate:0 0;
           --i-chair-sy:4px;  --i-chair-sb:0px; --i-chair-sa:0.30 }
  50%    { translate:0 -6px;
           --i-chair-sy:10px; --i-chair-sb:2px; --i-chair-sa:0.22 }
}
/* no float at all — not a 1ms one. `animation:none` kills the NAME, which
   the global reduce rule's animation-duration:1ms !important cannot
   override, so the chair simply sits still with its resting shadow. */
@media (prefers-reduced-motion: reduce){
  .sc-intro:not([data-loading]) .i-chair{ animation:none; }
}
/* §2 · A SOFT DARK LIFT BEHIND THE TAGLINE, and this is the element that
   actually needed it: measured at 360, .i-sub and .i-para both cross the
   building — the roof band and the flags — while the title clears it at
   every viewport and sits on the charcoal ground.
   TWO BLURRED LAYERS, NO OFFSET-HEAVY DROP. A tight one for the local
   contrast right at the glyph edge and a wide one that darkens the whole
   patch behind the line, both pure black at partial alpha. There is no
   0-blur term, so it can never read as an outline, and no colour, so it
   can never read as a glow. */
/* T1 · ONE LINE, AT THE QUESTION'S SIZE, IN SimplerPro REGULAR.
   THE FAMILY IS NEW HERE AND THAT IS THE POINT. This rule named no family,
   so it inherited --font-ui from body — system-ui, where `700` is a real
   semibold. The line is specified as SimplerPro, which ships 400 and 900
   and nothing between, so naming the family forces the weight to be
   chosen rather than inherited: left at 700 it would resolve UPWARD and
   silently print the Black across every line of it.
   400, AND NOT 900, for three reasons. It is a fourteen-word sentence
   running four to five lines, and the Black at 21-26px over five lines is
   a wall rather than a voice. The title directly above it is already
   SimplerPro Black at 52-84px, and a second Black block under it competes
   with the one thing on the screen that should dominate. And the
   standfirst this absorbs was already --font-body at 400, so the copy it
   replaces keeps its weight rather than gaining one.
   THE MEASURE AND THE LEADING COME WITH IT. At the question's size this is
   now a paragraph, not a label: 20ch keeps the line count honest at 360
   and 1.3 gives SimplerPro's tall em box somewhere to sit. */
.i-sub{ margin:clamp(10px,2.4vh,20px) 0 0; font-size:clamp(21px,3.1vh,26px);
  font-family:var(--font-body); font-weight:400; line-height:1.3;
  max-width:20ch; text-wrap:balance;
  color:#EFECE4; position:relative; z-index:4; flex:none;
  text-shadow:0 1px 5px rgba(0,0,0,.82), 0 2px 16px rgba(0,0,0,.62); }
/* T1 · .i-para IS NO LONGER RENDERED. The rule stays because the string
   stays — INTRO_COPY.para survives unrendered and putting it back is one
   line in renderIntro(), which is the arrangement the note there already
   describes for `note` and `lede`. */
/* 21 · THE STANDFIRST INHERITED --font-ui FROM body AND NAMED NO FAMILY,
   so switching it means stating one. 700 -> 400 for the same reason as
   .eg-eyebrow: 700 would resolve to the Black. The colour is NOT touched
   by item 22 — #EFECE4 measures 7.31:1 on the lightest dot, comfortably
   over the 4.5 floor, and --ink-body (4.56) would DARKEN it. */
/* 25 · EMPHASIS WITHOUT WEIGHT. This line carries the game's positioning
   and it sits over the illustration, where the ground is busiest. It was
   system-ui 700; item 21 moved it to SimplerPro, and the family has no
   500/600/700 to fall back on, so the two levers left are SIZE and INK.
   Size: the clamp is raised a step rather than replaced, so the short
   phones keep a floor that fits — 19px at 390x844 (was 16), 16px at 667
   and 640 (was ~14.6). The 1.62 ratio is unchanged.
   Ink: #EFECE4 -> #FBF7EE, the system's own paper white, which is what the
   display type on this screen already uses. NOT 900 — the title and the
   CTA are both 900 and a third would flatten the hierarchy, which is why
   this is a size-and-ink change and not a weight one. */
/* ITEM 41 · 1.62 -> 1.4, AND ONLY HERE. The 1.62 came from the body-face
   work, where it paid for SimplerPro's 7px descent against system-ui's
   4px — and it was tuned at 17px on the allocation screen, which runs
   many lines. This is two short centred lines at 19px, where the same
   ratio buys 30.8px of leading and the gap between the rows starts
   reading as a space rather than as a paragraph.
   SCOPE IS THIS SELECTOR. --font-body's own line-height is untouched and
   so are the other seven elements switched in item 21; their 1.6 was
   measured against their own sizes and none of them is two centred lines
   at 19px. */
.i-para{ margin:clamp(6px,1.2vh,10px) 0 0; max-width:30ch;
  font-family:var(--font-body);
  font-size:clamp(16px,2.3vh,19px); font-weight:400;
  line-height:1.4; color:#FBF7EE; position:relative; z-index:4; flex:none;
  text-shadow:0 1px 5px rgba(0,0,0,.82), 0 2px 16px rgba(0,0,0,.62); }
.i-stage{ position:absolute; left:-16px; right:-16px; bottom:-12px; top:0;
  z-index:1; pointer-events:none; overflow:hidden; }
.i-build{ position:absolute; left:0; right:0; bottom:0; width:100%; display:block; }
/* a scrim OVER the building and under the type. On a 667px phone the
   paragraph and the note come down onto the flags and the road stripes,
   and light type on that is unreadable. It sits inside .i-stage so it can
   never get between the composite and the viewer. */
/* §2 · THE SCRIM CARRIES THE REST, and this is the right lever rather than
   a heavier text-shadow. Measured under the glyph strokes at 360, the
   paragraph crosses the building's lit roof band at 2.13:1 — the shadow
   alone takes it to 3.87:1, past AA-Large but short of the 4.5:1 body
   text needs, and the brief says not to push the shadow further.
   The scrim is the mechanism already designed for exactly this case; it
   was simply too weak where the paragraph lands. It reached only ~31%
   black at the paragraph's height because the .55 stop sat at 46% of a
   46%-tall band. Raised to 56% tall with the mid stop held further up, so
   the type band is genuinely dark while the gradient still fades to
   nothing well before the roof line — the building keeps its own light.
   TUNED TO THE MINIMUM THAT CLEARS AA, by sweeping four candidates and
   measuring both the paragraph's contrast and the mean luminance of the
   building band underneath it: 50%/.78 gives 4.76:1, this gives 5.49:1,
   56%/.86 gives 7.01:1. The building band only moves 0.2251 -> 0.2122
   across that whole range, so the scrim costs the illustration far less
   than it looks like it does; this setting takes the margin over AA and
   still leaves the building 1.6% brighter than the heaviest candidate. */
.i-stage::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:52%; z-index:2;
  background:linear-gradient(to top,rgba(43,41,38,.80),rgba(43,41,38,.58) 52%,rgba(43,41,38,0));
}
/* the action sits with the group, not at the bottom of the screen — that
   is the whole point of A1's rebalance. A capped top margin keeps it
   clearly separate from the paragraph without opening a gap. */
/* §1 · DOWN 12px. Every bound of the top clamp moves by the same 12, so
   the gap grows by 12 at every viewport rather than only where the middle
   term is active — clamp(18,4vh,34) at 360 resolves to its 4vh term, at
   393 to its ceiling, and bumping only one of the three would have moved
   some phones and not others. Re-measured below the fold check. */
/* THE 12px COMES OUT OF THE BOTTOM MARGIN, NOT OUT OF THE COLUMN.
   .sc-intro is justify-content:center, so simply growing the top margin
   grows the whole group and the centring gives back half of it — the
   first attempt moved the CTA down 6px and pushed the title UP 6px.
   Adding 12 above and removing 12 below leaves the group exactly as tall
   as it was, so nothing above the CTA moves and the CTA moves the full
   12. Verified: title top unchanged, CTA top +12 at every width. */
.i-cta{ margin:clamp(30px,calc(4vh + 12px),46px) 0 clamp(0px,calc(2.4vh - 12px),8px);
  width:280px; font-size:clamp(21px,3.1vh,26px);
  position:relative; z-index:6; flex:none; }

/* =====================================================================
   ITEM 10 · THE CTA BREATHES
   ARRIVAL FIRST, THEN INVITATION. 1200ms of stillness before the first
   fire, so the screen is read before anything on it asks for a tap; then
   every 3500ms, which is long enough that the button is at rest almost
   all the time and the motion is an event rather than a texture.
   THE SHADOW TRAVELS WITH IT, AND THAT IS THE WHOLE EFFECT. A scale with
   a fixed shadow reads as a CSS pulse — the object grows but stays stuck
   to the page. Moving the hard offset 4 -> 6px says the sticker lifted
   off the ground, which is the one thing that makes it physical. If the
   shadow ever stops moving, the effect is dead and should be removed
   rather than kept.
   The 3-4.6px edge and keyline rings are re-stated at every keyframe
   because box-shadow interpolates as a LIST: drop a layer at one frame
   and the whole stack animates from nothing.
   TRANSFORM ONLY otherwise. No margin, no padding, no width — the button
   is in a centred flex column and any box change would move the
   paragraph above it.
   NO GLOW, NO HALO, NO SWEEP. The warm yellow-cream is the S-1 finale
   board's alone, and a diagonal sweep on a die-cut object reads as a
   loading skeleton.
   ===================================================================== */
.i-cta.is-breathing{
  transform-origin:center;
  animation:i-cta-breathe 3500ms linear 1200ms infinite both;
}
@keyframes i-cta-breathe{
  /* 0 -> 420ms · out */
  0%      { transform:scale(1) rotate(0deg);
            box-shadow:0 0 0 3px var(--edge), 0 0 0 4.6px var(--keyline),
                       0 var(--z-press) 0 var(--primary-base);
            animation-timing-function:cubic-bezier(.2,.8,.2,1) }
  /* 420 -> 720ms · back */
  12%     { transform:scale(1.035) rotate(-0.8deg);
            box-shadow:0 0 0 3px var(--edge), 0 0 0 4.6px var(--keyline),
                       0 calc(var(--z-press) + 2px) 0 var(--primary-base);
            animation-timing-function:ease-in-out }
  /* 720 -> 3500ms · at rest, waiting */
  20.571% { transform:scale(1) rotate(0deg);
            box-shadow:0 0 0 3px var(--edge), 0 0 0 4.6px var(--keyline),
                       0 var(--z-press) 0 var(--primary-base) }
  100%    { transform:scale(1) rotate(0deg);
            box-shadow:0 0 0 3px var(--edge), 0 0 0 4.6px var(--keyline),
                       0 var(--z-press) 0 var(--primary-base) }
}
/* NO ANIMATION AT ALL, not a 1ms one. The global reduce rule shortens
   animations; an invitation that fires in 1ms every 3500ms is a flicker.
   startBreath() also refuses to add the class — this is the backstop. */
@media (prefers-reduced-motion: reduce){
  .i-cta.is-breathing{ animation:none; }
}

/* ---- the display title, letter by letter --------------------------
   Each glyph is its own <svg><text>, which is how the white cut can
   overlap its neighbour's — a single <text> would paint one stroke around
   the whole run. paint-order puts the stroke behind the fill so the cut
   reads as a cut and not as an outline.
   AND IT IS IMMUNE TO THE WEBKIT BIDI BUG: one character per <text> has no
   order to get wrong. The right-to-left run is done by RTL inline-flex on
   .i-ls, which is HTML layout, not SVG text shaping. */
/* THE ONE SIZE THE TITLE HAS. .g is sized in em, so without this the
   glyphs inherit the body's 14px and the composite collapses. Tied to
   viewport HEIGHT, not width: height is what runs out on a 667px phone,
   and the chair below it is competing for the same space. */
.i-ls{ display:inline-flex; align-items:center; line-height:1;
  font-size:clamp(52px, 9.6vh, 84px); }
/* a run of Western numerals, kept LTR inside the RTL row */
.i-ls .i-run{ display:inline-flex; direction:ltr; flex:none; }
/* §2 · the title gets LIFT, not contrast. It already carries a white
   die-cut keyline and it does not cross the building at any viewport, so
   a heavy shadow here would only muddy the keyline. The existing hard 2px
   is kept — that is the sticker's own edge — and one soft blurred layer
   is added under it so the sheet lifts off the ground. */
.i-ls .g{ width:.86em; height:1em; overflow:visible; flex:none;
  margin:0 -.115em; position:relative;
  filter:drop-shadow(0 2px 0 rgba(0,0,0,.30))
         drop-shadow(0 4px 11px rgba(0,0,0,.45)); }
.i-ls .g:nth-child(2n){ z-index:2; }
.i-ls .g:nth-child(3n){ z-index:3; }
.i-ls text{
  font-family:var(--font-display); font-weight:900; font-size:92px;
  text-anchor:middle; fill:var(--ink); stroke:var(--edge); stroke-width:29px;
  stroke-linejoin:round; paint-order:stroke fill;
}
/* §3 · THE COLOURED RING, painted UNDER the white cut and nowhere else.
   fill:none so it contributes no letterform at all — it is an edge and
   only an edge, which is the whole difference from the rejected filled
   version. The stroke colour is set per glyph by lsGlyph().
   WIDTH: 36 against the white's 29, so 3.5 user units show on each side.
   The SVG's 100 units map to .86em of a fluid font-size, so that lands at
   1.9px at 360, 2.5px at 393 and 2.6px at 430 — the "~2px" the brief asks
   for at the small end, drifting up slightly on the larger phones because
   the whole title scales with the viewport. Holding a literal 2px at
   every width would need non-scaling-stroke, which cannot be combined
   with a width expressed relative to the white stroke underneath it. */
.i-ls text.g-ring{
  fill:none; stroke:var(--ring); stroke-width:36px; stroke-linejoin:round;
}
/* the soft lift added in §2 sits exactly where the ring does and was
   swallowing it, so a ringed glyph keeps the hard 2px edge shadow and
   drops the blur — the ring is doing that job for these two variants. */
.i-ls .g--ring{ filter:drop-shadow(0 2px 0 rgba(0,0,0,.30)); }

/* =====================================================================
   2 · THE PATH MAP
   The one surface in the app that scrolls, and the only reason .scrolls
   exists. Bottom to top: node 1 is at the FOOT of the path, which is why
   the window opens parked near the bottom and climbs.
   ===================================================================== */
/* §4 THE MAP HAS NO GROUND OF ITS OWN. It used to carry the board's
   .path-glow — two radial washes over a warm-to-blue vertical ramp — which
   is the only place in the app where the ground changed under a screen,
   and it made the map look like a different product from the round. It is
   gone. What shows through is .stage's own charcoal and its 4px/7px dot
   grid, unbroken from the top of the HUD to the foot of the path, which is
   what every other screen stands on. */
/* §5.3 · THE MAP SURFACE REACHES ALL FOUR VIEWPORT EDGES.
   THE CAUSE, measured rather than guessed. .stage carries
   `padding: 10px+safe-top / 16px / 12px+safe-bottom / 16px`, and .mapwin
   cancelled the HORIZONTAL half of that with `margin:0 -16px` and nothing
   at all vertically. So the map's scroll surface has always stopped 10px
   below the viewport top and 12px above the bottom.
   IT WAS INVISIBLE BECAUSE THE TWO GROUNDS MATCHED: the map had no ground
   of its own and what showed in those strips was .stage's charcoal, the
   same charcoal the map was drawn on. What made it visible was the
   ribbon — the road ran to .mapwin's edge and stopped 10/12px short of
   the screen, which is what reads as "the map is sitting in a box".
   §3.2's gradient would have made it obvious in a different way: a
   gradient surface with a flat charcoal strip above and below it.
   So this is NOT the styles/base.css `--vh` bug — .stage measures
   0->852 at 393x852, full height, and 100dvh does win here because
   proto.css puts it in an @supports block AFTER the --vh declaration.
   base.css:25 has the opposite order and that IS broken, but it is the
   shipped app's file and a different screen; see the report.
   THE FIX is to give the map back the padding on all four sides, not
   just two. The HUD floats over it at z-index 12, which it already did. */
.sc-map{ min-height:0; }
.mapwin{
  position:relative; flex:1; min-height:0;
  margin:calc(-10px - env(safe-area-inset-top)) -16px
         calc(-12px - env(safe-area-inset-bottom));
}
/* the HUD and the jump button keep their inset — only the SURFACE bleeds */
.sc-map .map-jump{ bottom:calc(14px + env(safe-area-inset-bottom)); }
.path{ position:relative; width:100%; }

/* §2 THE CONNECTOR IS THE BOARD'S RIBBON, not a bead trail. The v7 path
   study picked it outright — "a ribbon with a lit top and a shadowed
   under-edge, so the path carries the same thickness the nodes do" —
   and the archive note records it as already applied to the v8 map. The
   dotted variant it replaces was the treatment the study was run to
   REPLACE. Both strokes and both colours are PathRoad's own.
   The taper is the two strokes, not a width ramp: 23px of dark offset 6px
   down, 19px of lighter laid on top, so 2px of shadow shows on each side
   and 6px along the bottom — the same lit-top-over-dark-base construction
   the discs, the chips and the cards are all built from. */
.path-line{ position:absolute; inset:0; width:100%; height:100%; z-index:1; overflow:visible; }
/* §3.2 the ribbon is re-toned for the warm ground. The old #2A2924/#615D53
   pair was mixed for charcoal and read as a dead grey road on the
   gradient. Same construction — dark base offset 6px, lighter top laid
   over — with both tones pulled toward the ground's own warmth, so the
   road belongs to the map instead of sitting on it. */
.pl-under{ fill:none; stroke:#3A2119; stroke-width:23; stroke-linecap:round;
  transform:translateY(6px); }
.pl-dots{ fill:none; stroke:#7A5B49; stroke-width:19; stroke-linecap:round; }

.node{ position:absolute; z-index:2; width:var(--node-box);
  display:flex; flex-direction:column; align-items:center; }
/* THE GAP UNDER THE NODE IS PART OF THE 15%. It is not the margin — it is
   the distance from the base of the disc to the label, and the ring box
   closed up by 2px when the overhang went, so the margin has to make up
   the difference as well as grow. Board: 132 - (24.5+76+7) + 2 = 26.5px.
   Now: 130 - (17.5+87+8) + 13 = 30.5px, which is 26.5 x 1.15. */
/* §0b · THE SAME LIFT THE INTRO TAGLINE GOT, for the same reason and by
   the same mechanism. On flat charcoal this type was fine; §3.2's warm
   gradient put the labels over ground that runs to #F2B33C at the foot of
   the map, and #EFECE4 on that is nearly invisible. Two blurred black
   layers, no 0-blur term and no colour, so it is lift and never an
   outline or a glow. The status line gets it too and needs it more — it
   is smaller and it is --ink-faint rather than white. */
.node-name{ margin-top:13px; font-size:16px; line-height:1.2; text-align:center;
  color:#EFECE4; font-family:var(--font-display); font-weight:900;
  text-shadow:0 1px 4px rgba(0,0,0,.92), 0 2px 14px rgba(0,0,0,.78); }
/* THE STATUS LINE IS ALSO RE-INKED, and it had to be. --ink-faint is
   #BEB9AC — a light grey chosen for a dark ground — and on the gradient's
   light end it starts at 1.24:1, which no shadow can rescue: a shadow
   raises contrast by darkening what is BEHIND the glyph, and there is a
   limit to how much of a light ground a blur can darken before it reads
   as a plate. #E4DFD3 gives the shadow something to work with while
   staying clearly subordinate to the 16px display name above it.
   Measured ceiling for this mechanism is in the report — it clears
   AA-Large but not AA at the very foot of the map. */
.node-status{ font-size:13px; font-weight:700; color:#E4DFD3;
  text-shadow:0 1px 4px rgba(0,0,0,.92), 0 2px 14px rgba(0,0,0,.78); }

/* ---- the node itself ----------------------------------------------
   THE RING IS A SEPARATE ELEMENT BEHIND THE NODE and nothing the node
   carries may touch it. The face is 76px centred on (66,62.5); the ring is
   r=63 with a 6px stroke centred on (66,66) — 3.5px lower, because it is
   centred on the face PLUS its 7px base, not on the face alone. Both
   badges are inset from the face far enough that their casings clear it
   too — see .node-check below. */
.ringnode{ position:relative; display:block; overflow:visible;
  width:var(--node-box); height:var(--node-box-h); }
.ring{ position:absolute; left:0; top:0; z-index:1; overflow:visible;
  width:var(--node-box); height:var(--node-box-h); }
.seg{ stroke-width:var(--ring-w); fill:none; stroke-linecap:round; }
/* THE CASING CARRIES FULL WEIGHT NOW. It used to be a 4px arc at 72%
   opacity, which was legible when the ring hugged the disc at 6px; at the
   18.5px stand-off the icon's overhang forces, a thin pale arc stopped
   reading as part of the node and became two brackets floating near it.
   Both states are --ring-w, and the difference is VALUE and hue, not
   thickness — so the ring reads as one track with part of it filled,
   which is what a progress ring has to look like.
   The unplayed state is still legible without colour: it is a dark inert
   casing against the topic's own saturated hue. */
/* §3.2 · ONE NODE COLOUR, AND THE STATES MOVE FROM HUE TO VALUE.
   The disc used to carry the topic hue and the ring used the same hue for
   its filled segments, so "which topic" and "how far through it" were the
   same variable at two saturations. The sticker names the topic now, so
   the disc goes to one shared ink for all six and the ring carries state
   alone — CREAM for done, dark translucent for not. That is a value
   contrast rather than a hue contrast, which is what keeps it legible
   against a gradient that runs from #F2B33C to #8E2A2E; a hue-coded ring
   would have had to fight a different background colour at every node.
   The per-topic hues stay in data.js and are still written onto the node
   as --tc; nothing reads them here. Presentation only, as briefed. */
.seg-off{ stroke:rgba(24,20,16,.42); }
.seg-on { stroke:var(--paper); }

/* §1 THE DISC CARRIES THE HUE, AND THE ICON SITS ON CREAM. This reverses
   the neutral-dark disc: on device that lost the eight-hue topic system
   entirely, floated the icons on near-black, and sank the extrusion into a
   ground that was almost the same value as the base.
   The hue is now the raw data.js topic colour rather than a wash of it, so
   the disc reads as the topic from across the screen, and the base is that
   colour multiplied to 78% — a flat multiply, so the hue is preserved
   exactly and the seam between face and base cannot show. Against the
   charcoal ground a saturated disc over a darker version of itself is the
   same figure-on-ground the cards and chips use.

   DUOLINGO EXTRUSION, NOT A DROP SHADOW. Zero blur, zero spread, offset
   straight down by the depth. */
.node{ --node-fill:#2E2A26; --node-base:#171412; }
.node-face{
  position:absolute; left:var(--node-face-x); top:var(--node-face-y); z-index:2;
  width:var(--node-face); height:var(--node-face); border-radius:50%;
  border:0; padding:0; cursor:pointer;
  background:var(--node-fill);
  box-shadow:0 var(--node-depth) 0 0 var(--node-base),
             0 0 0 1.5px rgba(0,0,0,.42);
  transition:transform var(--t-node-press) linear, box-shadow var(--t-node-press) linear;
}
.node-face:active{ transform:translateY(var(--node-depth));
  box-shadow:0 0 0 0 var(--node-base), 0 0 0 1.5px rgba(0,0,0,.42); }
.node-face:focus-visible{ outline:3px solid var(--edge); outline-offset:3px; }

/* =====================================================================
   ITEM 43 · THE SUGGESTED FIRST NODE
   Item 10's breathing sticker, on a map disc: the same 3500ms cycle, the
   same 1.035 / -0.8deg, the same shadow travelling 2px out and back with
   it. Only the shadow's construction differs, because .node-face's
   extrusion is --node-depth on --node-base rather than the CTA's
   --z-press on --primary-base — and the shadow has to be restated at
   every frame, or the frames that omit it interpolate from "no shadow"
   and the disc's extrusion blinks.
   ON .node-face, NOT ON THE WRAPPER, and that is the shadow's doing: the
   extrusion lives on this element, so travelling it means animating this
   element's box-shadow. Scaling .ringnode instead would have scaled the
   shadow rather than moved it, which is the failure item 42 names — the
   sticker being nudged instead of the object lifting.
   THE PRESS STILL READS. A running animation outranks the :active
   transition, so a breathing disc could not show its own press — except
   that breatheFirstNode() removes the class on pointerdown, which is the
   same event :active begins on. The animation is gone by the frame the
   finger lands and --t-node-press takes it from there.
   The 1200ms first-fire delay is item 10's, unchanged: the sticker has
   just closed and the map should be still for a moment before anything
   on it moves. */
.node-face.is-breathing{
  animation:node-breathe 3500ms linear 1200ms infinite both;
}
@keyframes node-breathe{
  /* 0 -> 420ms · out */
  0%      { transform:scale(1) rotate(0deg);
            box-shadow:0 var(--node-depth) 0 0 var(--node-base),
                       0 0 0 1.5px rgba(0,0,0,.42);
            animation-timing-function:cubic-bezier(.2,.8,.2,1) }
  /* 420 -> 720ms · back */
  12%     { transform:scale(1.035) rotate(-0.8deg);
            box-shadow:0 calc(var(--node-depth) + 2px) 0 0 var(--node-base),
                       0 0 0 1.5px rgba(0,0,0,.42);
            animation-timing-function:ease-in-out }
  /* 720 -> 3500ms · at rest, waiting */
  20.571% { transform:scale(1) rotate(0deg);
            box-shadow:0 var(--node-depth) 0 0 var(--node-base),
                       0 0 0 1.5px rgba(0,0,0,.42) }
  100%    { transform:scale(1) rotate(0deg);
            box-shadow:0 var(--node-depth) 0 0 var(--node-base),
                       0 0 0 1.5px rgba(0,0,0,.42) }
}
/* NO ANIMATION AT ALL, not a 1ms one — item 10's argument verbatim, and
   the item asks for it explicitly: the sticker still shows, the node does
   not breathe. breatheFirstNode() also refuses to add the class; this is
   the backstop. */
@media (prefers-reduced-motion: reduce){
  .node-face.is-breathing{ animation:none; }
}

/* ---- THE ICON SITS DIRECTLY ON THE DISC ---------------------------
   §1.4 resolves the standing A3 conflict in favour of the newer
   instruction. The cream stadium tile and the 14px overhang are both
   gone; the icon lies flat on the hue, optically centred in the face,
   which is what the earliest node version did.

   WHAT STILL SEPARATES A CORAL ICON FROM A CORAL DISC is the die-cut —
   url(#dcw-sm), the 2px white dilate the avatars carry (the cards' own
   edge, #dcw, is 5px; this is the small one). The earlier
   instruction was "pick one, do not do both": the tile and the die-cut
   were the two candidates, the tile won that round, and removing the tile
   promotes the die-cut rather than leaving the icon unseparated. It
   follows the object's silhouette instead of boxing it, so the cream
   still shows through the gaps in the scales and the hole in the
   magnifier — which is where most of the separation came from anyway.

   OPTICALLY CENTRED, NOT BOX CENTRED. The face is 87px with an 8px base
   under it, so the composite's centre of mass sits below the face's; the
   icon is lifted 1.5px off the geometric centre so it reads as sitting in
   the middle of the disc rather than slightly low on it. Everything is
   measured from the FACE, so the icon still inherits the 80ms translateY
   the disc takes on press — it is a child of the face, not a sibling.

   IT MUST CLEAR THE RING, and it does with room to spare. The widest icon
   in the set is the scales at 56.6 x 49.6 (gender, node_scale 1.11), whose
   half-diagonal is 37.6px from the face centre — 46.0 from the ring
   centre once the ring's 4px drop is taken in — against the ring's 54.5px
   inner edge. The tallest, the receipt, is 29.2 x 54.9 and clears by
   more. Nothing in the set touches the ring at any of the three widths. */
.node-ico{
  position:absolute; left:50%; top:calc(50% - 1.5px); z-index:3;
  transform:translate(-50%, -50%);
  display:block; pointer-events:none;
  filter:url(#dcw-sm);
}
/* the emoji fallback has no die-cut to give it — it is a glyph, not art */
span.node-ico{ font-size:44px; line-height:1; filter:drop-shadow(0 2px 0 rgba(0,0,0,.28)); }

/* the two badges. The number is always there — nodes are numbered, and a
   number that vanishes on completion stops being an address. */
.node-num{
  position:absolute; left:6px; bottom:6px; z-index:3;
  width:24px; height:24px; border-radius:50%;
  display:grid; place-items:center;
  direction:ltr; font-family:var(--font-display); font-weight:900; font-size:14px;
  background:var(--paper); color:var(--ink);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45), 0 2px 0 rgba(0,0,0,.3);
}
/* THE BADGES CLEAR THE RING, halo included, and they still do at 15%.
   24px inset 6px on an 87px face puts a badge's far corner 50.3px from
   the RING centre — 51.8 with its 1.5px casing — against the ring's
   54.5px inner edge. That is 2.7px of clearance where the board's
   21/5 against r=50.5 had 1.7px, so the rule "nothing may overlap the
   ring, shadows included" holds by a wider margin than before. */
.node-check{
  position:absolute; right:6px; bottom:6px; z-index:3;
  width:24px; height:24px; border-radius:50%;
  display:grid; place-items:center;
  font-size:14px; font-weight:900;
  background:#2B2926; color:#fff; border:2px solid var(--paper);
  box-shadow:0 2px 0 0 #161413;
}
/* THE SOFT NUDGE, and it is the only one. No lock, no prerequisite, no
   "pick needed" warning: free choice is a SET decision (§3.1) and the
   current node is a suggestion for choice-paralysis, nothing more. */
/* §3.3 · THE SLOW PULSE, and it is a nudge and nothing more.
   Behind the node, never on it: a soft disc that breathes between 0.94
   and 1.16 over 3.4s. Slow enough that it reads as ambient rather than as
   an alert, and it never touches the node's own geometry, so a pulsing
   node and a still one are the same size and the same shape — nothing
   about it says "you must play this one". The map stays free choice.
   NO MOTION UNDER prefers-reduced-motion: the glow holds at its midpoint
   so the node is still marked, it simply stops moving. The global
   reduced-motion rule at the foot of this file caps durations at 1ms,
   which for an infinite alternate loop would strobe — hence the explicit
   `animation:none` here rather than relying on it. */
.node.is-current .ringnode::before{
  content:""; position:absolute; z-index:0;
  left:50%; top:calc(var(--node-face-y) + var(--node-face) / 2);
  width:150px; height:150px; margin:-75px 0 0 -75px;
  border-radius:50%; pointer-events:none;
  background:radial-gradient(circle,
    rgba(255,255,255,.34) 0%, rgba(255,255,255,.16) 42%, rgba(255,255,255,0) 70%);
  animation:node-pulse 3400ms var(--e-settle) infinite alternate;
}
@keyframes node-pulse{
  from{ transform:scale(.94); opacity:.55 }
  to  { transform:scale(1.16); opacity:1 }
}
@media (prefers-reduced-motion: reduce){
  .node.is-current .ringnode::before{
    animation:none; transform:scale(1.05); opacity:.8;
  }
}
.node.is-current .seg-off{ stroke:rgba(24,20,16,.55); }
/* §3.3 · THE NEXT-UP NODE. A white keyline plus the pulse behind it —
   see .node.is-current .ringnode::before. */
.node.is-current .node-face{
  box-shadow:0 var(--node-depth) 0 0 var(--node-base),
             0 0 0 3px var(--edge), 0 0 0 4.6px rgba(0,0,0,.45);
}
.node.is-current .node-face:active{
  box-shadow:0 0 0 0 var(--node-base),
             0 0 0 3px var(--edge), 0 0 0 4.6px rgba(0,0,0,.45);
}
/* an untouched node is carried by its RING — two unfilled casings — and by
   the tile and art sitting a little back. The disc cannot carry it: the
   disc is the topic's identity now and is the same on every state. */
.node.is-untouched .node-ico{ opacity:.9; }

/* §3 THE PINNED SECTION HEADER IS GONE. It named the node nearest the
   middle of the window and swapped as you climbed — Duolingo's move, and
   the right one on a list of lessons that have no labels of their own.
   Every node here already carries its topic's name under it, so the header
   was saying the same word twice, and it needed an opaque band across the
   top of the map to stay legible. Removing it also removes the band. */

/* ---- jump back to the current node ---------------------------------
   Only ever visible when the current node is off screen; it is a way back,
   not a nag. It is not a Claim button and it awards nothing. */
.map-jump{
  position:absolute; z-index:21; bottom:14px; left:50%; transform:translateX(-50%);
  display:flex; align-items:center; gap:7px;
  padding:7px 15px 8px; border:0; cursor:pointer;
  border-radius:999px; background:var(--paper); color:var(--ink);
  font-family:var(--font-display); font-weight:900; font-size:14px;
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
  opacity:0; visibility:hidden;
  transition:opacity 160ms linear, visibility 160ms linear;
}
.map-jump.is-on{ opacity:1; visibility:visible; }
.map-jump i{ font-style:normal; line-height:1; }

/* ---- map -> round -------------------------------------------------
   Under the brief's 350ms cap and deliberately cheap: the map drops back
   and fades, the round comes up. It is a cut with a hinge on it, not a
   transition to spend time on. */
.sc-map.is-leaving{ animation:map-out var(--t-screen) var(--e-in) both; }
.sc-round.is-entering{ animation:round-in var(--t-screen) var(--e-settle) both; }
.sc-map.is-arriving{ animation:map-in var(--t-map-in) var(--e-settle) both; }
@keyframes map-out  { to{ opacity:0; transform:scale(.965) } }
@keyframes round-in { from{ opacity:0; transform:scale(1.03) } }
@keyframes map-in   { from{ opacity:0; transform:scale(1.02) } }

/* =====================================================================
   4 · THE COIN AWARD, AS A FLIGHT
   Tokens leave the point the award was earned at — the stamp, the verdict
   — and arrive at the chip that counts them. They are spawned into a fixed
   layer over everything so no beat has to make room for them, and they are
   pointer-transparent so they cannot eat a tap mid-flight.
   ===================================================================== */
/* direction:ltr IS LOAD-BEARING, and this cost a bug. A token is
   positioned entirely by transform, so it was left with left:auto and
   top:auto — and in an RTL containing block the static position of an
   absolutely positioned box is its inline START, which is the RIGHT edge.
   Every token therefore began 390px to the right of where the maths put
   it and flew off the stage, where overflow:hidden ate it silently.
   left:0/top:0 pins the origin; direction:ltr keeps it pinned if the
   offsets are ever dropped again. */
.coinfly{ position:absolute; inset:0; z-index:60; pointer-events:none; direction:ltr; }
.coin-t{
  position:absolute; left:0; top:0; width:19px; height:19px; border-radius:50%;
  background:var(--primary);
  box-shadow:inset 0 0 0 2px rgba(0,0,0,.55), 0 2px 0 rgba(0,0,0,.3);
  will-change:transform, opacity;
}
/* the chip's own acknowledgement, once per arrival */
.hud-coins.is-landing{ animation:coin-land 180ms var(--e-out); }
@keyframes coin-land{ 0%,100%{transform:none} 40%{transform:scale(1.09)} }

/* =====================================================================
   A6 · THE CLAIM RESOLVES ON THE SPOT
   A full-screen surface over everything: the verdict stamp alone first,
   then the explanation under it and one way out. It sits above the HUD
   because the claim is the only thing on screen at this moment — the coin
   count and the issue title are not competing with the answer.
   ===================================================================== */
/* =====================================================================
   §1.2 · V18-1 · THE CLAIM REVEAL, ON THE CARD
   The full-screen scrim is GONE. .creveal used to be an inset:0 overlay
   at 90% opacity with a 14px backdrop blur, which is what took the HUD,
   the coins and the issue title off screen and left the stamp with
   nothing to land on. Everything here now lives inside .cardwrap.
   ===================================================================== */
/* THE STAMP OVERLAPS THE CARD'S EDGE. It is centred on the card's upper
   third and pulled 22px past the leading edge, which is the same
   relationship the cascade's stamp has — the overlap is what reads as
   "applied to this card" rather than "drawn on the screen in front of
   it". .cardwrap does not clip, so the overhang survives. */
/* §4a · THE STAMP CLEARS THE CLAIM TEXT, ALWAYS.
   It was 190px at top:118, which put it across y 294-484 while the claim
   ran 361-461 — half the sentence under the disc, on the option that was
   chosen precisely because the card stays readable.
   THE CLAIM'S TOP IS A KNOWN NUMBER, not a guess: in the revealing state
   the card is 44px of padding, then the art capped at --b1art-cap 126px,
   then a 10px margin, so the claim always starts 180px into the card and
   the art always ends at 170. A 136px stamp at top:20 ends at 156 — 24px
   of clearance from the claim and fully inside the art band. It stays
   over the artwork, which is what a stamp on a document does.
   It is smaller than the cascade's 190px on purpose: at 190 nothing fits
   in that band at any offset. */
/* §0 · THE STAMP STRIKES THE CARD. d2-land was already running, so the
   fault was not a missing animation — it was the EASE. --e-land is
   cubic-bezier(.2,.9,.25,1), which front-loads: measured frame by frame,
   the disc reached final scale at 134ms of a 190ms fall and the overshoot
   peaked near 1.036 instead of 1.06. It arrived rather than landed.
   Same keyframe, re-cut with a per-phase timing function over a linear
   base, so each stated duration is the duration it takes:
     0 -> 190ms    scale 1.8 -> 1.0, ACCELERATING, like a thing falling
     190 -> 252ms  overshoot to 1.06, the rebound off the card
     252 -> 340ms  settle to 1.0
   Opacity is up by 20ms so the disc is never a ghost mid-fall. The card's
   3px jolt is hachach.css's own d2-jolt at --t-stamp-drop, so it fires on
   the frame the disc hits. */
/* ITEM 4 · +15%, AND THE CLASS WAS ALREADY SPLIT. .d2--claim carries its
   own box and position; the MK-card stamp stays on bare .d2 at its 190px
   --d2-size and top:430px, so nothing here reaches it. 136 -> 156px and
   the word 34 -> 39px, both x1.15, so the disc and its lettering grow
   together rather than the word floating in a bigger circle.
   IT STILL CLEARS THE CLAIM. The disc is pinned by top/left, so the extra
   20px grows down and right from a corner that overhangs the card — see
   the report's measured gap to .b1claim. */
/* ITEM 47A · THE CLAIM STAMP RUNS d2-land-mk, THE MK STAMP'S OWN
   KEYFRAMES — not a copy of them. d2-claim-land is retired: it was the
   older 340ms / 1.8 / 1.06 landing, and keeping a second block that said
   almost the same thing is how the two drift apart. One landing, two
   boxes.
   THE BOX IS UNCHANGED AT 156px. Item 4 set that deliberately and this
   item shares the MOTION only; width, height, top and left are as they
   were. The disc still starts its fall larger — 2.2 x 156 = 343px against
   the old 1.8 x 156 = 281 — but it comes to rest in exactly the same
   place at exactly the same size. */
.d2--claim{
  top:20px; left:-26px; z-index:12;
  width:156px; height:156px;
  animation:d2-land-mk var(--t-stamp-land) linear both;
}
/* ITEM 47A · THE CARD TAKES THE HIT AT 5px, like the MK card. hachach.css
   gives every .mf-b.is-stamped a 3px d2-jolt at --t-stamp-drop; the claim
   card is an .mf-b too, so it was getting the old 3px at 190ms. This is
   the same 5px at the same 200ms contact the cascade uses.
   NO rotateY HERE. The cascade's version carries the card's 180deg face
   rotation through the jolt because that card is a 3D flipper; the claim
   card is not, and it has no transform at rest — the reveal's FLIP moves
   elements INSIDE it, never the card itself — so a plain translate is
   correct and cannot clobber anything. */
.mf-b.b1card.is-stamped{
  animation:d2-jolt-claim 120ms var(--e-land) var(--t-stamp-drop-mk) both;
}
@keyframes d2-jolt-claim{
  0%,100%{ transform:translateY(0) }
  40%    { transform:translateY(5px) }
}
.d2--claim .d2__word{ font-size:39px; }
/* =====================================================================
   ITEM 7 · THE MK-CARD STAMP LANDS HARDER
   Scale 1.8 -> 2.2 on the drop, overshoot 1.06 -> 1.10, and a rotation
   snap from -6deg that resolves on the same frame the scale does. 360ms
   end to end, which is the stated ceiling.
   THE FINAL ANGLE IS 0deg. .d2 carries no rotation in any rule — I
   checked all 15 of them — so "snap to the stamp's final angle" is a
   snap to square. The rotation is written explicitly at every frame from
   the landing on, because a transform list that drops rotate() midway
   would interpolate from "no rotation" and re-introduce the wobble the
   snap just resolved.
   :not(.d2--claim) STILL SEPARATES THE TWO, BUT ONLY BY BOX. Item 47
   moved the claim stamp onto these same keyframes and this same duration
   — one landing, shared — so what this selector now keeps apart is the
   190px MK disc from the claim's 156px, not two different motions. Both
   read --t-stamp-land; change it here and both move together, which is
   the point.
   THE LOCKED RULE IS UNTOUCHED: the disc's landing position is set by
   top/left, which this does not go near. Scale 2.2 makes the disc larger
   WHILE FALLING, about the same centre; where it comes to rest is
   unchanged. See the report's settled frame on edelstein.
   ===================================================================== */
.d2:not(.d2--claim){
  animation-name:d2-land-mk;
  animation-duration:var(--t-stamp-land);   /* ITEM 47 · was the 360ms literal */
  animation-timing-function:linear;
}
@keyframes d2-land-mk{
  /*    0 -> 200ms   scale 2.2 -> 1, rotate -6 -> 0, ACCELERATING */
  0%   { transform:scale(2.2) rotate(-6deg); opacity:0;
         animation-timing-function:cubic-bezier(.5,0,.75,.2) }
  5.5% { opacity:1;
         animation-timing-function:cubic-bezier(.5,0,.75,.2) }
  /*  200 -> 268ms   the rebound off the card */
  55.6%{ transform:scale(1) rotate(0deg);
         animation-timing-function:cubic-bezier(.25,.9,.35,1) }
  /*  268 -> 360ms   and the settle */
  74.4%{ transform:scale(1.1) rotate(0deg);
         animation-timing-function:cubic-bezier(.35,0,.45,1) }
  100% { transform:scale(1) rotate(0deg) }
}
/* ACHROMATIC. .d2 normally mixes --d2-ink at --verdict-wash over the
   card; the claim stamp mixes INK instead, so the disc is a neutral
   wash and the word is the only thing it says. Correctness is .cmark's
   job and .cmark's alone — see the note in claimReveal(). */
/* IT HAS TO BE READABLE, which the first attempt was not: a 26% black
   wash with black ink put a dark word on a dark disc over dark artwork
   and שקר disappeared. The disc is PAPER at 88% with ink lettering — the
   same figure-on-ground the rest of the system uses — so the word is the
   highest-contrast thing on the card while carrying no hue at all. */
.d2--neutral{
  --d2-ink:var(--paper);
  --d2-on:var(--ink);
  background:color-mix(in srgb, var(--paper) 88%, transparent);
  color:var(--ink);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.5);
}
.d2--neutral .d2__art circle{ stroke:var(--ink); }
.d2--claim.is-leaving{
  transition:transform var(--t-card-exit) var(--e-in), opacity 100ms linear 220ms;
}

/* =====================================================================
   ITEM 32 · THE PRE-REVEAL SCREEN
   A held screen between the cascade and the finale. It borrows beat 5's
   button geometry outright — .f5acts's column and .f5go's size — so the
   press that reveals the result sits exactly where every other advancing
   press on this round has sat. Nothing here animates beyond the shared
   .b5stage fade, which the global prefers-reduced-motion rule already
   flattens along with everything else.
   ===================================================================== */
.prereveal{ display:flex; flex-direction:column; }
.pr-inner{
  flex:1; min-height:0; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:var(--sp-3);
  text-align:center;
}
/* .f5acts's geometry, restated rather than shared: that class also carries
   beat 5's re-centring behaviour, which this screen has no use for. */
.pr-acts{
  margin-top:auto; padding-top:12px;
  display:flex; flex-direction:column; align-items:stretch; gap:8px; flex:none;
}
.pr-go{ font-size:18px; min-height:0; padding:12px 0 13px; }
/* THE PLACEHOLDER, AND WHY IT IS NOT ph(). Same hazard treatment the .ph
   marker draws, so it reads as placeholder copy at a glance — but it is
   NOT hidden by body.no-ph. ph()'s marker is, and the default build sets
   that class, which would leave this screen with two invisible strings
   and a button. This is meant to be impossible to miss until Tamar's copy
   replaces it. */
.pr-ph{
  margin:0; display:inline-block; max-width:100%;
  background-color:var(--primary);
  background-image:repeating-linear-gradient(45deg,#FFE03D 0 8px,var(--primary) 8px 16px);
  color:var(--ink); border:2px dashed var(--ink); border-radius:4px;
  padding:6px 12px 7px;
  font-family:var(--font-display); font-weight:900; font-size:17px; line-height:1.35;
  text-wrap:balance;
}

/* ---- the correctness chip, in the chyron slot ----------------------
   A DIFFERENT PLANE FROM THE STAMP, deliberately: it sits above the card
   in the round's chrome, where the player's own vote will be pinned two
   beats later. It carries correctness by COLOUR and by the word, never
   by a tick — the map already uses ✓ for a completed topic and a tick
   here would be the same glyph meaning something else. */
/* §4c · THE BAND COMES OFF FOR THE MARK. .chyron keeps its reserved 44px
   box — that is what stops the card resizing across the answer — but
   under .is-mark it drops the fill, the neon stroke and the padding, so
   there is no empty container around the chip. */
.chyron.is-mark{
  background:none !important; box-shadow:none !important;
  padding-inline:0 !important; justify-content:center;
  backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
}
/* §4d · the avatar, so the mark is visibly the PLAYER's */
.cmark__av{ width:26px; height:26px; flex:none; line-height:0; }
.cmark__av svg{ width:100%; height:100%; display:block; }
/* the correctness chip is now .bnr + this: the fill, the type and the
   entrance. Everything about its edge comes from .bnr. */
/* ITEM 47B · THE PILL IS THE SECOND BEAT, AND IT ARRIVES ON ITS OWN.
   It was a 380ms transition that started 120ms after a 340ms stamp; it is
   now a 200ms animation 140ms after a 360ms one, so the two motions never
   overlap and the pill reads as the consequence of the stamp rather than
   as part of the same arrival.
   SCALE ONLY, WITH A SMALL OVERSHOOT — .9 -> 1.04 -> 1 on the stamp's own
   rebound curve. The old entrance also dropped 6px, which at this size
   read as the pill falling in from somewhere; there is nowhere above it
   for it to fall from.
   rotate(-1.6deg) IS RESTATED AT EVERY FRAME because it is the pill's
   resting angle from .cmark.bnr — drop it from one keyframe and the
   animation interpolates the pill straight. */
.cmark{
  font-family:var(--font-display); font-weight:900; font-size:15px;
  opacity:0;
}
.cmark:not(.is-in){ transform:rotate(-1.6deg) scale(.9); }
.cmark.is-in{
  opacity:1;
  animation:cmark-in 200ms cubic-bezier(.25,.9,.35,1) both;
}
@keyframes cmark-in{
  0%   { transform:rotate(-1.6deg) scale(.90); opacity:0 }
  55%  { transform:rotate(-1.6deg) scale(1.04); opacity:1 }
  100% { transform:rotate(-1.6deg) scale(1); opacity:1 }
}
/* ITEM 47 · BOTH IN THEIR FINAL STATE, NO MOTION AND NO STAGGER. The
   waits are skipped in claimReveal() too, so the two arrive together
   rather than 500ms apart with the motion stripped out. `animation:none`
   kills the NAME, which the global reduce rule's animation-duration
   cannot override. */
@media (prefers-reduced-motion: reduce){
  .d2--claim, .mf-b.b1card.is-stamped, .cmark.is-in{ animation:none; }
  .cmark:not(.is-in){ transform:rotate(-1.6deg); }
}
.cmark--ok { --cm:var(--verdict-correct); }
.cmark--sur{ --cm:var(--verdict-surprise); }
/* the neutral dot is gone — the avatar is the thing on the leading edge
   now, and two tokens in one pill was one too many */

/* ---- the explanation panel ----------------------------------------
   It rises over the card's LOWER PORTION and is capped against the card,
   not against its own content. IT SCROLLS, and that is load-bearing:
   e3's tf_explain is 280 characters — the longest of the eleven active
   issues — and does not fit at a legible size on a 360px phone. The CTA
   is a sibling of the scroller, not inside it, so it can never be
   scrolled out of reach. */
/* §0 · THE CARD DOES NOT RESIZE. REVERTED, and the causal chain is worth
   writing down: the last pass dropped .b1card's 620px min-height so the
   card shrank to its content and the panel could sit below it. That did
   two things at once — the card visibly changed height on answering, and
   shrinking it EXPOSED THE DECK'S CARD BACK behind it, so the Knesset
   building appeared under the claim. One cause, two symptoms; fixing only
   the visible half would have brought the other half back.
   The height is restored, the deck is untouched, and the panel is a SHEET
   over the card's foot again rather than a block under it. */
.creveal__exp{
  position:absolute; left:0; right:0; bottom:0; z-index:14;
  max-height:62%;
  display:flex; flex-direction:column; gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-3) calc(var(--sp-3) + 2px);
  background:#211F1B;
  box-shadow:0 -3px 0 rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.06);
  opacity:0; transform:translateY(18px);
  transition:opacity var(--t-flip) var(--e-out), transform var(--t-flip) var(--e-settle);
}
.creveal__exp.is-in{ opacity:1; transform:none; }
.creveal__scroll{
  min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch;
  overscroll-behavior:contain;
  /* THE SCROLLBAR IS DRAWN OUT, NOT LEFT TO THE PLATFORM. On the two
     engines this ships to it is an overlay bar — no width, invisible at
     rest — so hiding it costs nothing and removes the one place a stray
     light grey track could sit on a #211F1B sheet. The affordance is the
     fade below, which is legible on both and does not depend on a
     scroll gesture having already started. */
  scrollbar-width:none;
}
.creveal__scroll::-webkit-scrollbar{ width:0; height:0; display:none; }
/* THE FADE IS A MASK ON THE SCROLLER, NOT A GRADIENT OVER IT. A pseudo
   element pinned to the panel would have to know the CTA's height to sit
   on the right edge, and a child of the scroller would scroll away with
   the text; a mask is anchored to the padding box and stays put while the
   content moves under it. 34px is a little over one line at 15.5px/1.5,
   so the cut line is half-there rather than half-gone — visibly more
   text, not a decorative shadow.
   .has-more is set only when the block actually overflows and .is-atend
   clears it at the bottom of the scroll, so a fully-read block and a
   short one both end on a hard edge. */
.creveal__exp.has-more:not(.is-atend) .creveal__scroll{
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 34px),transparent 100%);
          mask-image:linear-gradient(to bottom,#000 calc(100% - 34px),transparent 100%);
}
/* 24 · THE LARGEST BLOCK OF BODY COPY IN THE APP, ONTO THE BODY FACE.
   It inherited --font-ui from body and named no family, so it was still
   system-ui 700 after item 21 switched the other seven. Same token and the
   same line-height treatment: 1.5 -> 1.6, because SimplerPro's em box is
   23/17 of the font size against system-ui's 20/17 — at 15.5px that is a
   20.97px box where the old leading assumed 18.24px.
   THE WEIGHT HAD TO MOVE TOO, and not as a style choice: the family ships
   400 and 900 only, and CSS resolves an unavailable 700 UPWARD — leaving
   700 here would have printed the Black at 15.5px across the whole
   explanation. See the item 23 audit. */
/* ITEM 44 · 15.5 -> 17px. THE WEIGHT STAYS EXPLICIT AT 400 and that is
   not decoration: SimplerPro ships 400 and 900 only, CSS resolves an
   unavailable middle weight UPWARD, and this is the largest block of body
   copy in the app — a stray 500/600/700 here would print the Black across
   the whole explanation. Measured at 16, 17 and 18: computed weight is
   400 at every one.
   LINE-HEIGHT IS UNCHANGED AT 1.6, and it does not need to move: the ink
   box against a 1.6 leading has headroom at every size tested — +3.8px at
   15.5, +4.6 at 16, +4.2 at 17, +4.8 at 18 — so none of them puts a line
   box narrower than the face's own ink. That was item 41's failure mode
   and it does not arise here.
   THE OVERFLOW RISES AND THAT IS THE DESIGN. Nothing else shrinks to pay
   for it; the panel is a scroller with a fade that says so, and it has
   been verified working on every issue that overflows. See the report's
   table for the per-issue figures at all three widths. */
.creveal__text{
  margin:0; font-family:var(--font-body);
  font-size:17px; font-weight:400; line-height:1.6;
  color:#EFECE4; text-align:start; text-wrap:pretty;
}
/* §0/§3 · הלאה goes back INSIDE the sheet with the structure, but not back
   to the 97px pill in the corner it was before the last pass. It keeps the
   full width it earned there and gains height: the widest thing in the
   panel and the only thing in it that is not type. §3 asked for
   substantially wider — 100% of the card against the 28% it had. */
.creveal__go{
  position:relative; z-index:15;
  flex:none; align-self:stretch; width:100%;
  display:flex; align-items:center; justify-content:center; gap:9px;
  padding:14px 0 15px; font-size:19px;
}
.creveal__go i{ font-style:normal; }

/* =====================================================================
   A9 · THE FINAL REVEAL, rebuilt on Tamar's fields
   The explanation paragraph and the bill summary are GONE from here: the
   explanation resolves the claim at beat 1 now, and the bill's description
   lives in the law modal. What is left is the result, the terms and the
   ways out — no prose we wrote ourselves.
   ===================================================================== */
/* the result is PROSE and it wraps. Some rows are three lines about a
   committee; nothing here truncates or ellipsises. */
.b5result{
  margin:6px 0 0; font-size:var(--fs-body); font-weight:700; line-height:1.45;
  color:var(--ink); text-align:center; text-wrap:pretty;
}
.b5panel.b5-nocount .b5result{ margin-top:0; }

/* glossary terms as chips, not as body text */
.b5gloss{ display:flex; flex-wrap:wrap; gap:6px; justify-content:center; margin-top:2px; }
.b5chip{
  appearance:none; cursor:pointer; border:0;
  padding:4px 11px 5px; border-radius:var(--r-chip);
  background:rgba(255,255,255,.10); color:#EFECE4;
  font-family:var(--font-display); font-weight:900; font-size:var(--fs-meta);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
}
.b5def{
  margin:6px 0 0; font-size:var(--fs-meta); font-weight:700; line-height:1.5;
  color:#D8D2C4; text-align:start;
  background:rgba(0,0,0,.22); border-radius:var(--r-chip); padding:8px 11px;
}
.b5def b{ color:#EFECE4; }

/* the links row. A missing URL is a DEAD chip that looks dead — it is
   marked data-missing-url and listed in the report, never a live-looking
   anchor that goes nowhere. */
.b5links{ display:flex; flex-direction:column; gap:5px; margin-top:4px; align-items:stretch; }
.b5link{
  display:flex; align-items:center; gap:7px;
  font-size:var(--fs-meta); font-weight:700; line-height:1.3;
  color:#D8D2C4; text-decoration:none;
}
.b5link i{ font-style:normal; flex:none; opacity:.85; }
.b5link:not(.is-missing){ text-decoration:underline; text-underline-offset:3px; }
.b5link.is-missing{ color:var(--ink-faint); opacity:.55; cursor:default; }

/* the secondary way out, under the primary */
.b5back{ margin-top:8px; }

/* ITEM 34c · .b2bill--link IS RETIRED, both rules. The law name is no
   longer a line of its own under the question — the tap target is the
   title inside .b2head now, and .b2title-link carries the underline and
   the control semantics it used to. Removed rather than left as a
   tombstone: nothing emits either class any more. */
/* =====================================================================
   B3-3 · THE DIE-CUT STICKER MODAL
   One component, two contents: the law (title + date + summary + the
   police-hat placeholder) and a glossary term (term + definition). §3.1
   moved the glossary onto it; the old inline .gdef panel is gone.
   ===================================================================== */
.stmodal{
  position:absolute; inset:0; z-index:60; display:grid; place-items:center;
  /* §N THE SCRIM NEVER SHORTENS. The keyboard's height (0 unless a field
     in the modal has focus — see kbSync()) goes into the BOTTOM PADDING,
     so the grid area the box centres in ends above the keyboard while
     the scrim still paints the whole stage. It was `bottom` before, and
     on the phone the strip between the box and the keyboard showed bare
     map — the scrim had been cut to the estimate, and any estimate is
     wrong by the accessory bar. Padding is wrong by nothing. */
  /* THE BOX CENTRES IN THE SAFE AREA, not in the whole stage. The stage
     runs under the status bar (viewport-fit=cover) and pads its own HUD
     down by the inset; the modal covers the stage's padding box, so
     without this its box centred against the notch line and its ✕ could
     sit where a browser's toolbar overlays the top — Chrome on iOS was
     seen hiding it. Safari reported no problem, but the inset is the
     same rule in both. */
  padding:calc(var(--sp-4) + env(safe-area-inset-top, 0px)) var(--sp-4)
          calc(var(--sp-4) + env(safe-area-inset-bottom, 0px) + var(--kb-h, 0px));
  background:rgba(20,18,15,.76);
  opacity:0; transition:opacity var(--t-ov-in) linear;
}
.stmodal.is-in{ opacity:1; }
.stmodal.is-out{ opacity:0; }
/* THE STICKER. Same construction as every card and every node icon in the
   system: a white die-cut edge, the keyline outside it, and a flat
   extrusion straight down with zero blur. The 1.4-degree tilt is what
   makes it a sticker laid on the screen rather than a dialog box. */
.stmodal__box{
  /* THE BOX NO LONGER SCROLLS — ITS BODY DOES. The ✕ went from sticky to
     absolute when it became a drawn control, and an absolute child of a
     scrolling box scrolls away with the content; on b3's 309-character
     summary the close would have left the screen. Moving the scroll to
     .stmodal__body pins the ✕, the graphic and the title, which is the
     better behaviour anyway. */
  position:relative; width:100%; max-width:312px; max-height:76%;
  background:var(--paper); color:var(--ink);
  border:5px solid var(--edge); border-radius:18px;
  box-shadow:0 0 0 2px var(--keyline), 0 8px 0 rgba(0,0,0,.4);
  padding:var(--sp-4);
  display:flex; flex-direction:column; align-items:center; gap:var(--sp-2);
  transform:rotate(-1.4deg) scale(.9);
  transition:transform var(--t-flip) var(--e-land);
}
.stmodal.is-in .stmodal__box{ transform:rotate(-1.4deg) scale(1); }
/* the ✕ sits over the scrolling content, not inside it, so a long
   bill_summary cannot scroll the way out off the top of the box */
/* flex-start in an RTL column is the PHYSICAL RIGHT, which is where the
   app's own ✕ lives — see .exitsheet__x. Sticky so a long bill_summary
   cannot scroll the way out off the top of the box. */
/* §1 + §1b · ONE CLOSE BUTTON, SHARED BY BOTH MODALS, AND IT IS DRAWN.
   It was an 18px ✕ glyph at 60% opacity inset 8px from the corner — a
   typographic character, not a control, and the thinnest thing on a sheet
   whose every other edge is 2-5px. A glyph also cannot be given a stroke
   weight: ✕ has no bold in this face, so the only way to specify
   thickness is to draw it. Two 3px bars in a 46px target.
   MATCHED TO THE HUD's ✕, which is already right: .ib-b is a 40px cream
   button and .hud-x sets its glyph at 17px. This is 46px with a 19px mark
   and a 3px stroke — slightly larger because it sits on paper rather than
   in a chip, and closer to the corner because a modal's close belongs in
   the corner, not floated in from it.
   ONE COMPONENT: the law sheet, the glossary sheet and the exit confirm
   all use these rules. Two of them previously had their own. */
.stmodal__x,
.exitsheet__x{
  position:absolute; top:2px; right:2px; left:auto; z-index:4;
  width:46px; height:46px; margin:0;
  border:0; background:none; cursor:pointer; padding:0;
  font-size:0; color:var(--ink); opacity:.72;
}
.stmodal__x::before, .stmodal__x::after,
.exitsheet__x::before, .exitsheet__x::after{
  content:""; position:absolute; left:50%; top:50%;
  width:19px; height:3px; border-radius:2px; background:currentColor;
  margin:-1.5px 0 0 -9.5px;
}
.stmodal__x::before, .exitsheet__x::before{ transform:rotate(45deg); }
.stmodal__x::after,  .exitsheet__x::after { transform:rotate(-45deg); }
.stmodal__x:active, .exitsheet__x:active{ opacity:1; }
/* the sheet is position:relative already; the title needs to clear the
   button now that it sits in the corner rather than in the flow */
.stmodal__box{ padding-top:calc(var(--sp-4) + 8px); }
/* ITEM 9 · .stmodal__art IS RETIRED. Nothing emits the class any more —
   the graphic it used to draw now lives inside .sthero, which reserves
   its space whether or not there is art. The rule is removed rather than
   left as a tombstone. */

/* =====================================================================
   ITEM 9 · THE RESERVED HERO SLOT
   96px, HELD WHETHER OR NOT IT IS FILLED. That is the point of the item:
   the glossary term's sticker and the bill's sticker are the same object
   at the same height, so opening one after the other is a change of
   content and not a change of shape. Before this, a modal with a graphic
   and one without were 64px apart in height and read as two components.
   16px BELOW THE TOP EDGE, which is why the box's own padding-top is
   overridden here — the shared 24px would have put the hero at 24. The
   ✕ is absolutely positioned and unaffected; the hero is 96px centred in
   a 312px box, so it clears the control by ~44px on each side.
   `:has()` rather than a modifier class on the box, matching the rule
   directly above the old .stmodal__art that already keyed off it. */
.stmodal__box:has(.sthero){ padding-top:16px; }
/* ITEM 45 · A 96px BAND, NOT A 96px SQUARE — and this is a correction to
   item 9. The brief reserved "96px tall"; I built it square, and with the
   real topic icons in it that was wrong twice over. The icons run from
   0.535 to 1.52 in aspect ratio, so against a square: the wide ones were
   capped by WIDTH and drew 63px tall in a 96px box, and the tall ones
   blew straight through it — economy drew 179px in a 96px slot, because
   an auto grid row sizes to its content and the % max-height then
   resolved against the overflowing track rather than against the 96.
   Height is the dimension that has to be constant for the slot to read as
   one slot, so height is what is fixed. Every icon now draws exactly 96
   tall at its own ratio, and the widths vary — which is what a row of
   different objects at the same scale actually looks like. */
.sthero{
  flex:none; width:100%; height:96px;
  display:grid; place-items:center;
}
/* the ART IS BOUNDED BY THE BAND, never the other way round: max-height
   pins it to 96 and max-width is the box's own content width, so nothing
   can overflow the slot in either direction whatever ratio arrives. */
.sthero__art{
  max-height:96px; max-width:100%; width:auto; height:auto;
  display:block; filter:url(#dcw-sm);
}
/* THE FALLBACK, IN THE STICKER CONSTRUCTION EVERYTHING ELSE USES: charcoal
   fill, 3px white die-cut, the keyline outside it, a hard offset shadow.
   It is a span with rings, not SVG text — so the glyph is real type in
   the display face and picks up SimplerPro rather than being a drawn
   path that would have to be redrawn to change.
   It is decorative and aria-hidden: the modal is announced by its title,
   and a "?" read aloud before the title would say nothing true. */
.sthero__q{
  display:grid; place-items:center;
  width:82px; height:82px; border-radius:50%;
  background:var(--ink); color:var(--paper);
  box-shadow:0 0 0 3px var(--edge), 0 0 0 4.6px var(--keyline),
             0 4px 0 rgba(0,0,0,.34);
  font-family:var(--font-display); font-weight:900;
  font-size:46px; line-height:1;
  /* the glyph sits high in the em box; nudge it onto the disc's centre */
  padding-bottom:4px;
  user-select:none;
}
.stmodal__title{
  margin:0; padding-inline:30px;   /* symmetric, so a centred title clears the ✕ */
  font-family:var(--font-display); font-weight:900;
  font-size:var(--fs-body); line-height:1.22; text-align:center;
}
.stmodal__meta{ margin:0; font-size:var(--fs-meta); font-weight:700; color:var(--ink-soft); }
/* T7 · the date's label. SimplerPro Regular and the date's own grey, one
   step down in size — it names the field rather than competing with it.
   400 IS WRITTEN OUT because the family ships 400 and 900: inheriting the
   700 above would resolve upward and print the Black on a caption. */
.stmodal__metalab{
  display:block; font-family:var(--font-body); font-weight:400;
  font-size:var(--fs-micro); line-height:1.3; color:var(--ink-soft);
}
.stmodal__body{
  margin:2px 0 0; font-size:var(--fs-meta); font-weight:700;
  line-height:1.55; text-align:start; align-self:stretch;
  min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch;
  overscroll-behavior:contain;
}
/* a glossary term has no graphic and no date, so its title carries the
   term alone and can afford to be bigger. ITEM 9 re-points this from the
   retired .stmodal__art to the hero's fallback: "has a hero but no real
   art in it" is now what "has no graphic" means. */
.stmodal__box:has(.sthero__q) .stmodal__title{ font-size:20px; }

/* =====================================================================
   ITEM 43 · THE FIRST-ARRIVAL STICKER, AND ITS THREE UNWRITTEN STRINGS
   All three are Tamar's and none of them is written. They carry the
   hazard treatment ph() uses so they read as placeholders at a glance and
   cannot be mistaken for copy — but they are NOT hidden by body.no-ph,
   which is the second time this file makes that exception (the first is
   .pr-ph, the third beat 5's rejected verb). The reason is the same every
   time: hiding a marker whose host is the only thing in the slot does not
   leave a cosmetic gap, it leaves an empty screen. A first-run sticker
   with a blank title, a blank body and a blank button is worse than one
   that says out loud what it is waiting for.
   THE BODY IS THE SLOT, NOT THE STRING. The item asks for two lines of
   body maximum; that is a constraint on Tamar's sentence, and the brief
   standing in for it is longer than the sentence will be. It is not
   clamped here — clamping would hide the very text that says what is
   missing. See the report for the measured line count at each width. */
.stmodal[data-mapintro] .stmodal__title,
.stmodal[data-mapintro] .stmodal__body{
  background-color:var(--primary);
  background-image:repeating-linear-gradient(45deg,#FFE03D 0 8px,var(--primary) 8px 16px);
  color:var(--ink); border:2px dashed var(--ink); border-radius:6px;
  padding:6px 10px 7px;
}
/* the button keeps P-C's own fill — it is already the one yellow in the
   system — so the dash is what marks it, not a second yellow on top. */
.stmodal[data-mapintro] .mi-go{
  border:2px dashed var(--ink);
  margin-top:4px;
}
/* the body is a scroller in the base component, for the law's long
   summary. Here it holds two lines and must not grow its own scrollbar
   around a placeholder. */
.stmodal[data-mapintro] .stmodal__body{ overflow:visible; text-align:center; }

/* =====================================================================
   B2-4 · THE ASK STICKER
   One component, two copy strings: the claim card asks אמת או שקר? and
   the first cascade card asks מה הוא/היא הצביע/ה?. Nothing else about it
   differs between the two.
   ===================================================================== */
/* IT COSTS THE CARD NOTHING. It is a child of .cardwrap, which does not
   clip, and it hangs off the card's TOP-RIGHT corner — the corner
   diagonally opposite the verdict stamp at top:430 left:-22. The two
   cannot meet on any card at any scale, which is what makes this the one
   B2 option with no stamp risk.

   IT IS CHROME-SCALE, NOT CARD-SCALE, and this is the only unusual thing
   about it. sizeStage() scales .stack so a 620px card fits a short phone;
   a sticker inside it would inherit that and settle SMALLEST on the
   smallest screen, which is exactly backwards for an instruction. The
   counter-scale is 1/--card-scale about the same corner it is pinned to,
   so the settled sticker is the same number of CSS pixels at 360, 393 and
   430 — measured, not asserted; the figures are in the report.

   THE TWO TRANSFORMS ARE ON TWO ELEMENTS ON PURPOSE. .ask-st carries the
   counter-scale, which is static and depends on a custom property;
   .ask-st__i carries the slap, which is a keyframe animation. Composing
   them on one element would mean animating a transform whose base value
   changes on resize. */
/* IT STRADDLES THE CARD'S TOP EDGE rather than sitting inside the corner,
   and that is a collision fix, not a preference. .mf-b__id puts the MK's
   name at top:18px inset-inline:14px with text-align:right — which in RTL
   is the card's top-RIGHT corner, exactly where a corner sticker wants to
   go. Drawn there it covered יאיר לפיד outright, and a name long enough
   would reach a top-LEFT sticker just as surely.
   bottom:calc(100% - 10px) pins the sticker's FOOT 10px below the card's
   top edge — 5px of that is the card's own die-cut border — so it reads as
   stuck onto the card while its body sits above the card entirely. The
   name block starts 8px below where the sticker ends, at any name length,
   because the sticker is no longer in the name's band at all. */
.ask-st{
  position:absolute; z-index:9; top:auto; bottom:calc(100% - 10px); right:-8px;
  transform-origin:100% 100%;
  transform:scale(calc(1 / var(--card-scale, 1)));
  pointer-events:none;
  transition:opacity 160ms linear, transform 160ms var(--e-in);
}
.ask-st.is-retired{ opacity:0; }
/* §5.4 · INVERTED. It was cream with ink type — the same fill, the same
   ink and nearly the same radius as the .v-a vote buttons directly below
   it, so an INSTRUCTION looked like a CONTROL and invited a tap that does
   nothing.
   THE FILL IS NEUTRAL AND MUST STAY NEUTRAL. #2E2A26 is the same ink the
   map nodes use — a dark grey with no hue in it. Nothing here may take a
   verdict colour: lime and magenta code correctness, and a coloured
   sticker on the card the player is about to answer would read as a hint
   about the answer.
   IT STILL READS AS APPLIED, not as chrome: the white die-cut border, the
   keyline outside it and the 4px extrusion are unchanged, and it still
   straddles the card's top edge at a slap angle. Only the two colours
   swapped. Type goes up 0.5px to hold at 360px against the darker fill. */
.ask-st__i{
  display:block; opacity:0; transform-origin:100% 100%;
  background:#2E2A26; color:#F4F1E8;
  font-family:var(--font-display); font-weight:900;
  font-size:16.5px; line-height:1.06; white-space:nowrap;
  padding:8px 13px 9px; border-radius:12px;
  border:3px solid var(--edge);
  box-shadow:0 0 0 2px var(--keyline), 0 4px 0 rgba(0,0,0,.34);
}
/* THE SLAP. It arrives from above and outside the card, over-shoots small
   on contact and settles — the same land-and-hit shape the verdict stamp
   uses, at a fraction of its weight, so it reads as part of the same
   physical language without ever being mistaken for a verdict. It has no
   hue: the verdict stamp is the only coloured mark in the round. */
.ask-st__i.is-ready,
.ask-st.is-slapped .ask-st__i{
  animation:ask-slap var(--t-ask-in) var(--e-land) both;
}
@keyframes ask-slap{
  0%   { opacity:0; transform:translate(10px,-30px) rotate(-21deg) scale(1.7) }
  40%  { opacity:1 }
  62%  { transform:translate(0,0) rotate(-3.2deg) scale(.94) }
  82%  { transform:translate(0,0) rotate(-6.6deg) scale(1.03) }
  100% { opacity:1; transform:translate(0,0) rotate(-5.5deg) scale(1) }
}

/* =====================================================================
   B1-2 · THE FIRST-RUN INSTRUCTION OVERLAY
   Full screen, the player's first ever issue, never again. The card is
   already dealt underneath it — the overlay explains the thing that is
   there, rather than standing in front of an empty screen.
   ===================================================================== */
/* ITEM 27 · OPTICALLY CENTRED, NOT GEOMETRICALLY. The block measured
   312.6/312.6 at 390x844 — true centre — and still read low, because the
   yellow button is the heaviest mass in the stack and sits last. The lift
   is 16px and it is paid for with 32px of extra BOTTOM padding rather
   than a transform: place-items:center then centres inside a content box
   whose floor has risen, so the box moves up by half of it and the
   entrance transform on .b1intro__box is left alone to do its own job. */
.b1intro{
  position:absolute; inset:0; z-index:80; cursor:pointer;
  display:grid; place-items:center;
  padding:var(--sp-5) var(--sp-5) calc(var(--sp-5) + 32px);
  background:rgba(20,18,15,.88);
  opacity:0; transition:opacity var(--t-ov-in) linear;
}
.b1intro.is-in{ opacity:1; }
.b1intro.is-out{ opacity:0; }
.b1intro__box{
  display:flex; flex-direction:column; align-items:center; gap:var(--sp-4);
  text-align:center; max-width:320px;
  transform:translateY(10px); transition:transform var(--t-flip) var(--e-settle);
}
.b1intro.is-in .b1intro__box{ transform:none; }
/* the heading is the SAME string the claim sticker carries — one question
   asked once big and then kept small on the card for every round after */
/* §1 · the heading is longer than the one it replaced — "טענה — אמת או
   שקר?" against "אמת או שקר?" — and at 34px in a 300px box it broke with
   "שקר?" alone on the second line. balance splits it evenly instead, and
   the box gains 20px so the two halves have room. Type size untouched:
   this is a wrap fix, not a shrink. */
.b1intro__t{
  margin:0; font-family:var(--font-display); font-weight:900;
  font-size:34px; line-height:1.05; color:#EFECE4; text-wrap:balance;
}
.b1intro__b{
  margin:0; font-size:var(--fs-body); font-weight:700; line-height:1.5;
  color:#D8D2C4;
}
/* ITEM 27 · THE BUTTON COMES IN TO THE PARAGRAPH'S OWN MEASURE. The two
   already shared a 320px BOX — .p-c is width:100% and the box is at its
   max-width — but the paragraph's widest inked line is 292.7px while the
   button paints a solid bar plus its 4.6px keyline on each side, so it
   read 36.5px wider than the copy above it. 284px + the 9.2px of ring is
   293.2px of visible width: the same column, to within half a pixel.
   Fixed rather than fluid because it is matching an inked line, and that
   line is 292.7px at all three test widths — the paragraph wraps
   identically at 390, 375 and 360.
   ITEM 27 · AND THE GAP ABOVE IT OPENS. .b1intro__box's gap is 16px
   between every child; margin-top adds 8 to this one pair only, so the
   title-to-paragraph gap stays 16 and the paragraph-to-button gap becomes
   24 — the 1.5x the brief asked for, applied where the eye needs the
   break between reading and acting. */
.b1intro__go{
  min-width:170px;
  max-width:284px;
  margin-top:8px;
}

/* A7 · the avatar inside the pinned own-vote band, at the leading edge */
.chyron-av{ flex:none; width:28px; height:28px; line-height:0; }
.chyron-av svg{ width:100%; height:100%; display:block; }

@media (prefers-reduced-motion: reduce){
  *{ animation-duration:1ms !important; transition-duration:1ms !important; }
}


/* =====================================================================
   A-1 · PARTY CONCEALMENT — THE PEELING COVER
   An object placed OVER the party line, not a missing field: it is the
   size of the chip beneath it, it carries the sticker sheet's own die-cut
   edge, and it says what it is covering. A-2's redaction bar was rejected
   for reading as accusation on a product about the Knesset, and A-3's
   priced chip for making this the first thing in the game a player pays
   for.
   ===================================================================== */
.pty{ position:relative; }

/* THE RESIDUE — the ghost of where the tape was stuck.
   IT IS THE TAPE'S OWN FOOTPRINT, NOT AN APPROXIMATION OF IT: inset and
   radius are copied from .pcov / .pcov__face, so the patch and the sheet
   are the same rectangle. That is the whole trick — the residue is
   already there, exactly covered, BEFORE the peel, so nothing appears,
   grows or shifts when the sheet lifts. The peel's clip-path uncovers it
   at the same rate it uncovers the party name, which is what makes it
   read as "this was covered and now isn't" rather than as a new object
   arriving to announce that something happened.
   FLAT BLACK AT 5.5%, hard-edged, no blur — it darkens the kraft AND the
   grid ruling printed on it by the same amount, which is what an
   adhesive actually does to a printed card. On #D8C9A8 that lands on
   #CCBE9F: about 5% down in luminance, enough to find and too little to
   read as a filled shape. */
.pty::before{
  content:""; position:absolute; inset:-2px -4px; z-index:1;
  border-radius:4px; background:rgba(0,0,0,.055);
  pointer-events:none;
}
/* the name has to sit ON the residue, not under it. .pcov is z-index 3
   and stays above both, so the stacking is card -> residue -> party ->
   tape. */
.pty__val{ display:inline-block; position:relative; z-index:2; }
/* the settle. The line is already visible by the time this runs — the
   peel uncovered it progressively — so this is a small landing, not an
   entrance: a 4% overshoot that resolves, nothing that fades in. */
.pty.is-open .pty__val{
  animation:pty-land var(--t-peel-land) var(--e-land) both;
}
@keyframes pty-land{
  0%  { transform:scale(1.04) translateY(-1px) }
  100%{ transform:none }
}

.pcov{
  position:absolute; inset:-2px -4px; z-index:3;
  border:0; padding:0; background:none; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* THE FLAT SHEET. Paper, not a panel: the same cream the stickers are cut
   from, a hard keyline and a flat 2px extrusion — the construction every
   other sticker in the app uses. clip-path is what the peel animates, so
   the sheet is genuinely removed from over the text rather than faded. */
.pcov__face{
  position:absolute; inset:0; display:grid; place-items:center;
  border-radius:4px;
  background:linear-gradient(160deg,#FBF7EE 0%,#EFE7D4 62%,#E2D9C2 100%);
  box-shadow:0 0 0 1.4px rgba(0,0,0,.45), 0 2px 0 rgba(0,0,0,.26);
  clip-path:inset(0 0 0 0);
}
.pcov__lab{
  font-family:var(--font-display); font-weight:900; font-size:11px;
  line-height:1; color:#6B6350; letter-spacing:.01em;
}
/* THE CURL, and this is what gives it thickness. It rides the peeling
   edge: a narrow strip whose gradient runs light on the outer face to
   dark on the underside, so the sheet reads as having two sides and a
   rolled edge rather than as a rectangle being wiped away. It widens as
   it lifts — a tighter roll opens out — and casts a shadow onto the card
   below it. Sits at the LEADING edge, which under dir=rtl is the right. */
.pcov__curl{
  position:absolute; top:-1px; bottom:-1px; right:0; width:9px;
  border-radius:2px 5px 5px 2px;
  transform-origin:right center;
  background:linear-gradient(to left,
    #B9AE93 0%, #E8E0CD 34%, #FBF7EE 62%, #D8CFB6 100%);
  box-shadow:-3px 1px 6px rgba(0,0,0,.42), inset 0 0 0 1px rgba(0,0,0,.18);
}

/* 1 · THE PEEL. Two animations on one clock: the sheet's clip retreats
   from the right and the curl rides exactly that edge. They are separate
   elements because the curl has to rotate and widen while the sheet only
   clips, and one element cannot do both. */
.pcov.is-peeling .pcov__face{ animation:peel-face var(--t-peel) var(--e-peel) both; }
.pcov.is-peeling .pcov__curl{ animation:peel-curl var(--t-peel) var(--e-peel) both; }
@keyframes peel-face{
  from{ clip-path:inset(0 0 0 0) }
  to  { clip-path:inset(0 100% 0 0) }
}
@keyframes peel-curl{
  0%   { right:0;    transform:rotate(0deg)  scaleX(1);   }
  35%  { transform:rotate(-7deg)  scaleX(1.7); }
  100% { right:100%; transform:rotate(-14deg) scaleX(2.4); }
}
/* the label goes as soon as the sheet starts moving — type sliding under
   a clip reads as a wipe, which is the one thing this must not look like */
.pcov.is-peeling .pcov__lab{ animation:peel-lab 180ms linear both; }

@keyframes peel-lab{ to{ opacity:0 } }

/* =====================================================================
   ITEM 6 · THE AFFORDANCE NUDGE   ·   ITEM 51C · SAID TWICE
   A wiggle 400ms after the card settles, and — for a player who did not
   take the hint — one repeat 2000ms after that one ends. TWO RUNS, THEN
   REST. It is a hint that the sheet is liftable, so it has to be small
   enough to read as paper shifting and not as something wrong: 2 degrees
   and 2px, which at this chip's size is about a millimetre of travel.
   TWO ITERATIONS OF ONE 180ms CYCLE PER RUN, not one 360ms keyframe — so
   the ease-in-out applies to EACH wiggle, and each one accelerates out of
   rest and decelerates back into it. One long curve over both would give
   a fast middle and two slow ends, which reads as a wobble rather than
   as two taps.
   THE REPEAT IS A SECOND ANIMATION, NOT A LOOP. A CSS animation cannot
   hold a gap between iterations, so the pause is a second delay on the
   same keyframes: run 1 is 400 -> 760, run 2 is 2760 -> 3120, and 2000ms
   of rest sits between them. There is no interval, no iteration count
   above 2 on either, and nothing to stop — it cannot become a loop by
   retuning a number.
   THE SECOND RUN'S FILL IS `forwards`, NOT `both`, AND THAT IS LOAD-BEARING.
   Both entries animate `transform` and the later one wins wherever both
   apply — so `backwards` fill on run 2 would hold transform:none through
   its own 2760ms delay and flatten run 1 completely. `forwards` makes it
   contribute nothing until it starts.
   STILL STOPS ON TAP: wirePeel() removes the class, which takes both runs
   with it whether the tap lands during a wiggle or in the rest between.
   STANDING BEHAVIOUR, EVERY ROUND. Nothing here is conditioned on the
   round being inverted; the tape covers the party on every card that
   has one. */
.pcov.is-nudging{
  animation:pcov-nudge 180ms ease-in-out  400ms 2 both,
            pcov-nudge 180ms ease-in-out 2760ms 2 forwards;
}
@keyframes pcov-nudge{
  0%  { transform:none }
  25% { transform:rotate(2deg) translateX(2px) }
  75% { transform:rotate(-2deg) translateX(-2px) }
  100%{ transform:none }
}
/* skipped entirely, not shortened — see nudgeCover(), which does not add
   the class at all. This is the belt to that braces: if the class ever
   arrives by another route, it still does nothing here. */
@media (prefers-reduced-motion: reduce){
  .pcov.is-nudging{ animation:none; }
}

/* 2 · THE DISCARD. It leaves on an arc — left, down and turning — on an
   accelerating curve, so it reads as dropped rather than slid or faded.
   The opacity only goes at the very end, so what the eye follows is the
   object leaving, not the object dissolving. */
.pcov.is-gone{ animation:peel-drop var(--t-peel-out) var(--e-fall) both; pointer-events:none; }
@keyframes peel-drop{
  0%   { transform:translate(0,0) rotate(0deg); opacity:1 }
  55%  { transform:translate(-14px,16px) rotate(-26deg); opacity:1 }
  100% { transform:translate(-30px,58px) rotate(-68deg); opacity:0 }
}

/* NO MOTION, NO COVER. The button is removed outright in wirePeel(), so
   this only has to make sure nothing animates in the frame before that. */
@media (prefers-reduced-motion: reduce){
  .pcov, .pcov__face, .pcov__curl, .pty.is-open .pty__val{ animation:none !important; }
}

/* =====================================================================
   §V21-1 · THE INVERTED ROUND
   One MK, face withheld, vote stated. The card, the sticker, the foot
   and the stamp are the cascade's own objects — only the question is
   inverted, and the styling here is deliberately thin for that reason.
   ===================================================================== */

/* THE BLUR IS ON THE PORTRAIT, NOT ON THE CARD. Blurring .mf-b would
   take the card's border, its keyline and anything else inside it with
   the photograph, and would read as a broken render rather than as a
   withheld face. The drop-shadow is the cascade's own and is kept, so
   the portrait still sits on the card the same way.
   Gaussian only. It removes detail and never displaces it, so no step
   in the sequence can deform a real politician's face — which is the
   reason the mechanic is blur and not mosaic, pixelation or warp. */
.mf-b--inv .mf-b__port{
  filter:blur(var(--inv-blur, 13px)) drop-shadow(0 2px 0 rgba(0,0,0,.18));
  transition:filter 200ms linear;
}
/* 200ms inside a 1400ms hold is 14% of the step: enough not to snap,
   far too little to read as a continuous sharpen. The step has to stay
   a step, because a step is the thing the player decides against. */

.inv-tray{ display:flex; flex-direction:column; gap:var(--sp-2); }
/* .v-a is flex:1, which in a COLUMN stretches each name to fill the
   foot instead of sizing to its type. */
.inv-name{ flex:none; width:100%; min-height:52px; font-size:19px; }
.inv-name.is-picked{ box-shadow:0 0 0 3px var(--edge), 0 0 0 6px var(--ink); }

/* SUBORDINATE. Half the type of the names, no fill, no edge, no
   extrusion — it is a readout and a secondary control sharing one line
   under the answers, and neither may look like a fourth option. */
.inv-bar{
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-2); padding:5px 9px 6px; margin-top:2px;
  /* A GROUND, because what is behind it is a photograph. The bar sits on
     the portrait at the card's foot, and the portrait is different behind
     every MK — on this deal it lands on a red tie. Cream type on an
     uncontrolled image is a contrast bug waiting for the next card, so
     the bar carries its own bed rather than trusting the picture.
     Dark and neutral, the same #2E2A26 family as the question sticker:
     nothing at the foot of an unanswered card may carry a hue. */
  background:rgba(30,27,24,.62); border-radius:11px;
  -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
}
.inv-steps{ display:inline-flex; gap:5px; align-items:center; }
.inv-steps i{
  width:7px; height:7px; border-radius:50%;
  background:rgba(244,241,232,.30);
  box-shadow:0 0 0 1.5px rgba(0,0,0,.42);
  transition:background 200ms linear;
}
/* filled = sharpness reached. It reports the FACE, which the player can
   already see; it never reports the payout, which they cannot. */
.inv-steps i.is-on{ background:#F4F1E8; }
.inv-hint{
  border:0; background:none; padding:2px 4px; cursor:pointer;
  font-family:var(--font-display); font-weight:900; font-size:13px;
  color:#F4F1E8; text-decoration:underline; text-underline-offset:3px;
}
.inv-hint:disabled{ opacity:.42; text-decoration:none; cursor:default; }

/* THE HINT LANDS AS A STICKER, mid-card on the TRAILING side — which
   under RTL is the left, hence inset-inline-end. Same die-cut family as
   the peel reveal it stands in for, and cream rather than the question
   sticker's neutral dark, because this one is an ANSWER to something and
   the question sticker is not. It still carries no hue: lime and magenta
   code correctness and nothing on an unanswered card may borrow them. */
.inv-hint-st{
  /* 56%, not the vertical middle. At 47% it landed across the cheek and
     the eye line — the two features a player actually recognises a face
     by — so a hint that is supposed to HELP was covering the evidence.
     It sits on the jaw and shoulder instead, still unmistakably on the
     card and still on the trailing side. */
  position:absolute; z-index:8; top:56%; inset-inline-end:-9px;
  pointer-events:none;
  /* the counter-scale is on the WRAPPER and the slap is on the inner
     span, exactly as .ask-st splits them — an animation that sets
     transform would otherwise wipe the counter-scale out on frame one */
  transform-origin:0 50%;
  transform:scale(calc(1 / var(--card-scale, 1)));
}
.inv-hint-st__i{
  display:block; opacity:0; transform-origin:0 100%;
  background:var(--paper); color:var(--ink);
  font-family:var(--font-display); font-weight:900;
  font-size:16px; line-height:1.06; white-space:nowrap;
  padding:7px 12px 8px; border-radius:12px;
  border:3px solid var(--edge);
  box-shadow:0 0 0 2px var(--keyline), 0 4px 0 rgba(0,0,0,.34);
}
.inv-hint-st.is-slapped .inv-hint-st__i{
  animation:inv-hint-slap var(--t-ask-in) var(--e-land) both;
}
@keyframes inv-hint-slap{
  0%   { opacity:0; transform:translate(-12px,-22px) rotate(15deg) scale(1.6) }
  40%  { opacity:1 }
  62%  { transform:translate(0,0) rotate(2.6deg) scale(.95) }
  82%  { transform:translate(0,0) rotate(5.4deg) scale(1.03) }
  100% { opacity:1; transform:translate(0,0) rotate(4.2deg) scale(1) }
}

/* the name arriving with the reveal, in the slot the cascade has always
   kept for it — the card gains an identity block rather than unhiding
   one, because it never had one to unhide */
.inv-id{ opacity:0; transform:translateY(-7px);
  transition:opacity var(--t-flip) linear, transform var(--t-flip) var(--e-land); }
.inv-id.is-in{ opacity:1; transform:none; }

/* THE INVERTED QUESTION IS TWICE THE LENGTH OF THE CASCADE'S. .ask-st__i
   is white-space:nowrap and .ask-st counter-scales against --card-scale
   to hold true size while the card shrinks, so at 360px the longer
   string ran past BOTH edges of the card it is supposed to be stuck to —
   a sticker that is wider than the object it was slapped onto stops
   reading as applied and starts reading as a banner.
   It wraps to two lines inside the card's own width instead. Centred,
   because a two-line sticker with a ragged trailing edge reads as a
   paragraph; and half a point smaller, which buys the second line back
   most of what the wrap cost. */
.ask-st--inv{ inset-inline-start:auto; }
.ask-st--inv .ask-st__i{
  white-space:normal; text-align:center; text-wrap:balance;
  max-width:calc(var(--card-w) - 34px);
  font-size:16px; line-height:1.18; padding:8px 14px 9px;
}
.inv-hint-st.is-retired{ opacity:0; transition:opacity 160ms linear; }


/* =====================================================================
   §1 · THE LOADING BEAT
   Between the intro and wherever the game goes next. Everything on the
   intro leaves except the chair; the chair grows and centres; a bar
   fills beneath it; then the destination.

   IT IS HONESTLY FAKE AND THE CODE SAYS SO. There is nothing to load —
   no fetch, no decode, no wait. The bar runs on a fixed clock. When
   there IS something to load the clock is the thing to replace, and
   loadingBeat() takes the destination as an argument so the beat does
   not have to know what it is waiting for.
   ===================================================================== */

/* everything but the chair goes. The plaza gradient is part of the
   building scene, so it leaves with it. */
.sc-intro.is-loading .i-title,
.sc-intro.is-loading .i-sub,
.sc-intro.is-loading .i-para,
.sc-intro.is-loading .i-stage,
.sc-intro.is-loading .i-cta,
.sc-intro.is-loading::before{
  opacity:0; pointer-events:none;
  transition:opacity var(--t-load-fade) linear;
}

/* THE CHAIR KEEPS ITS DIE-CUT, AND THAT IS THE DECISION. filter runs
   BEFORE transform, so a scaled chair carries a scaled white edge — at
   the sizes used here 5px becomes 6.2-7.3px, which is heavier but still
   plainly the same cut. Dropping it at the larger scale was the
   alternative and it is wrong: the chair becomes the only object on
   screen at exactly the moment it would stop belonging to the sticker
   language everything else in the game is drawn in, so it would read as
   a different object rather than as the same one, bigger. The edge is
   the object's identity, not its decoration.
   Transform only — no width, height or margin is touched — so the growth
   cannot reflow the composite it is leaving. */
.sc-intro.is-loading .i-chair{
  transition:transform var(--t-load-rise) var(--e-settle);
  will-change:transform;
}

/* THE BAR. Sticker construction, same as everything else that matters:
   3px white die-cut, a hard keyline outside it, a flat extrusion. The
   cyan is the chyron's own #37C4FF, which is the accent this app already
   uses for "this is the live thing on screen".
   NO LABEL — see loadingBeat(). It carries a progressbar role and an
   accessible name instead, so it is announced without being drawn. */
.i-load{
  position:absolute; left:50%; translate:-50% 0; z-index:6;
  width:min(196px, 54%); height:18px;
  border:3px solid var(--edge); border-radius:999px;
  background:rgba(24,22,20,.55);
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
  overflow:hidden;
  opacity:0; transition:opacity var(--t-load-barin) linear;
}
.i-load.is-in{ opacity:1; }
.i-load__fill{
  display:block; height:100%; width:0;
  border-radius:999px;
  background:linear-gradient(180deg,#7FDCFF 0%,#37C4FF 46%,#12A9E8 100%);
}
/* DECELERATING INTO THE END, which is the one thing a fake bar has to get
   right — a linear fill reads as a timer, and a timer invites the player
   to notice it is exactly the same length every time.
   THE FIRST CURVE HERE WAS cubic-bezier(.12,.72,.24,1) AND IT WAS WRONG:
   measured on the running page it was 42% full at 9% of the duration and
   93% at the halfway point, so the bar sprinted and then crawled — the
   opposite of the brief, and the shape that makes a progress bar feel
   like it is lying. This one is near-linear through the first half and
   eases only into the last third: 9/38/79/96/99% at 9/25/50/75/90% of
   the clock. */
.i-load.is-filling .i-load__fill{
  width:100%;
  transition:width var(--t-load-fill) cubic-bezier(.30,.22,.30,1);
}

@media (prefers-reduced-motion: reduce){
  /* the beat does not run at all — loadingBeat() goes straight to the
     destination — so there is nothing here to soften. */
  .i-load, .sc-intro.is-loading .i-chair{ transition:none; }
}

/* =====================================================================
   §S-1 · THE FINALE AS A SEQUENCE
   Two numbers, a bar between, counting up; then the player's own token
   flies to the side they voted for. Type sizes are the spec's, and the
   spec is explicit about why: the old lower section PASSED WCAG AA at
   every element — worst item 5.32:1 — so contrast was never the problem
   and darkening grounds or brightening text would have fixed nothing.
   It was SIZE and DENSITY. Everything below is bigger, and three things
   went behind a tap.
   ===================================================================== */

.f5{ align-items:stretch; }
/* THE BOARD IS CENTRED WHILE IT IS ALONE AND THEN HOLDS ITS PLACE.
   justify-content:center cannot do this — it re-centres the whole group
   every time a block is added, so the board climbed 441 -> 404 -> 340 ->
   75 as the outcome, the coins and the reading arrived, which is four
   moves for an object that is supposed to make one. The offset is
   measured in JS by f5Place() instead: it is the centring offset while
   the board is alone, and afterwards the SMALLER of that and whatever
   the content actually leaves, so the board holds until the content
   genuinely needs the room and then rises by exactly that much. */
.f5 .b5fit{ display:flex; flex-direction:column; gap:11px; position:relative;
  justify-content:flex-start;
  transition:padding-top var(--t-f5-in) var(--e-settle); }

/* ---- the board -------------------------------------------------------
   A GENERIC plenum display and deliberately not the Knesset's own: no
   emblem, no menorah, no colonnade, no seat plan. Those are protected
   insignia and were excluded before this brief. */
.f5board{
  background:#15130F; border-radius:14px; padding:24px 16px 20px;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4);
  position:relative; overflow:hidden; flex:none;
  transition:padding var(--t-f5-in) var(--e-settle);
}
/* the scanline. It is what makes it read as a lit board rather than as a
   dark panel, and it is 1px in 3 so it never muddies the numerals. */
.f5board::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:repeating-linear-gradient(to bottom,rgba(255,255,255,.045) 0 1px,transparent 1px 3px);
}
.f5row{ display:flex; align-items:flex-end; gap:10px; }
.f5cell{ flex:1; text-align:center; }
.f5lab{ font-size:13px; font-weight:700; color:#8E897C; margin:0 0 2px; }
.f5dash{ flex:0 0 22px; padding-bottom:20px; color:#5A554A; font-size:24px; text-align:center; }
/* 74px, and tabular so the numerals do not jitter as they climb — a
   proportional 1 is narrower than a 0 and the whole block would twitch
   on every frame of the count. */
.f5n{
  direction:ltr; font-family:var(--font-display); font-weight:900;
  font-size:74px; line-height:.92; font-variant-numeric:tabular-nums;
  display:flex; align-items:center; justify-content:center; gap:8px;
}
.f5n--for{ color:var(--f5-for); }
.f5n--ag { color:var(--f5-ag); }
/* ITEM 11d · THE PLAYER'S SIDE GOES GOLD, and the transition is declared
   on .f5n rather than on the state class so it eases in BOTH directions —
   there is nothing that takes it off today, and a one-way transition is
   how a colour ends up snapping back in some future beat.
   WHAT THIS BUYS, and it is the reason the item is worth doing: the board
   colours בעד lime and נגד salmon, which is vote direction carrying
   colour. The gold tick pulls the one numeral the player is responsible
   for OUT of that pair — for the rest of the beat their own total is
   neither of the two direction colours. See the §S-1 note above: the
   direction coding predates this and is NOT extended here. */
.f5n{ transition:color var(--t-f5-tick) var(--e-out); }
.f5n.is-mine{ color:var(--f5-gold); }
/* the slot the token lands in. It holds its size from the start so the
   numerals do not shift sideways when the avatar arrives — a landing
   that moves the thing it lands next to reads as a collision. */
.f5slot{ flex:none; width:34px; height:34px; display:block; }
.f5av{ display:block; width:100%; height:100%; border-radius:50%; overflow:hidden;
  background:#C9BFA6; box-shadow:var(--f5-token); }
.f5av svg{ display:block; width:100%; height:100%; }

/* ---- ITEM 11d · THE ODOMETER ---------------------------------------
   Per DIGIT, not per number: 63 -> 64 slides the 4 up past the 3 and
   leaves the 6 standing, which is what an odometer does and what a whole
   numeral sliding as a block does not. Digits that do not change are
   plain text and never enter a window.
   THE WINDOW IS EXACTLY THE LINE BOX, AND THE BLEED IS ZERO. The first
   version bled .22em at each end on the assumption that the figures' ink
   is taller than the line box. It is not, and the assumption cost a real
   defect: a bled window is a window that PAINTS OUTSIDE THE NUMERAL, and
   the label sits 1.88px above it, so the outgoing digit slid up over
   נגד / בעד and wiped it for the length of the move.
   MEASURED, at 74px: the digits' ink is 56.24px tall (max ascent 54.91,
   max descent 1.33 across 0-9) inside a 68.08px line box, and the glyph
   lands 0.13px below the box top and 11.71px above its bottom. So the
   line box already contains every figure and nothing needs to be bled.
   THE ROW IS THE LINE BOX FOR THE SAME REASON THE WINDOW IS: the digit
   has to sit at exactly the height the plain text node will, or swapping
   the windows back for text at the end of the move is a jump.
   THE STEP IS THE WINDOW, so the outgoing digit is clear of the frame at
   the same moment the incoming one is centred in it.
   THE MARKUP IS TEMPORARY: tickVote() puts the settled value back as
   plain text when the move ends, so the resting numeral is the same
   single text node it has always been and no window survives the beat. */
/* THE WINDOW IS ONE FIGURE WIDE, AND THE ROW CANNOT WRAP OR SHRINK.
   Measured, not assumed: left to shrink-to-fit, m1's 99 -> 100 read 91.8px
   as text, 100px through the whole 220ms slide and 136.8px when the text
   node came back — the numeral changed width twice inside one move. Two
   causes, both here. The <b> is a FLEX ITEM: as a run of inline-blocks its
   min-content is the widest single one, so flex-shrink squeezed it to the
   cell; as a text node it is the whole number and cannot shrink, so it
   overflows the cell instead. And an empty leading row makes its window
   as narrow as the '1' it is going to hold.
   THE WIDTH IS SET FROM JS, PER WINDOW, and 1ch is NOT it — measured at
   this size, 1ch is 46.914px against a figure advance of 45.883, so a
   two-digit numeral bulged 2.06px for the length of the slide. tickVote()
   measures the advance of the digit each window SETTLES on and writes it
   here, which makes the sum of the windows exactly the width of the text
   node that replaces them.
   AND THE FIGURES ARE NOT QUITE TABULAR. font-variant-numeric:tabular-nums
   is set and 1-9 all advance 45.883px at 74px — but ZERO is 45.438, 0.445
   narrower, so SimplerPro is not equalising it. Measuring per digit
   absorbs that; assuming one figure width would not. */
.f5n > b{ white-space:nowrap; }
.f5dig{
  --f5-dig-line: .92em;                /* .f5n's own line-height        */
  --f5-dig-bleed: 0px;                 /* see above — measured, not guessed */
  display:inline-block; vertical-align:top; overflow:hidden;
  text-align:center;                   /* width: set per window, in JS   */
  height:calc(var(--f5-dig-line) + 2 * var(--f5-dig-bleed));
  margin-block:calc(-1 * var(--f5-dig-bleed));
}
.f5dig__c{ display:block; transition:translate var(--t-f5-tick) var(--e-out); }
.f5dig__c > i{
  display:block; font-style:normal; text-align:center;
  height:calc(var(--f5-dig-line) + 2 * var(--f5-dig-bleed));
  line-height:calc(var(--f5-dig-line) + 2 * var(--f5-dig-bleed));
}
.f5dig.is-up .f5dig__c{ translate:0 calc(-1 * (var(--f5-dig-line) + 2 * var(--f5-dig-bleed))); }

.f5bar{
  position:relative; height:14px; border-radius:99px; overflow:visible;
  display:flex; margin-top:9px;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
}
.f5bar > i{ display:block; height:100%; min-width:0; }
.f5bar > i:first-child{ border-radius:99px 0 0 99px; }
.f5bar > i:last-child { border-radius:0 99px 99px 0; }
/* ITEM 11d · THE BAR GROWS BY ONE UNIT OF ITS OWN SCALE. flex-grow is a
   <number> and interpolates, so the +1 is the same value the count-up
   paints — one more of the 120 the bar is already divided into — eased
   rather than assigned.
   THE TRANSITION IS ARMED, NOT STANDING. runCount() writes flexGrow on
   every frame of a 2.4s count; a standing transition would put a 220ms
   ease on each of those frames and smear the whole count. .is-plus1 is
   added on the frame the +1 is written and never before it. */
.f5bar.is-plus1 > i{ transition:flex-grow var(--t-f5-tick) var(--e-out); }
.f5bar__f{ background:var(--f5-for); }
.f5bar__a{ background:var(--f5-ag); }
.f5bar__r{ background:var(--f5-rem); }
/* THE MAJORITY LINE SITS AT 61/121, not at the midpoint. 50.4% is where
   the 61st vote of a 121-seat house actually falls, and the half-point
   would put it a whole seat out. */
.f5maj{
  position:absolute; top:-5px; bottom:-5px; inset-inline-start:50.4%; width:3px;
  background:#FBF7EE; box-shadow:0 0 0 1px rgba(0,0,0,.5);
  transition:box-shadow 120ms linear;
}
.f5maj.is-flare{ box-shadow:0 0 0 1px rgba(0,0,0,.5), 0 0 16px 5px rgba(251,247,238,.9); }
.f5majlab{
  position:absolute; top:-20px; inset-inline-start:50.4%; transform:translateX(50%);
  font-family:var(--font-display); font-weight:900; font-size:11px; color:#FBF7EE;
}
/* ITEM 11 · נמנע LANDS ON THE LINE. There is no side to land beside, so
   the token goes to the 61 marker itself — the thing the abstention is
   measured against — and nothing on the board changes: no tick, no bar
   move, no colour. It is built from .f5maj's own two numbers, 50.4% and
   the same translateX(50%) the label uses, so it is centred on the line
   rather than near it under RTL.
   THE MARKER DOES NOT MOVE FOR IT, in this case or any other. */
.f5slot--maj{
  position:absolute; z-index:2; top:50%; inset-inline-start:50.4%;
  /* 50.4% is where the marker's LEADING EDGE sits, not its centre — it is
     3px wide and drawn from that line. translateX(50%) alone put the token
     on the line and therefore 1.5px off the thing it is supposed to be
     centred on; half the marker's width puts the two centres together.
     Measured: 1.5px before, 0.0px after. */
  margin-inline-start:1.5px;
  transform:translateX(50%) translateY(-50%);
}

/* the degraded twin's peak: with no count, the outcome prose IS the
   loudest object, at 19px. It is the only account of the outcome that
   exists on those seven issues. */
.f5board--prose{ padding:22px 16px 20px; }
.f5prose{
  margin:0; font-family:var(--font-display); font-weight:900;
  font-size:19px; line-height:1.35; color:#EFECE4; text-align:start;
}

/* ITEM 8 · NOTHING ADDS .is-strip ANY MORE. The board keeps its full
   size for the whole beat and rises instead; beat5() no longer sets the
   class. The rules below are kept, unreferenced, because they are the
   only record of what the demoted board looked like and because
   .f5plate.is-small above still reads one of them — deleting them would
   take that with it. If the strip is not coming back, this block and the
   is-small variant go together. */
/* ---- the strip. THE BOARD DOES NOT LEAVE, IT DEMOTES — same element,
   same numbers, same bar, re-laid onto one line so the outcome stays
   readable above the sheet while the reading takes the room.
   `display:contents` on the row is what lets the bar move BETWEEN the
   two numerals without the markup changing: the cells become direct
   children of the board and `order` puts the bar in the middle. Under
   RTL order 1 is the rightmost, which is the side בעד is on. */
.f5board.is-strip{ padding:11px 12px 10px; }
.f5board--num.is-strip{ display:flex; align-items:center; gap:10px; }
.f5board--num.is-strip .f5row{ display:contents; }
.f5board.is-strip .f5lab, .f5board.is-strip .f5dash{ display:none; }
.f5board--num.is-strip .f5cell{ flex:0 0 auto; }
.f5board--num.is-strip .f5cell:first-child{ order:1; }
.f5board--num.is-strip .f5bar{ order:2; flex:1; margin-top:0; }
.f5board--num.is-strip .f5cell:last-child{ order:3; }
.f5board.is-strip .f5n{ font-size:32px; }
.f5board.is-strip .f5slot{ width:26px; height:26px; }
.f5board.is-strip .f5prose{ font-size:19px; }

/* ---- the token, and it is the ONLY object on this beat carrying both a
   yellow keyline and a cyan glow. That pairing is what keeps it from
   ever being read as a verdict mark: lime and magenta are correctness,
   and this is neither. */
/* ITEM 11d · THE RING'S GOLD IS A NAMED TOKEN, because the numeral now
   has to take the SAME one. It was the literal #FFD60A twice here and
   nowhere else, so "the gold used by the avatar ring" was not addressable.
   ON THE RECORD: this value IS --primary, the coin and CTA yellow. The
   item asks for the ring's gold and says "not the coin/CTA yellow"; on
   this branch those are one colour, #FFD60A, and the ring is the thing
   named twice, so the ring wins and the numeral matches it exactly.
   Aliased rather than pointed at --primary: if a distinct finale gold is
   ever introduced, it is introduced HERE and both the ring and the
   numeral move together, which is the property the item is buying. */
:root{
  --f5-gold: #FFD60A;
  --f5-token:
  0 0 0 3px var(--f5-gold), 0 0 0 4.6px rgba(0,0,0,.5), 0 0 18px 5px rgba(55,196,255,.72); }
.f5board.is-strip .f5av, .f5plate.is-small .f5av{ box-shadow:
  0 0 0 2.5px var(--f5-gold), 0 0 0 4px rgba(0,0,0,.5), 0 0 14px 4px rgba(55,196,255,.72); }
/* the clone that actually travels. Fixed, above everything, so nothing
   it passes over can clip it. */
.f5fly{
  position:fixed; z-index:120; display:block; border-radius:50%; overflow:hidden;
  background:#C9BFA6; box-shadow:var(--f5-token); pointer-events:none;
}
.f5fly svg{ display:block; width:100%; height:100%; }
/* the banner the token LEFT. It dims and keeps an empty socket rather
   than closing up: the vote is still pinned there, the token has just
   gone. A banner that reflowed would read as the vote being withdrawn. */
.chyron .bnr.is-spent{ opacity:.42; transition:opacity var(--t-f5-in) linear; }
.chyron .bnr.is-spent .chyron-av{ visibility:hidden; }

/* ---- the plate the twin's token lands on. It moves, and it does not
   claim a number that is not there. */
.f5plate{
  display:flex; align-items:center; gap:11px; flex:none;
  background:#15130F; border-radius:14px; padding:14px 14px 13px;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4);
  font-size:15px; color:#EFECE4;
}
.f5plate__slot{ flex:none; width:34px; height:34px; display:block; }
/* 23 · the weight is STATED, not inherited from the UA's <b> default. It
   rendered at 900 already — bold resolves upward in a two-weight family —
   so this changes nothing on screen and everything about whether the next
   reader can tell that 900 was the intention. */
.f5plate__t b{ font-family:var(--font-display); font-weight:900; font-size:19px; color:#FBF7EE; margin-inline-start:6px; }

/* ---- the resolution line ---- */
.f5res{
  margin:0; text-align:center; font-family:var(--font-display); font-weight:900;
  font-size:16px; line-height:1.35; color:#EFECE4;
}
/* 23 · 700 -> 400, AND THE DECLARATION FINALLY DOES SOMETHING. The family
   ships 400 and 900 only, and CSS resolves an unavailable 700 upward, so
   this line has been printing the Black at 14px — identical in weight to
   the 16px/900 resolution above it. Everything else here demotes it (14px
   against 16, #E6E2D8 against #EFECE4); the weight was the one part of
   that intent that never landed. The real tally owns this board; the
   player's own vote arrives quieter. */
/* ITEM 51A · THIS ONE PLACEHOLDER IS NOT HIDDEN BY no-ph, and it is the
   second exception in the file for the same reason .pr-ph is the first:
   hiding the marker here does not leave a cosmetic gap, it removes the
   only statement of what the Knesset did. The two rejected issues would
   render a bare 'עם הקול שלכם: 27—43' with nothing above it — a wrong
   sentence traded for a missing one.
   It is ph()'s own treatment rather than a third copy of the hazard fill,
   scoped to this line so no other placeholder is affected. When Tamar's
   string lands, the ph() call goes and this rule goes with it. */
body.no-ph .f5res .ph{ display:inline-block !important; }
.f5res .ph{ font-family:var(--font-ui); }
.f5res__you{ display:block; font-size:14px; font-weight:400; color:#E6E2D8; }
/* 23 · the numeral stays IN the demoted line. <b> takes the UA's bold,
   which in a 400/900 family resolves to the Black — so the figure was
   jumping back to the weight the line had just stepped down from. It
   keeps the brighter ink, which is what marks it as the number; the
   weight is the parent's. */
.f5res__you b{ font-weight:400; color:#FBF7EE; }

.f5shape{ margin:0; font-size:14px; font-weight:700; color:#E6E2D8; text-align:center; }

/* ---- the reading. 15px, up from 12.5px. ---- */
.f5read{ margin:0; }
.f5exp{ margin:0; font-size:15px; line-height:1.55; color:#F2EFE7; }
.f5more{
  display:inline-flex; align-items:center; gap:5px; margin-top:9px;
  border:0; cursor:pointer; font-family:var(--font-display); font-weight:900;
  font-size:14px; color:#131310; background:#D8D2C4; border-radius:999px;
  padding:5px 13px 6px; box-shadow:0 2px 0 rgba(0,0,0,.4);
}

/* ---- the coin moment ---- */
/* OUT OF FLOW, because it is a MOMENT and not a block. In the column it
   was 117px of layout that arrived, pushed the board up 13px and then
   gave it back 800ms later — the board bounced 339 -> 326 -> 339 before
   its real settle, which is exactly the double move Part 3 forbids. It
   is absolutely placed under the board instead, so it can be as large as
   it likes and move nothing. */
.f5coin{
  position:absolute; left:50%; translate:-50% 0; z-index:3;
  display:flex; flex-direction:column; align-items:center;
  opacity:0; transform:scale(.9) rotate(-3deg);
  transition:opacity var(--t-f5-in) linear, transform var(--t-f5-in) var(--e-land);
}
.f5coin.is-in{ opacity:1; transform:scale(1) rotate(-3deg); }
.f5coin.is-out{ opacity:0; transform:scale(.94) rotate(-3deg);
  transition:opacity var(--t-f5-coin-out) linear, transform var(--t-f5-coin-out) var(--e-in); }
.f5coin__n{
  font-family:var(--font-display); font-weight:900; font-size:34px; direction:ltr;
  color:#131310; background:var(--primary); padding:10px 20px 12px; border-radius:16px;
  box-shadow:0 0 0 3px #fff, 0 0 0 4.6px rgba(0,0,0,.55), 0 5px 0 rgba(0,0,0,.34);
}
.f5coin__sub{ margin:9px 0 0; font-size:13px; color:#D8D2C4; text-align:center; }
.f5coin__tot{ margin:3px 0 0; font-size:12px; font-weight:700; color:#8E897C; text-align:center; }

/* ---- the buttons. Last, and nothing lands after them. ---- */
.f5acts{ margin-top:auto; padding-top:12px; display:flex; flex-direction:column;
  align-items:stretch; gap:8px; flex:none; }
.f5go{ font-size:18px; min-height:0; padding:12px 0 13px; }
.f5back{
  align-self:center; border:0; background:none; cursor:pointer;
  font-family:var(--font-display); font-weight:900; font-size:15px; color:#D8D2C4;
  padding:9px 18px 10px; text-decoration:underline; text-underline-offset:3px;
}

/* ---- PART 2 · the disclosure's own surfaces are GONE. .f5scrim,
   .f5sheet, .f5grab, .f5sheet__body and .f5close went with moreSheet();
   the content opens on stickerModal() now and that component draws the
   box, the scrim and the close. Only the things INSIDE it survive —
   chips, links and the definition line — because those are content and
   not surface. */
.f5chips{ display:flex; flex-wrap:wrap; gap:6px; }
.f5chip{
  border:0; cursor:pointer; padding:4px 11px 5px; border-radius:12px;
  background:rgba(255,255,255,.16); color:#EFECE4;
  font-family:var(--font-display); font-weight:900; font-size:13px;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
}
.f5def{ margin:0; font-size:14px; line-height:1.5; color:#E6E2D8; }
/* inside the white modal the chips and links sit on paper, not on the
   dark ground they were built for */
.stmodal__box .f5chips{ margin-top:12px; justify-content:center; }
.stmodal__box .f5chip{
  background:rgba(19,19,16,.10); color:var(--ink);
  box-shadow:inset 0 0 0 1.5px rgba(19,19,16,.28);
}
.stmodal__box .f5links{ margin-top:12px; }
.stmodal__box .f5link{ color:#2B2926; }
.f5links{ display:flex; flex-direction:column; gap:4px; }
.f5link{
  font-size:14px; font-weight:700; color:#EFECE4; text-decoration:underline;
  text-underline-offset:3px; display:flex; gap:7px; align-items:center; padding:3px 0;
}
.f5link.is-missing{ opacity:.5; text-decoration:none; }
.f5close{
  align-self:center; flex:none; border:0; cursor:pointer;
  font-family:var(--font-display); font-weight:900; font-size:14px;
  color:#131310; background:#D8D2C4; border-radius:999px; padding:6px 18px 7px;
  box-shadow:0 2px 0 rgba(0,0,0,.4);
}

@media (prefers-reduced-motion: reduce){
  .f5board, .f5coin, .f5sheet, .f5scrim, .f5maj{ transition:none; }
}

/* =====================================================================
   §T · THE TACHLES → CASCADE TRANSITION
   The player's vote leaves the button, assembles into a callout over the
   chair, becomes the banner, and flies to its pin — while the bill slides
   in behind it. One object the whole way; nothing is created, swapped or
   faded into anything else mid-flight.
   ===================================================================== */

/* THE CALLOUT IS A SIBLING OF THE BANNER, not a relative of it: same B-5
   construction, sized to its contents, skewed — but set down at the
   opposite angle so the two read as the same kind of object caught at
   two moments rather than as one object that slid. It carries NO cyan
   halo. Only the pinned banner does; the callout is a way-station. */
.tcal{
  position:absolute; z-index:11; pointer-events:none;
  display:inline-flex; align-items:center; gap:8px;
  padding:6px 6px 7px 6px;
  padding-inline-start:11px; padding-inline-end:12px;
  font-family:var(--font-display); font-weight:900; font-size:22px;
  color:var(--ink);
  border:3px solid transparent; border-radius:13px;
  transform-origin:50% 50%;
  will-change:transform;
}
/* PHASE 1 · the letters travel. The sticker is not there yet — no fill,
   no edge, no extrusion — so what leaves the button is the WORD, which is
   the thing the player just chose. */
.tcal__w{ display:block; line-height:1; white-space:nowrap; }
.tcal:not(.is-sticker) .tcal__w{ color:var(--paper); text-shadow:0 2px 6px rgba(0,0,0,.6); }

/* PHASE 2 · the avatar joins. It scales up from nothing at the point the
   letters are passing through, so the pair assembles in flight. */
.tcal__av{
  display:block; width:30px; height:30px; flex:none;
  opacity:0; transform:scale(.4);
  transition:opacity var(--t-tc-av) linear, transform var(--t-tc-av) var(--e-land);
}
.tcal__av svg{ display:block; width:100%; height:100%; }
.tcal.is-paired .tcal__av{ opacity:1; transform:none; }

/* PHASE 3 · the construction resolves. The fill, the white die-cut, the
   keyline and the extrusion arrive together on one transition, which is
   what makes it read as the sticker being CUT rather than as a panel
   fading up behind the words. */
.tcal{
  background:transparent;
  box-shadow:0 0 0 0 var(--keyline), 0 0 0 rgba(0,0,0,0);
  transition:transform var(--t-tc-letters) var(--e-settle),
             background var(--t-tc-resolve) linear,
             border-color var(--t-tc-resolve) linear,
             box-shadow var(--t-tc-resolve) linear;
}
.tcal.is-sticker{
  background:var(--paper);
  border-color:var(--edge);
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
}
.tcal.is-sticker .tcal__w{ color:var(--ink); text-shadow:none; }

/* PHASE 4 · the travel. The longest step and the one the eye follows, so
   it gets --e-land: it arrives and sets down rather than easing to a
   stop. */
.tcal.is-pinning{
  transition:transform var(--t-tc-travel) var(--e-land),
             background var(--t-tc-travel) linear,
             box-shadow var(--t-tc-travel) linear;
}

/* the chosen chip's own label goes invisible the moment the letters
   leave it — the word is now the flying object, and two of it on screen
   would break the illusion that it travelled. */
.v-a.is-chosen .v-a__lab{ opacity:0; transition:opacity 60ms linear; }

/* ---- the surface behind, once the sequence has landed ----------------
   THE BLUR HOLDS AT 3px RATHER THAN CLEARING. A fully-cleared blur with
   nothing moving looks finished, and this beat is not finished — it is
   waiting for a tap. The residual keeps the surface reading as a surface,
   and the slow breath on it plus the affordance below are what say the
   screen is alive rather than dead. */
.ov--stage.is-held{
  -webkit-backdrop-filter:blur(3px) saturate(.94);
  backdrop-filter:blur(3px) saturate(.94);
  transition:backdrop-filter var(--t-tc-next) linear,
             -webkit-backdrop-filter var(--t-tc-next) linear;
  animation:tc-breathe 2800ms ease-in-out 400ms infinite;
}
@keyframes tc-breathe{
  0%,100%{ background:rgba(28,26,23,.52) }
  50%    { background:rgba(28,26,23,.44) }
}

/* ---- THE TAP AFFORDANCE. It must be obvious: this beat must never look
   like a dead screen. A pill on the sticker family with a moving chevron,
   not a whispered line of small print — the whole surface is the target,
   so the cue has to be as loud as the action is large. */
.tctap{
  position:absolute; z-index:10; left:50%; translate:-50% 0;
  bottom:calc(26px + env(safe-area-inset-bottom));
  display:inline-flex; align-items:center; gap:9px;
  padding:9px 18px 10px; border-radius:999px;
  background:var(--paper); color:var(--ink);
  border:3px solid var(--edge);
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
  font-family:var(--font-display); font-weight:900; font-size:15px;
  white-space:nowrap; pointer-events:none;
  opacity:0; transform:translateY(10px);
  transition:opacity var(--t-tc-next) linear, transform var(--t-tc-next) var(--e-land);
}
.tctap.is-in{ opacity:1; transform:none; animation:tc-tap 2000ms ease-in-out 600ms infinite; }
.tctap__c{ display:block; font-size:17px; line-height:1; }
@keyframes tc-tap{
  0%,100%{ transform:translateY(0)   }
  50%    { transform:translateY(-4px) }
}

@media (prefers-reduced-motion: reduce){
  .tcal, .tcal__av{ transition:none; }
  .tctap.is-in{ animation:none; }
  .ov--stage.is-held{ animation:none; }
}

/* THE CHIP THE WORD CAME OUT OF. With its label gone it was a blank white
   pill sitting there for the rest of the beat, which reads as a rendering
   fault rather than as a choice that has been made. It becomes the SOCKET
   instead — the die-cut sheet with the sticker peeled off it: the keyline
   and the shape stay, the fill and the extrusion go. The option is not
   hidden (the round never hides what it offered); it is visibly spent,
   and the thing that was in it is now on its way to the banner. */
.ovpane--vote.is-taken .b2votes .v-a.is-chosen{
  background:transparent;
  box-shadow:0 0 0 3px rgba(255,255,255,.34), 0 0 0 var(--ring-chip) rgba(0,0,0,.34);
  transition:background var(--t-tc-letters) linear, box-shadow var(--t-tc-letters) linear;
}

/* =====================================================================
   §S-1 AMENDMENTS · parts 1, 3 and 4
   ===================================================================== */

/* ---- PART 1 · the emptied banner leaves ------------------------------
   Slide up and fade, 250ms, no pop and no scale. It is not being
   dismissed — it has handed its content to the board and is vacating. It
   goes to hidden at the end so it cannot be tabbed to or read out. */
/* [hidden] IS A UA RULE AND .chyron SETS display:flex, so the class won
   and the emptied banner stayed on screen for the whole of beat 5 —
   grey, spent, and exactly the object Part 1 exists to remove. The
   attribute has to be made to stick. */
.chyron[hidden]{ display:none !important; }
.chyron.is-exiting{
  opacity:0; transform:translateY(-14px);
  transition:opacity var(--t-f5-bnr-out) linear,
             transform var(--t-f5-bnr-out) var(--e-in);
}

/* ---- PART 3 · the finale takes the round's chrome back ---------------
   The banner's slot, the helper line and .round's top padding are 88px
   reserved for a card that does not exist on this beat, and they were
   what put the board 65-70px below centre. They collapse the moment beat
   5 starts — BEFORE the banner leaves, not with it, because the banner
   is absolute at stage level with its box already frozen: it does not
   move when the slot goes, so the board is centred against the layout
   that will exist after the exit and is never placed twice. */
.sc-round.is-finale .chyron-slot{ min-height:0; margin:0; }
.sc-round.is-finale .helper{ display:none; }
.sc-round.is-finale .round{ padding-top:0; }
/* .b5 > * is flex:none, which left .b5fit at its content height inside a
   taller .beat — so `justify-content:center` was centring against the
   content rather than against the screen. */
.f5 > .b5fit{ flex:1; height:auto; }
/* .b5's own clamp(14px,5vh,44px) top padding is the last of the chrome
   that was pushing the board down — 42px at 844 — and on this beat it
   pads nothing: the board centres itself and the buttons are pinned to
   the floor by their own auto margin. Zeroed here rather than on .b5,
   which the other beat-5 variants still use. */
.f5{ padding-top:0; }

/* ---- PART 4 · EVERY BLOCK SITS ON THE BOARD --------------------------
   The rule is that nothing on beat 5 sits directly on the dot-grid
   ground. The tally had a surface and the resolution, the guess line and
   the prose did not, so the beat read as one object plus three captions
   floating under it. They are all the same blackboard now — same fill,
   same inset hairline, same extrusion, same scanline — at a quieter
   padding than the tally, because the tally is still the peak.
   ONE SURFACE, DECLARED ONCE. .f5board keeps its own paddings and its
   glow; everything else takes the surface and nothing more. */
.f5board, .f5plate, .f5surf{
  background:#15130F; border-radius:14px;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4);
  position:relative; overflow:hidden; flex:none;
}
.f5surf::after, .f5plate::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:repeating-linear-gradient(to bottom,rgba(255,255,255,.045) 0 1px,transparent 1px 3px);
}
.f5surf{ padding:11px 14px 12px; }
.f5surf > *{ position:relative; z-index:1; }

/* the resolution and the guess line are ONE surface, not two: they are
   both the outcome — what happened, and what the player made of it —
   and splitting them put a hairline between a sentence and its own
   footnote. */
.f5outcome{ display:flex; flex-direction:column; gap:7px; }
.f5outcome .f5res, .f5outcome .f5shape{ margin:0; }
.f5read{ padding:12px 14px 13px; }
.f5read .f5more{ margin-top:11px; }

/* ---- PART 4 · THE GLOW ----------------------------------------------
   WARM YELLOW-CREAM, and it is neither of the two colours it could have
   been mistaken for. Not --primary #FFD60A: that is the CTA and the coin,
   and a saturated yellow halo on a non-tappable board would read as an
   invitation to press it. Not cyan: that is the avatar token's identity,
   the token LANDS on this board, and a cyan board would swallow the one
   object whose whole job is to stay legible as the player.
   #F5E3AA is warm, desaturated against the CTA, and carries no meaning
   anywhere else in the app. It is a halo only — no ring, no hard rule —
   so it can never read as an edge. */
:root{ --f5-glow:245,227,170; }
@keyframes f5-glow{
  0%,100%{ box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4),
                      0 0 14px 1px rgba(var(--f5-glow),.26),
                      0 0 34px 6px rgba(var(--f5-glow),.11); }
  50%    { box-shadow:inset 0 0 0 2px rgba(255,255,255,.16), 0 4px 0 rgba(0,0,0,.4),
                      0 0 22px 3px rgba(var(--f5-glow),.46),
                      0 0 52px 12px rgba(var(--f5-glow),.20); }
}
/* it breathes while the board is the beat — through the count, the hold
   and the landing — and settles the moment there is somewhere else for
   the eye to go. */
.f5board{ animation:f5-glow 2200ms ease-in-out infinite; }
.f5board.is-glow-calm{
  animation:none;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4),
             0 0 12px 1px rgba(var(--f5-glow),.20),
             0 0 28px 5px rgba(var(--f5-glow),.09);
  transition:box-shadow var(--t-f5-in) linear;
}

@media (prefers-reduced-motion: reduce){
  .f5board{ animation:none;
    box-shadow:inset 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 rgba(0,0,0,.4),
               0 0 16px 2px rgba(var(--f5-glow),.30),
               0 0 38px 7px rgba(var(--f5-glow),.13); }
  .chyron.is-exiting{ transition:none; }
}

/* ---- PART A · THE SETTLED STACK RE-CENTRES, ONCE ---------------------
   Holding the board at its centred position while the reading filled in
   below it left 327px of bare ground above the stack at 390 and 6px
   under it — correct to the letter of the last brief and wrong to look
   at. After the buttons land, and only then, the whole stack centres in
   the content box in ONE eased move.
   .f5acts loses its auto margin for it: pinned to the floor, only the
   top of the stack could move and the result would be a stretch rather
   than a re-centre. It is the last motion on the beat — nothing is
   appended after the buttons, so nothing follows it. */
/* ITEM 8 · 400 -> 320ms, and on --e-out rather than a curve written out
   here. The move itself is unchanged in kind: padding-top on the column,
   measured by f5Place() and eased to, which is what makes it impossible
   for the board to jump — the value is restored to where the board
   actually is before the transition is re-armed and the measured figure
   set. What DID change is when it runs: every panel is in the DOM and in
   layout before this fires, so there is one measurement and one move
   instead of a move that had to dodge blocks still arriving. */
.f5 .b5fit.is-recentring{
  transition:padding-top var(--t-f5-board) var(--e-out);
}
/* ITEM 8 · THE PANELS COME FROM BELOW, BEHIND THE BOARD'S MOVE. .b5stage
   lifts 6px, which is an arrival rather than a slide; these travel far
   enough to read as coming from under the board. The delay is set inline
   per block — the stagger is a position in a list, not a class per slot.
   They are appended BEFORE the move and held at opacity 0, so the
   measurement the move is based on already includes them. */
.f5late{
  transform:translateY(22px);
  transition-timing-function:var(--e-out);
}
.f5 .b5fit.is-recentring .f5acts{ margin-top:0; }
@media (prefers-reduced-motion: reduce){
  .f5 .b5fit.is-recentring{ transition:none; }
}

/* =====================================================================
   §1b · THE LOADING SCREEN'S EXIT — the chair flies into the player
   Zooming into the chair IS sitting down in it. The 121st seat is the
   game's title and this beat is the only place it is enacted rather
   than stated.
   ===================================================================== */

/* THE STRUCTURAL FIX, and it is the same class of blocker the chyron
   was. Two things stopped the destination co-existing under the loading
   screen, not one:
     1  showScreen() sets .hidden on every screen but the named one, so
        rendering the map destroys the chair mid-flight.
     2  even unhidden they could not overlap — .screen is flex:1 inside
        .stage's flex COLUMN, so two visible screens stack vertically.
   The intro is taken OUT OF FLOW for the whole loading beat instead.
   showScreen()'s contract is untouched: it still hides the intro when
   the map is shown, and the exit simply un-hides it again for the few
   hundred ms it is still flying. Nothing is faked and no screenshot is
   substituted for the live screen — the map underneath is the real map,
   already rendered and already animating.
   It goes absolute BEFORE the chair is measured, so no coordinate the
   loading beat computes is invalidated by the switch. */
.sc-intro.is-lifted{
  position:absolute; inset:0; z-index:40;
  padding:calc(10px + env(safe-area-inset-top))
          calc(16px + env(safe-area-inset-right))
          calc(12px + env(safe-area-inset-bottom))
          calc(16px + env(safe-area-inset-left));
}
/* the plaza gradient is .sc-intro::before and would otherwise paint an
   opaque band over the map for the whole zoom */
.sc-intro.is-lifted.is-loading::before{ opacity:0; }

/* the bar goes first, on its own */
.i-load.is-out{
  opacity:0;
  transition:opacity var(--t-lx-bar) linear;
}

/* THE LAUNCH. Transform and opacity only — no width, height, margin or
   layout property is touched, so the whole thing is compositor work and
   holds frame rate on a cheap Android.
   EASE-IN, and that is the point of it: a symmetrical ease reads as the
   chair politely growing, and this has to read as the chair rushing the
   camera. It accelerates all the way out.
   THE FADE IS THE HANDOFF, not a cut, AND IT BELONGS TO THE SAME EVENT
   as the zoom rather than being something that happens to the chair
   afterwards. It begins on the frame the chair reaches full-screen and
   reaches zero on the frame the scale does, so the object is dissolving
   the whole time it is still visibly rushing at the camera. */
/* the parent is in the selector because it has to be: the loading beat's
   own rule is `.sc-intro.is-loading .i-chair` at (0,3,1) and this was
   (0,2,1), so it lost — the chair got no opacity transition at all and
   cut to nothing on the first frame of the zoom instead of fading over
   the last 40% of it. */
.sc-intro.is-loading .i-chair.is-launching{
  transition:transform var(--t-lx-zoom) cubic-bezier(.55,0,1,.45),
             opacity var(--t-lx-fade) cubic-bezier(0,0,.58,1) var(--t-lx-fade-at);
  opacity:0;
}

/* the destination, entering underneath while the chair is still coming.
   It overrides .sc-map's own 260ms/1.02 arrival — the same idea, given
   longer and a little more travel so it is legible through the zoom. */
.stage.is-launch .sc-map.is-arriving{
  animation:lx-dest var(--t-lx-dest) var(--e-settle) both;
}
@keyframes lx-dest{ from{ opacity:0; transform:scale(1.04) } }

@media (prefers-reduced-motion: reduce){
  /* no zoom at all — a plain crossfade. A 10x accelerating object filling
     the viewport is exactly what this setting is asked to suppress. */
  .sc-intro.is-loading .i-chair.is-launching{
    transition:opacity var(--t-lx-dest) linear;
    transform:none !important;
  }
  .stage.is-launch .sc-map.is-arriving{ animation:none; opacity:1; }
}

/* =====================================================================
   §E · THE END-GAME.

   IT TAKES THE ROUND'S OWN SURFACES. .eg-card is .f5surf — the same lit
   board, the same inset hairline, the same scanline — and .eg-go is
   .p-c. Nothing here invents a second visual system; the only new shapes
   are the allocation chip and the share card, and both are built out of
   the existing card and chip language.

   NO CORRECTNESS COLOUR ANYWHERE ON THIS SCREEN. --verdict-correct and
   --verdict-surprise are reserved for the D2 stamp, which answers "was
   this right". Finishing the map is not a correctness event and the
   surprise count is explicitly NOT a score, so neither hue appears here
   — and the D2 stamp itself is deliberately not reused, because reusing
   it would assert a rightness the beat does not mean.
   ===================================================================== */
.sc-end{
  align-items:stretch; justify-content:flex-start;
  padding:clamp(8px,2vh,18px) 0 calc(10px + env(safe-area-inset-bottom));
  position:relative; overflow:hidden;
}
/* the column carries every beat; #egFx is its SIBLING and sits under it,
   which is what makes "confetti never covers content" structural */
.eg-col{
  position:relative; z-index:2;
  flex:1; min-height:0; display:flex; flex-direction:column;
  align-items:stretch; gap:clamp(8px,1.6vh,14px);
  justify-content:center; text-align:center;
}
.eg-fx{ position:absolute; inset:0; z-index:1; pointer-events:none; overflow:hidden; }

/* =====================================================================
   ITEM 14 · THE ALLOCATION'S COINS
   THE SAME TOKEN, SMALLER. .coin-t is the award's 19px coin and this is
   that object at 13px — same fill, same inset keyline, same hard offset —
   because a second coin drawn a second way would be a second currency.
   It is a handful landing on a 56px row rather than a payment arriving at
   the HUD, so it reads at the size a handful is.
   THE LAYER IS #coinfly, SHARED. It is already "out of every layout, over
   everything", which is exactly what an arc between two elements in a
   scrolling column needs — .eg-fx is inside the column and behind it
   (z-index 1 against .eg-col's 2), so a sprite hosted there would fly
   BEHIND the rows it is landing on. */
.coin-t--eg{
  width:13px; height:13px;
  box-shadow:inset 0 0 0 1.5px rgba(0,0,0,.55), 0 1px 0 rgba(0,0,0,.3);
}
/* the balance's own acknowledgement: it is the number that just went
   down, so it is the thing that reacts. One pulse, no travel — the
   coins carry the movement and this only has to say "from here". */
.eg-left.is-pulse{ animation:eg-left-pulse var(--t-eg-pulse) var(--e-out); }
@keyframes eg-left-pulse{
  0%  { transform:scale(1) }
  50% { transform:scale(1.06) }
  100%{ transform:scale(1) }
}

/* ---- type ---------------------------------------------------------- */
.eg-eyebrow{
  /* 21 · 400, not the 700 it carried in system-ui. The family ships two
     faces, 400 and 900, and CSS font matching answers a 700 request from
     ABOVE first — so leaving 700 here would have printed the Black and
     made the eyebrow HEAVIER than before, which is the opposite of the
     change. Same reasoning on .eg-clear and .i-para. */
  margin:0; font-family:var(--font-body); font-size:var(--fs-meta);
  font-weight:400; letter-spacing:.08em; color:var(--ink-body);
}
.eg-h1{
  margin:0; font-family:var(--font-display); font-weight:900;
  font-size:clamp(30px,8vw,var(--fs-hero)); line-height:1.05; color:var(--paper);
}
.eg-h2{
  margin:0 0 2px; font-family:var(--font-display); font-weight:900;
  font-size:var(--fs-title); line-height:1.1; color:var(--paper);
  opacity:0; transform:translateY(6px);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-h2.is-in{ opacity:1; transform:none; }
.eg-sub, .eg-lede{
  /* 1.45 -> 1.6, AND THE REASON IS THE DESCENT, NOT THE X-HEIGHT.
     Measured at 17px: SimplerPro's Hebrew glyphs are SHORTER than
     system-ui's (ascent 8.82 vs 10.13) but its font bounding box is
     TALLER — descent 7 against 4, so a 23px em box against 20px. At the
     old 1.45 (24.65px) that left 1.65px of leading where system-ui had
     4.65px, and the two lines of the lede closed up. 1.6 (27.2px) puts
     the leading back at 4.2px, near the value the line was tuned to. */
  margin:0; font-family:var(--font-body); font-size:var(--fs-body);
  color:var(--ink-body); line-height:1.6;
}
.eg-lede{ opacity:0; transform:translateY(6px); padding:0 6px;
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle); }
.eg-lede.is-in{ opacity:1; transform:none; }

.eg-b1{ display:flex; flex-direction:column; gap:6px;
  opacity:0; transform:translateY(10px) scale(.98);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle); }
.eg-b1.is-in{ opacity:1; transform:none; }

/* ---- a stat surface ------------------------------------------------ */
.eg-card{
  padding:14px 14px 13px; text-align:start;
  opacity:0; transform:translateY(8px);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-card.is-in{ opacity:1; transform:none; }
.eg-card--quiet{ box-shadow:inset 0 0 0 2px rgba(255,255,255,.07), 0 3px 0 rgba(0,0,0,.34); }
/* the label takes the room and the numeral sits at the left edge, which
   is the key-right/value-left rule made structural rather than aligned */
.eg-stat{ margin:0; display:flex; align-items:baseline; gap:10px;
  justify-content:space-between; }
.eg-stat .eg-stat__l{ flex:1; min-width:0; }
/* SimplerPro tabular figures — the numerals must not jitter and must not
   re-order inside the RTL line; .num is the app's LTR digit run */
.eg-num{
  font-family:var(--font-display); font-weight:900;
  font-size:clamp(34px,10vw,46px); line-height:1; color:var(--paper);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  flex:none;
}
.eg-stat--sm .eg-num{ font-size:clamp(24px,7vw,30px); }
.eg-stat__l{
  font-family:var(--font-display); font-weight:900; font-size:var(--fs-body);
  line-height:1.25; color:var(--paper); text-align:start;
}
.eg-stat__sub{
  margin:6px 0 0; font-family:var(--font-ui); font-size:var(--fs-meta);
  color:var(--ink-faint); line-height:1.4; text-align:start;
}

/* ---- the allocation ------------------------------------------------ */
.eg-left{
  margin:0; font-family:var(--font-body); font-size:var(--fs-body);
  color:var(--ink-body); line-height:1.6;
}
.eg-left b{ font-family:var(--font-display); font-weight:900; color:var(--primary);
  font-variant-numeric:tabular-nums; }
.eg-left.is-spent{ color:var(--primary); }

.eg-chips{
  display:flex; flex-direction:column; gap:var(--gap-visible);
  overflow-y:auto; min-height:0; padding:2px 0;
  opacity:0; transition:opacity var(--t-f5-in) var(--e-settle);
}
.eg-chips.is-in{ opacity:1; }
/* KEY RIGHT, VALUE LEFT — the phone-sweep rule. The topic reads from the
   right edge and its number anchors left, like every other key/value row
   in the app. */
.eg-chip{
  display:flex; align-items:center; gap:10px;
  width:100%; padding:9px 12px; border:0;
  background:var(--kraft); color:var(--ink);
  border-radius:var(--r-chip); box-shadow:var(--shadow-press);
  font-family:var(--font-display); font-weight:900; font-size:var(--fs-body);
  text-align:start; cursor:pointer;
  transition:transform var(--t-press) var(--e-land), box-shadow var(--t-press) var(--e-land),
             background var(--t-f5-in) var(--e-settle);
}
.eg-chip:active{ transform:translateY(var(--z-press)); box-shadow:0 0 0 rgba(0,0,0,.45); }
.eg-chip[disabled]{ opacity:.55; cursor:default; }
.eg-chip.has-v{ background:var(--paper); }
.eg-chip__ico{ flex:none; width:30px; height:30px; display:grid; place-items:center; line-height:0; }
.eg-chip__ico .eg-ico{ display:block; }
.eg-chip__name{ flex:1; min-width:0; text-align:start;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.eg-chip__v{
  flex:none; font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  color:var(--ink-soft); direction:ltr;
}
.eg-chip.has-v .eg-chip__v{ color:var(--ink); }
.eg-chip.is-bump{ animation:egBump 260ms var(--e-land); }

/* =====================================================================
   ITEM 19 · THE FREE-TEXT ROW
   The wrapper is a column so the field can sit UNDER the row it names
   rather than inside it — see the note in egBeat3() for why it cannot be
   a child of the button. The row itself is a .eg-chip and is styled by
   every rule above; nothing here restyles it.
   ===================================================================== */
.eg-other{ display:flex; flex-direction:column; gap:6px; }
.eg-other__g{ font-size:19px; line-height:1; opacity:.75; }
.eg-other__in{
  appearance:none; width:100%; box-sizing:border-box;
  border:0; border-radius:12px; padding:11px 14px 12px;
  font-family:var(--font-body); font-size:17px;   /* 17 so iOS does not zoom */
  font-weight:400; color:var(--ink); background:var(--paper);
  box-shadow:0 0 0 2px rgba(19,19,16,.30) inset, 0 2px 0 rgba(0,0,0,.22);
  direction:rtl;
}
.eg-other__in::placeholder{ color:var(--ink-soft); opacity:.75; }
.eg-other__in:focus{ outline:3px solid var(--edge); outline-offset:2px; }

/* =====================================================================
   ITEM 20B · THE VARIANT SWITCHER
   Three chips, one on. It is chrome for choosing what the card says, so
   it is deliberately quieter than the card and than the actions: no
   yellow, no die-cut ring, nothing that competes with the artifact it is
   editing. A disabled tab is a variant with nothing to lead with — see
   egBeat4() — and reads as unavailable rather than as an error.
   ===================================================================== */
.eg-vars{
  display:flex; gap:7px; align-self:center; flex:none; margin-top:2px;
  opacity:0; transition:opacity var(--t-f5-in) var(--e-settle);
}
.eg-vars.is-in{ opacity:1; }
.eg-var{
  appearance:none; border:0; cursor:pointer; border-radius:999px;
  padding:7px 14px 8px; font-family:var(--font-ui); font-size:13px; font-weight:700;
  color:#D8D2C4; background:rgba(255,255,255,.10);
  -webkit-tap-highlight-color:transparent;
  transition:background var(--t-press) linear, color var(--t-press) linear;
}
.eg-var.is-on{ background:var(--paper); color:var(--ink); }
.eg-var[disabled]{ opacity:.4; cursor:default; }

/* ITEM 20A · the share action, in the exit row above the two doors. R-B
   rather than a second P-C: the yellow on this screen is the way out, and
   two primaries would make the player choose between leaving and sharing
   by weight rather than by intent. */
.eg-shareb{ min-height:52px; }
.eg-shareb.is-copied{ color:var(--primary); }

/* ---- ITEM 20B · variant B leads with the topic and states no number --- */
.eg-share__blead{
  margin:0; display:flex; align-items:center; gap:11px;
  font-family:var(--font-display); font-weight:900; font-size:clamp(20px,5.4vw,26px);
  line-height:1.16; color:var(--ink); text-wrap:balance;
}
.eg-share__bico{ flex:none; line-height:0; }
/* ---- variant C · the record at the lead's size ----------------------- */
.eg-share__lead--c{ flex-wrap:wrap; }
.eg-share__of{ font-family:var(--font-display); font-weight:900; font-size:19px; color:var(--ink-soft); }
@keyframes egBump{ 40%{ transform:translateY(2px) scale(.985); } 100%{ transform:none; } }

/* ---- the share card ------------------------------------------------ */
/* it is a CARD, in the game's own card language: kraft ground, white
   die-cut edge, the extrusion every card carries. */
.eg-share{
  background:var(--kraft); color:var(--ink);
  border:var(--card-border) solid var(--edge); border-radius:18px;
  box-shadow:var(--shadow-card);
  padding:18px 16px 16px; text-align:start;
  display:flex; flex-direction:column; gap:8px;
  opacity:0; transform:translateY(10px) scale(.98);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-share.is-in{ opacity:1; transform:none; }
.eg-share__tag{
  margin:0; font-family:var(--font-ui); font-size:var(--fs-micro);
  font-weight:700; letter-spacing:.1em; color:var(--ink-soft);
}
/* THE HEADLINE. It is the biggest thing on the card by a wide margin,
   because §5.3's whole argument is that this number leads. */
.eg-share__lead{ margin:0; display:flex; align-items:baseline; gap:10px;
  justify-content:space-between; }
.eg-share__lead > span{ flex:1; min-width:0; }
.eg-share__n{ color:var(--ink); font-size:clamp(40px,12vw,54px); }
/* DIRECT CHILD ONLY. N() wraps every numeral in <span class="num">, so a
   descendant selector here also caught the number inside the <b> and
   shrank the headline to label size — the one thing on this card that
   must not happen. Scoped to > span, the label is styled and the
   numeral keeps .eg-share__n. */
.eg-share__lead > span{
  font-family:var(--font-display); font-weight:900;
  font-size:var(--fs-body); line-height:1.2; color:var(--ink);
}
.eg-share__topic{
  margin:0; display:flex; align-items:center; gap:8px;
  font-family:var(--font-ui); font-size:var(--fs-meta); color:var(--ink-soft);
}
.eg-share__ico{ flex:none; line-height:0; }
.eg-share__rule{ border:0; border-top:1px solid rgba(19,19,16,.16); margin:2px 0 0; }
/* demoted, and it has to LOOK demoted */
.eg-share__second{
  margin:0; font-family:var(--font-ui); font-size:var(--fs-meta);
  color:var(--ink-soft); font-variant-numeric:tabular-nums;
}

/* ---- actions -------------------------------------------------------- */
.eg-go{
  width:100%; flex:none;
  opacity:0; transform:translateY(6px);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-go.is-in{ opacity:1; transform:none; }
.eg-acts{
  display:flex; flex-direction:column; gap:8px; flex:none;
  opacity:0; transform:translateY(6px);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-acts.is-in{ opacity:1; transform:none; }
.eg-acts .eg-go{ opacity:1; transform:none; }
.eg-clear{
  border:0; background:none; color:var(--ink-body);
  font-family:var(--font-body); font-size:var(--fs-meta); font-weight:400;
  padding:8px; cursor:pointer; text-decoration:underline; text-underline-offset:3px;
}
.map-done{
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:calc(14px + env(safe-area-inset-bottom)); z-index:6;
  border:0; border-radius:var(--r-pill); cursor:pointer;
  background:var(--primary); color:var(--ink);
  box-shadow:0 var(--z-press) 0 var(--primary-base);
  font-family:var(--font-display); font-weight:900; font-size:var(--fs-meta);
  padding:9px 16px;
}

/* ---- the confetti ---------------------------------------------------
   CSS keyframes, not a rAF loop: it is decoration, it must cost nothing,
   and prefers-reduced-motion has to be able to remove it outright.
   THREE NEUTRALS AND THE SYSTEM'S ONE YELLOW — no topic hues. Cycling
   the six live topic colours would draw a rainbow, which lsGlyph()
   rejected on the title for a reason that applies here with more force:
   this is the celebration screen, one of the six topics is מגדר ושוויון,
   and an accidental pride flag on it would be an unintended political
   statement in the loudest possible place. */
.eg-cf{
  position:absolute; top:-24px; width:8px; height:12px; border-radius:2px;
  will-change:transform, opacity;
  animation-name:egFall; animation-timing-function:cubic-bezier(.35,.1,.7,1);
  animation-fill-mode:both;
}
.eg-cf--0{ background:var(--primary); }
.eg-cf--1{ background:var(--paper); }
.eg-cf--2{ background:var(--kraft); }
@keyframes egFall{
  0%   { transform:translate3d(0,0,0) rotate(0deg); opacity:0; }
  12%  { opacity:1; }
  100% { transform:translate3d(var(--cf-drift,0), 104vh, 0) rotate(var(--cf-spin,180deg));
         opacity:0; }
}

/* ---- 360px. The chips are the thing that runs out of room first: the
   topic labels are up to 14 Hebrew characters and the value sits beside
   them, so below 380 the icon gives up its gap before the name gives up
   a character. */
@media (max-width:379px){
  .eg-chip{ padding:8px 10px; gap:8px; font-size:var(--fs-meta); }
  .eg-chip__ico{ width:26px; height:26px; }
  .eg-card{ padding:12px 12px 11px; }
  .eg-share{ padding:15px 13px 14px; }
}

@media (prefers-reduced-motion: reduce){
  .eg-b1, .eg-h2, .eg-card, .eg-share, .eg-go, .eg-acts, .eg-lede, .eg-chips{
    opacity:1; transform:none; transition:none;
  }
  .eg-cf{ display:none; }
  .eg-chip.is-bump{ animation:none; }
  /* ITEM 14 · no sprites at all and no pulse — egCoinFlight() refuses to
     emit and egBalancePulse() refuses to add the class, so this is the
     backstop rather than the mechanism. The numbers tick normally: with
     no flight there is no hold on the row, so its value lands on the tap
     exactly as it did before this item. */
  .eg-left.is-pulse{ animation:none; }
}

/* =====================================================================
   §B · THE CHARACTER (2b) AND THE STICKER SHEET (2a)
   Both live inside stickerModal()'s box, through `extra`. The sticker
   draws the surface, the scrim and the ✕; only what is INSIDE it is
   styled here, and it is the board's own vocabulary — die-cut white edge,
   hard offset shadow, dashed wells, the gender chips at the board's
   12px — re-inked for paper where the board drew it on the dark ground.
   ===================================================================== */
/* the HUD sticker is a button now (index.html). It keeps AS-D's 40px box
   and gets nothing a sticker would not have: no chrome, a press. */
.hud-you{
  border:0; padding:0; background:none; cursor:pointer; border-radius:50%;
  -webkit-tap-highlight-color:transparent;
  transition:transform var(--t-press) linear;
}
.hud-you:active{ transform:scale(.92); }
.hud-you:focus-visible{ outline:3px solid var(--primary); outline-offset:3px; }

/* the profile sticker has no title of its own — the character is the
   title — so the component's empty <h2> is out of the flow */
.stmodal[data-profile] .stmodal__title{ display:none; }
/* the profile sheet is sized to FIT (see .prof-hero), so the 76% ceiling
   the law modal wants would only clip it; it may use the whole area */
.stmodal[data-profile] .stmodal__box{ max-height:100%; }
/* THE CONTENT SCROLLS, THE STICKER DOES NOT — the same rule as
   .stmodal__body, for the same reason: the ✕ has to stay put. 2a at
   360×640 is taller than the box's 76%, and this is what absorbs it. */
/* ITEM 16 · THE INLINE PADDING IS THE PRIMARY'S DIE-CUT EDGE, NOT A GAP.
   .p-c paints its white cut at 3px and its keyline at 4.6px OUTSIDE its
   border box, and this element is a scroll container: overflow-y:auto
   forces overflow-x to compute to auto as well, so it clips horizontally
   whether or not anything ever scrolls sideways. Against 2px of padding
   that cut 2.6px off both rings at both ends — the yellow ran to a flat
   vertical edge with a sliced dark line beside it, which is why the fill
   read as inset inside a border it never reached.
   5px, so the 4.6px ring clears with 0.4 to spare. The button gives back
   the 6px: 266 -> 260 wide, and the ring's outer edge then lands on
   .stmodal__box's own 16px padding edge — the sticker's inset, which is
   the one this control sits in. See the report for why that rather than
   the stage's.
   IT IS NOT A per-button FIX. Everything this scroller holds — 2a's
   tiles, the invitation's chips — gains the same room, and none of them
   was near the edge to begin with. */
.prof{
  align-self:stretch; min-height:0; overflow-y:auto;
  -webkit-overflow-scrolling:touch; overscroll-behavior:contain;
  display:flex; flex-direction:column; align-items:center;
  padding:2px 5px 6px;
}
/* 2b · the hero: the SAME round token the HUD carries, at 156px, on a
   dashed well like 2a's, with the board's die-cut. */
/* FITS A 640px STAGE WITHOUT SCROLLING — iPhone 16 in Safari with its
   bars is about 660, and 360×640 is the floor. Measured settled at
   360×640: the box is 523px tall inside the modal's 608, with no internal
   scroll (clientHeight equals scrollHeight). A scrolling sticker
   card was the wrong fix (and the stage blocks touch scrolling anyway):
   the hero came down from 156 to 116, which still reads, and every gap
   gave a little. */
.prof-hero{
  position:relative; width:132px; height:132px; display:grid; place-items:center;
  /* as a button: nothing of a button's own. The whole 132px is the target. */
  border:0; padding:0; margin:0; background:none; cursor:pointer; color:var(--ink);
  -webkit-tap-highlight-color:transparent; font:inherit;
}
button.prof-hero:active .prof-st{ transform:scale(.96); }
button.prof-hero:focus-visible{ outline:3px solid var(--primary); outline-offset:6px; border-radius:50%; }
/* the corner chip: 44px, the paper chip the HUD's ✕ is made of (IB-B),
   carrying the sheet glyph. Bottom-left physical, off the disc's edge,
   so it reads as pinned on the token rather than part of the drawing. */
.prof-hero__chip{
  position:absolute; left:-2px; bottom:-2px; z-index:3;
  width:44px; height:44px; display:grid; place-items:center;
  border-radius:var(--r-chip); background:var(--paper); color:var(--ink);
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
}
.prof-hero__chip svg{ display:block; }
.prof-well{ position:absolute; inset:0; border-radius:50%; border:1.5px dashed rgba(19,19,16,.32); }
.prof-st{ position:relative; width:116px; height:116px; transition:transform var(--t-press) linear; }
/* ON THE SVG, NOT THE SPAN: the span can then carry a mark (.avp-check)
   that the dilate does not fatten */
.avs-cut > svg{ filter:url(#dcw) drop-shadow(0 3px 0 rgba(0,0,0,.28)); }
.prof-lbl{ margin:12px 0 0; font-size:var(--fs-meta); font-weight:700; color:var(--ink-soft); }
/* ITEM 16 · A TWO-UP ROW, and it is .inv-row's idiom rather than a new
   one — the invitation already lays three of these chips out this way and
   its three measure 84px each. Left to itself .gchip is inline-flex and
   sized by its own text, so לשון נקבה came out 93 and לשון זכר 81: a 12px
   step between two options that are the same kind of answer, which reads
   as one of them being the larger choice.
   THE ROW STRETCHES TOO. It was shrink-to-fit and centred, 183px in a
   260px column, so the pair floated free of the two buttons under it;
   full width puts all four controls on the same two edges.
   NEITHER IS SELECTED AND NOTHING HERE SELECTS ONE. `.on` is written by
   paint() from PROFILE.gender only, and with no answer yet that is null,
   so both chips render in the same resting state — the third state IS the
   default. Nothing below styles a first child, a checked state or a
   fallback. */
.prof-gender{ display:flex; gap:9px; margin-top:7px; width:100%; }
.prof-gender > .gchip{ flex:1; justify-content:center; padding-inline:6px; white-space:nowrap; }
/* the board's .gchip, as a button, on paper: white so it reads as a
   sticker on the cream, the board's hard 3px shadow, and the board's
   double ring when it is on. Never a colour — a voice is not a verdict. */
.gchip{
  appearance:none; border:0; cursor:pointer; display:inline-flex; align-items:center;
  background:#fff; border-radius:12px; padding:9px 15px;
  font-family:var(--font-ui); font-size:13.5px; font-weight:700; color:var(--ink);
  box-shadow:0 3px 0 rgba(0,0,0,.3);
  -webkit-tap-highlight-color:transparent;
  transition:transform var(--t-press) linear, box-shadow var(--t-press) linear;
}
.gchip.on{ box-shadow:0 0 0 3px #FBF7EE, 0 0 0 5.5px #131310, 0 4px 0 rgba(0,0,0,.32); }
.gchip:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.3); }
.gchip.on:active{ box-shadow:0 0 0 3px #FBF7EE, 0 0 0 5.5px #131310, 0 2px 0 rgba(0,0,0,.32); }
/* the two doors, at the exit confirm's shared height so P-C and R-B
   match — see .exitsheet__row for why the type sizes make them unequal */
.prof-actions{ margin-top:14px; width:100%; display:flex; flex-direction:column; gap:8px; }
.prof-actions > *{
  min-height:52px; display:flex; align-items:center; justify-content:center;
  padding-block:0; gap:8px;
}
/* the one primary. Everything on this sheet applied the moment it was
   tapped; this closes, and its copy is the player's model, not the code's. */
.prof-save{ font-size:19px; }
/* ITEM 16 · `.prof-tweak{ min-height:46px }` IS GONE. .prof-actions > *
   sets 52px for exactly the reason its own comment gives — "so P-C and
   R-B match" — and this rule, later in the file at the same specificity,
   quietly undid it: 52 against 46, a 6px step between two controls in one
   stacked pair. The type sizes still differ (19 and 17) and should; the
   BOX is what has to agree. */
/* R-B re-inked for paper, exactly as .exitsheet .r-b is */
.prof-tweak{ font-size:17px; color:var(--ink); box-shadow:inset 0 0 0 2.5px rgba(19,19,16,.38); }

/* 2a · the board's sheet, inside the sticker. Title and sub at the
   modal's own sizes, in ink, because they sit on paper here rather than
   on the dark ground the board drew them on. */
.peel-title{
  margin:0; padding-inline:30px; font-family:var(--font-display); font-weight:900;
  font-size:20px; line-height:1.22; text-align:center;
}
.peel-sub{ margin:4px 0 0; font-size:var(--fs-meta); font-weight:700; color:var(--ink-soft); text-align:center; }
.sheet{
  margin-top:14px; width:100%; padding:14px 6px 12px;
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:14px 6px; justify-items:center;
  background:
    linear-gradient(102deg,rgba(255,255,255,.5) 0 16%,rgba(255,255,255,0) 40%,
                    rgba(255,255,255,0) 62%,rgba(255,255,255,.3) 88%),#E9ECEA;
  border:5px solid #fff; box-shadow:0 0 0 2px var(--keyline), 0 6px 0 rgba(0,0,0,.4);
}
/* each sticker is a button with nothing of a button's own */
/* no names under the stickers (see renderSheet) — the stickers take the
   room: 84px on a 390 sheet, the column's width less the well on a 360 */
.avp{
  position:relative; width:100%; max-width:100px;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  border:0; background:none; padding:0; cursor:pointer;
  font:inherit; color:var(--ink); -webkit-tap-highlight-color:transparent;
}
.avp-well{
  position:absolute; top:-5px; left:50%; translate:-50% 0;
  width:100%; max-width:94px; aspect-ratio:1;
  border:1.5px dashed #9AA4A0; border-radius:14px;
}
.avp-st{
  position:relative; z-index:2; display:block; line-height:0;
  width:calc(100% - 10px); max-width:84px; aspect-ratio:1;
  transition:translate 200ms var(--e-settle);
}
.avp-st svg{ display:block; width:100%; height:100%; }
/* the current one, mid-peel: lifted off its well, its shadow gone soft
   and directional, the corner it is peeling from showing its back */
.avp-peel .avp-st{ translate:6px -9px; }
.avp-peel .avp-st > svg{ filter:url(#dcw) drop-shadow(-6px 10px 9px rgba(0,0,0,.38)); }
/* THE MAP'S MARK ON THE CHOSEN STICKER. .node-check is 24px inset 6px on
   a 76px face with a 14px glyph and a 2px casing; here every one of
   those is the same fraction of the sticker, in container units, so it
   reads identically on the 78px sticker at 390 and the 62px one at 360.
   Hidden until the sticker is the chosen one; the lift and the mark
   arrive together. */
.avp-st{ container-type:inline-size; }
.avp-check{
  display:none; right:7.9cqw; bottom:7.9cqw; width:31.6cqw; height:31.6cqw;
  font-size:18.4cqw; border-width:max(1.5px, 2.6cqw); box-shadow:0 2.6cqw 0 0 #161413;
}
.avp-peel .avp-check{ display:grid; }
.avp-lift{
  display:none; position:absolute; z-index:1; top:0; left:8px; width:30px; height:30px;
  background:linear-gradient(135deg,#fff 0 48%,#D6D9D7 52%,#C2C6C4 100%);
  border-radius:0 0 24px 0; box-shadow:-2px 3px 5px rgba(0,0,0,.22);
}
.avp-peel .avp-lift{ display:block; }
/* the mark's arrival, and its re-arrival on the already-chosen sticker:
   a short pop, so a tap always shows something landing */
.avp-check.is-pop{ animation:avp-pop 180ms var(--e-land) both; }
@keyframes avp-pop{ from{ transform:scale(.4); opacity:0 } 60%{ transform:scale(1.12); opacity:1 } to{ transform:none } }

/* =====================================================================
   §D · 2c THE BUILDER (see renderBuilder). The board's construction on
   the profile sticker: the preview on its dashed well up top, then the
   MK card's own kraft — the 27px grid, the die-cut edge, the hard
   shadow — holding one axis at a time as white tiles built the way the
   vote chips are (14px radius, hard 3px shadow, the double ring when
   on), and the map's mark on the chosen one.
   ===================================================================== */
.bld-title{ padding-inline:34px; }
.bld-hero{ margin-top:4px; }
/* THE HERO NEVER SCROLLS OFF. While the builder is showing, .prof stops
   being the scroller: the hero is a fixed-height child, the sheet takes
   the rest and shrinks, and inside it only the tile grid may scroll.
   §0.4-3 was exactly this bug in the old build — the preview scrolled
   away with the options — and this makes it structural: the grid is
   the one overflow:auto in the chain, so a sheet that cannot fit
   scrolls its tiles under a preview that stays put. On a 640 stage
   nothing scrolls at all (measured below the axes, see the report). */
.prof--bld{ overflow:hidden; }
.prof--bld .bld-hero{ flex:none; }
.bsheet{
  margin-top:10px; width:100%; min-height:0; flex:1 1 auto;
  display:flex; flex-direction:column; gap:8px;
  padding:9px 8px 9px;
  background:var(--card-surface);
  border:5px solid var(--edge); border-radius:var(--r-chip);
  box-shadow:0 0 0 2px var(--keyline), 0 6px 0 rgba(0,0,0,.4);
}
.bhead{ display:flex; align-items:center; gap:10px; }
/* the board's progress: the count in H-A's meta, the bar in the one
   yellow — it is progress, not a control, so P-C's colour is right */
.bprog{ flex:1; min-width:0; margin:0; display:flex; flex-direction:column; gap:5px; }
.bprog__n{ font-size:var(--fs-meta); font-weight:700; color:var(--ink-soft); line-height:1.1; }
.bprog__bar{
  display:block; height:8px; border-radius:99px; overflow:hidden;
  background:rgba(19,19,16,.12); box-shadow:inset 0 0 0 1.5px rgba(19,19,16,.28);
}
.bprog__bar i{ display:block; height:100%; border-radius:99px; background:var(--primary);
  transition:width 200ms var(--e-settle); }
/* the shuffle: an IB-B chip, 44px, so it is a control and not a primary */
.bshuf{ flex:none; width:44px; height:44px; -webkit-tap-highlight-color:transparent; }
.bshuf:active{ transform:translateY(2px); box-shadow:0 0 0 2px var(--keyline), 0 1px 0 rgba(0,0,0,.34); }
.baxis{ margin:0; font-family:var(--font-display); font-weight:900; font-size:17px; line-height:1.2; }
/* the tiles. 8px of padding all round so the ring (5.5px) and the mark
   (6px past the corner) stay inside the scroller's clip. */
.bgrid{
  min-height:0; flex:1 1 auto; overflow-y:auto;
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px 8px;
  padding:8px; margin:-8px;
  align-content:start;
  /* TWO ROWS' WORTH ON EVERY AXIS. Six options fill two rows; three
     (eyes) fill one, and a shorter sheet would re-centre the whole
     sticker and drop the hero 44px between axes — measured. The grid
     reserves the second row so the box, and the hero, hold still. */
  min-height:186px;
}
.bgrid--voice{ grid-template-columns:repeat(2, minmax(0,1fr)); }
.bopt{
  position:relative; appearance:none; border:0; cursor:pointer;
  padding:6px 4px 5px; min-width:0;
  display:flex; flex-direction:column; align-items:center; gap:3px;
  background:#fff; border-radius:var(--r-chip); color:var(--ink); font:inherit;
  box-shadow:0 3px 0 rgba(0,0,0,.3); -webkit-tap-highlight-color:transparent;
  transition:transform var(--t-press) linear, box-shadow var(--t-press) linear;
}
.bopt.on{ box-shadow:0 0 0 3px #FBF7EE, 0 0 0 5.5px #131310, 0 4px 0 rgba(0,0,0,.32); }
.bopt:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.3); }
.bopt.on:active{ box-shadow:0 0 0 3px #FBF7EE, 0 0 0 5.5px #131310, 0 2px 0 rgba(0,0,0,.32); }
.bopt-st{ display:block; width:50px; height:50px; line-height:0; }
.bopt-st svg{ display:block; width:100%; height:100%;
  filter:url(#dcw-sm) drop-shadow(0 2px 0 rgba(0,0,0,.28)); }
.bopt-lbl{
  max-width:100%; font-family:var(--font-ui); font-size:12px; font-weight:700; line-height:1.15;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* the map's mark, at its own size, on the tile's corner */
.bopt-check{ display:none; right:-6px; bottom:-6px; width:22px; height:22px; font-size:13px; }
.bopt.on .bopt-check{ display:grid; }
.bopt-check.is-pop{ animation:avp-pop 180ms var(--e-land) both; }
/* the voice step's third answer, a chip across both columns */
.bopt-any{ grid-column:1 / -1; justify-content:center; min-height:44px; }
/* the chevrons: two paper chips, the forward one yellow on the last step
   because there it is the way back to the character, not a step */
.bfoot{ display:flex; gap:8px; }
.bnav{
  flex:1; min-height:44px; appearance:none; border:0; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center; gap:3px;
  background:var(--paper); color:var(--ink); border-radius:var(--r-chip);
  font-family:var(--font-display); font-weight:900; font-size:16px;
  box-shadow:0 0 0 2px var(--keyline), 0 3px 0 rgba(0,0,0,.34);
  -webkit-tap-highlight-color:transparent;
  transition:transform var(--t-press) linear, box-shadow var(--t-press) linear;
}
.bnav:active{ transform:translateY(2px); box-shadow:0 0 0 2px var(--keyline), 0 1px 0 rgba(0,0,0,.34); }
.bnav--last{ background:var(--primary); }
.bnav__chev{ display:block; flex:none; }

/* =====================================================================
   §C · THE INVITATION, AND THE PLAYER ON THE END-GAME
   The invitation is the profile sticker with less in it: one line, three
   equal chips, the avatar line. Same .gchip, same paper, same ✕.
   ===================================================================== */
.prof--invite{ padding-top:0; }
.inv-q{
  margin:4px 0 0; padding-inline:30px; text-align:center;
  font-family:var(--font-display); font-weight:900; font-size:20px; line-height:1.22;
}
/* three of the same size. "I'd rather not say" is an answer, so it is
   not smaller, not lighter and not last-but-quieter. */
.inv-row{ display:flex; gap:7px; margin-top:14px; width:100%; }
.inv-row > .gchip{ flex:1; justify-content:center; padding-inline:6px; white-space:nowrap; }
.inv-av{
  margin:18px 0 2px; display:flex; align-items:center; gap:8px;
  font-size:var(--fs-meta); font-weight:700; color:var(--ink-soft);
}
.inv-av__st{ width:30px; height:30px; }
.inv-link{
  border:0; background:none; padding:0; cursor:pointer;
  font:inherit; color:var(--ink); text-decoration:underline; text-underline-offset:3px;
}

/* the record's head: the token, then the line */
.eg-av{
  width:56px; height:56px; margin:0 auto 4px; display:block; filter:url(#dcw-sm);
  opacity:0; transform:translateY(6px);
  transition:opacity var(--t-f5-in) var(--e-settle), transform var(--t-f5-in) var(--e-settle);
}
.eg-av.is-in{ opacity:1; transform:none; }
/* the card's head: the token beside the tag, the optional name under it */
.eg-share__head{ display:flex; align-items:center; gap:10px; }
.eg-share__av{ width:34px; height:34px; flex:none; }
.eg-share__name{
  margin:0; width:100%; font-family:var(--font-display); font-weight:900;
  font-size:var(--fs-body); color:var(--ink);
}

/* ---- §N · the name field in 2b ------------------------------------- */
.prof-name{ width:100%; display:flex; flex-direction:column; align-items:center; gap:5px; margin-top:12px; }
.prof-lbl--name{ margin:0; text-align:center; }
/* the board's .prof-field, on paper: white so it reads as a field on the
   cream, the chips' keyline, a hard shadow. 17px keeps iOS from zooming
   the page to it. */
.prof-field{
  appearance:none; width:100%; margin:0; border:0; border-radius:14px;
  padding:12px 16px; background:#fff; color:var(--ink);
  font-family:var(--font-ui); font-size:17px; font-weight:700; line-height:1.2;
  box-shadow:inset 0 0 0 2px rgba(19,19,16,.28), 0 3px 0 rgba(0,0,0,.2);
  text-align:start;
}
.prof-field::placeholder{ color:#6B6759; font-weight:700; }
.prof-field:focus{ outline:0; box-shadow:inset 0 0 0 2.5px var(--ink), 0 3px 0 rgba(0,0,0,.2); }
/* THE CEILING IS IN PX WHILE THE KEYBOARD IS OPEN. The box's normal
   max-height is 76% of .stmodal, and once `bottom` has shortened .stmodal
   a percentage no longer tracks it (a grid item's percentage resolves
   against the auto row, not the container — measured, not assumed). So
   kbSync() also writes --kb-vis, the visible height in px, and the box
   takes that less the modal's padding: fully above the keyboard, its
   scrolling body absorbing the rest. */
/* three classes deep on purpose: it has to outrank the profile sheet's own
   `max-height:100%` (.stmodal[data-profile] .stmodal__box), which is what
   lets that sheet fill the stage when no keyboard is up */
.kb-open .stmodal .stmodal__box{ max-height:calc(var(--kb-vis, 100vh) - 2 * var(--sp-4)); }
