/* play.css -- extracted verbatim from index.html's game CSS + play-menu CSS blocks.
   Source ranges (index.html, commit 6567adc): 310-320, 321-402, 406-409, 414-424,
   425-429, 574-591, 920-935, 974-1027, 1051-1061, 1062-1283, 1349-1399, 1457-2312,
   plus the four live shared control selectors rewritten from 1284-1456. */
/* ---- FIX ROUND 2 (Task 2): SHARED with index.html, not moved -- index.html keeps every
   rule below untouched, the home page still needs them for its own head. Placed first, ahead
   of every component rule above and below, so the cascade order matches index.html's: design
   tokens and base rules before components. Without these, play.html's 91 font-family:var(--sans)
   declarations resolved to nothing (serif fallback -- the "random different fonts" Jayden saw)
   and the companion eye rig had no CSS at all (no .eye/.iris/.pupil/.glint geometry -- the
   "eyes don't work"). Verified against actual play-engine.js usage, not just the cited source
   range: index.html:138-153 (the range given for the eye rig) also contains .face, .introwake
   and .glasses, three classes grep confirms play-engine.js's spawnCompanion never sets -- those
   style the BIG head's own overlays on index.html and are correctly left out here. Only the
   classes spawnCompanion actually creates (.eye, .iris, .pupil, .glint) are copied, from
   index.html:140-144 and 153-156. Design tokens + media override: index.html:46,48. Body base
   rule: index.html:58 (overflow-x:clip kept -- harmless alongside play.html's own
   html,body{overflow:hidden}, which already wins the cascade for both axes regardless). ---- */
/* ---- THE WEBFONTS. This file carried 91 font-family:var(--sans) declarations and three
   font-family:'Archivo' declarations, and NOT ONE @font-face to load either family -- neither
   here nor in play.html. --sans is "Instrument Sans",-apple-system,system-ui,sans-serif, so
   the named family never arrived and every word on the page silently fell through to
   -apple-system: SF Pro on a Mac, close enough to Instrument Sans to have been reported as
   fixed twice when it was not. .bcNum (the split-flap scoreboard numerals) was rendering in
   SF Pro too, and the Archivo/Instrument register split -- display face on numerals and
   moments only, everything else Instrument 400/600 -- is a settled part of the broadcast
   identity, not decoration.
   Copied verbatim from index.html:608-609. Instrument Sans Var is deliberately NOT copied:
   it exists on index.html for the two-line wordmark's font-stretch trick, and play.html has
   no wordmark. Preloads for the two Instrument faces live in play.html's own <head>, where a
   preload has to be; Archivo is not preloaded, matching index.html, because it is 90 KB and
   nothing shows a numeral until a match has started. ---- */
@font-face{font-family:"Instrument Sans";font-style:normal;font-weight:400;font-display:swap;src:url(fonts/instrument-sans-latin-400-normal.woff2) format("woff2")}
@font-face{font-family:"Instrument Sans";font-style:normal;font-weight:600;font-display:swap;src:url(fonts/instrument-sans-latin-600-normal.woff2) format("woff2")}
@font-face{font-family:'Archivo';src:url('fonts/archivo-variable.woff2') format('woff2');
  font-weight:100 900;font-stretch:62% 125%;font-display:swap}
/* ---- THE TOKEN BLOCK IS GONE FROM THIS FILE. It lived here as a hand-copied subset of
   index.html's :root, and a second flagged "top-up" block sat at the foot of the file for
   the tokens the site header needed. play.html now links tokens.css AHEAD of this file
   (play.html:13), so the shared file is the single source and this file only declares the
   handful of values this page legitimately binds DIFFERENTLY. Everything below was verified
   to leave every computed :root value unchanged at 1280px and 390px -- see
   .superpowers/sdd/2026-08-02-play-page/play-tokens-report.md §1.

   --accent: tokens.css binds --accent to the site's INTERACTIVE signal (a blue; it is the
   link/back-link hover colour on the case studies). This page has always bound it to the
   live-match green instead, and two rules consume it that way -- .tM.tLive's border and
   .tResRow.tResLive's inset bar, both of which mean "this match is live". Letting the
   shared value through would turn the live-match marker blue. header.css never touches
   --accent (it carries its own --nav-accent) precisely so this override is safe.
   --accent-live, --fs-hero, --fs-heroline: not in tokens.css at all; kept verbatim so the
   computed-value diff stays empty. --lh-prose: this page carries 1.6, tokens.css 1.5. ---- */
/* ---- CORRECTION, 2026-08-03: --accent is NOT rebound here any more.
   It meant the interactive blue on seven pages and the live-match green on this
   one and the Lab, which is the most dangerous shape a token bug takes: every
   page looks right in isolation and the system is silently incoherent. Both of
   this file's two var(--accent) uses were the LIVE marker -- .tM.tLive's border
   and .tResRow.tResLive's inset bar -- so both now name --accent-live, which is
   what they always meant and which this file already defines and uses three
   times. Nothing here wants the blue rebound; --accent now resolves to
   tokens.css's blue on this page as on every other. ---- */
:root{--accent-live:#17A45A;
 --fs-hero:clamp(38px,4.7vw,60px);--fs-heroline:var(--fs-pagehead-sm);--lh-prose:1.5;}

/* ---- THE PAGE GUTTER. Jayden: "nothing is really in the correct margins ... for that
   whole screen". The Play bar was the case in point: it was pinned with a hard-coded
   right:16px/top:16px in play.html's markup, while the site header sits on
   .jbStick's padding of var(--sp-8) var(--sp-16-24) var(--sp-12) -- a 24px gutter at
   1280px. So the page's only two pieces of chrome hung off two different margins,
   8px apart, for no reason. --play-gutter IS the header's gutter, tracked at every
   width, so the Play control and the bar share one edge.

   --play-top is the other half, and it fixes a real bug rather than a misalignment.
   MEASURED at 390px before this change: .jbNav ran 22->368 and the Play button ran
   289->374, so they overlapped by 79px, and since header.css sits at z-index 100 and
   .moodbar at 60, the bar painted straight over it -- the word "Play" was invisible
   and most of its 44px target was unclickable. On the page whose entire purpose is
   starting a game, on a phone. The collision is geometric, not a 640px-phone
   special case: the bar is centred and ~481px wide, the button is ~85px plus its
   gutter, so they meet as soon as the viewport drops under ~699px. Below 760px --
   an existing site breakpoint, with room to spare over that 699 -- Play stops trying
   to share the header's row and takes its own, right-aligned to the same gutter and
   clearing --nav-h plus a step. Above it, nothing moves vertically: top:var(--sp-16)
   is the 16px the bar already had, which bottom-aligns the 44px button with the 52px
   nav (8+52 = 16+44 = 60). That alignment was right and is kept. ---- */
:root{--play-gutter:var(--sp-16-24);--play-top:var(--sp-16)}
@media(max-width:760px){:root{--play-top:calc(var(--nav-h) + var(--sp-20))}}
/* 24, not 12. THE PHONE HALF OF THE MARGIN FIX (the other half is .pCards' max-width in
   play.html). Above ~1280 the mode row's inset is set by centring and this value never bites;
   below it the gutter IS the margin, and at 390 this was putting the four doors 12px from the
   glass while index.html's nav sits at 16 and gradientlab.html's main -- the page that settled
   the site standard -- sits at 24. 12 was the odd one out on the one width where the margin is
   most visible, because there is nothing else on the row to compare it against.
   This also moves .pTeam, which takes the same token and was 12 for the same wrong reason. */
@media(max-width:640px){:root{--play-gutter:var(--sp-24)}}

body{background:var(--c50);color:var(--c950);font-family:var(--sans);-webkit-font-smoothing:antialiased;overflow-x:clip}
.eye{position:absolute;border-radius:var(--r-full);overflow:hidden;z-index:3;pointer-events:none;transition:transform .08s steps(2);filter:url(#inkEye)}
.eye.eclosed{transform:scaleY(0.08);visibility:hidden}
.eye.eclosed .iris,.eye.eclosed .glint{opacity:0}
/* belt and braces: a closed eye has no iris, whatever the lid is doing that frame */
.eye[style*="display: none"] .iris,.eye[style*="display:none"] .iris{opacity:0}   /* hidden, not squashed: a bare scaleY leaves the iris as a hard dark slit across the face, which is the one thing a blink must never look like. visibility (not display) so nothing reflows. */
.iris{position:absolute;border-radius:46%;background:repeating-conic-gradient(from 4deg,rgba(255,255,255,.045) 0deg 2deg,rgba(0,0,0,.24) 2deg 4.3deg),radial-gradient(circle at 50% 50%,#050505 0%,#080808 25%,#191919 39%,#121212 57%,#181818 72%,#0a0a0a 88%,#030303 100%),radial-gradient(circle at 38% 32%,#1e1e1e 0%,#0d0d0d 52%,#050505 100%);background-blend-mode:multiply,normal,normal}.iris::before{content:"";position:absolute;inset:0;border-radius:46%;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");background-size:20px 20px;-webkit-mask:radial-gradient(circle,#000 58%,transparent 90%);mask:radial-gradient(circle,#000 58%,transparent 90%);mix-blend-mode:soft-light;opacity:.45;pointer-events:none}.iris::after{content:"";position:absolute;inset:0;border-radius:46%;background:radial-gradient(circle at 68% 74%,rgba(244,244,244,.38) 0%,rgba(244,244,244,0) 14%);filter:blur(.4px);pointer-events:none}
.pupil{position:absolute;left:50%;top:50%;width:55%;height:55%;border-radius:var(--r-full);background:radial-gradient(circle at 50% 50%,#000 0%,#000 58%,#060606 80%,rgba(6,6,6,0) 100%);transform:translate(-50%,-50%) scale(1);transform-origin:50% 50%;pointer-events:none;will-change:transform}
.glint{position:absolute;left:32%;top:26%;width:20%;height:20%;border-radius:var(--r-full);z-index:4;pointer-events:none;background:radial-gradient(circle at 50% 50%,rgba(255,255,255,.98) 0%,rgba(252,252,252,.72) 24%,rgba(250,250,250,0) 60%)}
.glint::after{content:"";position:absolute;left:128%;top:165%;width:46%;height:46%;border-radius:var(--r-full);background:radial-gradient(circle,rgba(250,250,250,.55) 0%,rgba(250,250,250,0) 68%)}

/* ---- THE BIG HEAD'S OWN CHROME. SHARED with index.html, copied verbatim, index.html keeps
   every rule below untouched. The comment at the top of this file recorded that .face, .glasses
   and .introwake were "correctly left out" because spawnCompanion never sets them -- true then,
   wrong now. play.html loads hero-engine.js and carries a real (hidden) #stage, and mini-Jayden
   renders by CLONING that stage's whole subtree, so every class INSIDE #stage has to be styled
   here or the clone renders wrong: an unstyled .face is a natural-size image instead of an
   inset:0 object-fit:contain one, and an unstyled .tongue/.glasses/.mouth loses its opacity:0
   and paints a tongue and 3D glasses across his face.
   The completeness rule that produced this block: style (a) everything the engine appends to
   #stage, because that is exactly what gets cloned, and (b) everything it appends to
   #stageMorph, because those default to in-flow and would otherwise stretch the host box.
   (a) at boot = .eye/.iris/.pupil/.glint (above), .face, .mouth + markup's .tongue/.glasses.
   (b) at boot = .speech, .dlogo, .dshadow, plus markup's .queueDots and .floorshadow.
   DELIBERATELY OMITTED, with the reason: .introwake (the wake-up cinematic is index.html's page
   intro and is gated off by HEADONLY), and .crumb/.crumbshadow/.stachecrumb/.popbucket/.kernel/
   .popcrumb/.blush/.heartEye (created only by the eat / party / love / rain / movie easter eggs,
   which on play.html are unreachable -- they fire from dragging a headline word there is no
   headline for, or from the Play menu block that HEADONLY turns off).
   Sources, index.html: 134, 137, 138, 139, 145-147, 162, 163, 164, 166, 168, 308. ---- */
#stageMorph{position:relative;width:100%;transform-origin:center center}

/* ══ THE HEAD LEANS IN WHEN IT WILL TAKE WHAT YOU ARE HOLDING ═══════════════════
   Jayden: "when you drag it to the face, the face comes forward a little bit to show
   that you can drag on it."

   IT IS A DROP TARGET, SO IT IS BUILT LIKE ONE. .catchReady is the site's existing
   "a valid payload is over me" signal -- hero-engine.js's faceTrackTo() raises it for a
   dragged cookie and play-games.js's word rig raises it for a dragged mood word -- and
   index.html already rewards it at line 149 with exactly this scale. Copying the value
   rather than picking a new one means the head answers a drag identically on both pages;
   a drop target that leans by a different amount depending on which page you are on is
   two affordances pretending to be one.

   #stageMorph, NOT #stage, AND THAT IS THE WHOLE REASON IT COMPOSES. hero-engine.js
   writes #stage's transform on every frame of the idle float -- translate, rotate and
   squash -- so anything this stylesheet put there would be overwritten within 16ms.
   #stageMorph is its parent and nothing writes it, so the lean multiplies into the float
   instead of racing it: the head keeps breathing and drifting exactly as it did, and the
   whole portrait -- head, floor shadow, speech bubble and the drifting logo marks, which
   are #stageMorph's other children -- comes forward together as one object. The crowd of
   companions is NOT in this subtree, so they stay put; it is the portrait that leans,
   not the scene.

   IT ONLY EVER FIRES DURING A DRAG. Both owners of .catchReady set it inside a pointer
   gesture and clear it on release, so a bare cursor crossing the head does nothing. On
   this page there is no competing gesture to confuse it with: hero-head-transform.js --
   the selection frame that makes the portrait draggable, scalable and rotatable -- is
   loaded by index.html only, so on Play the head is never itself picked up.

   THE RETURN IS SLOWER THAN THE ARRIVAL, and asymmetric on purpose. Arming is a response
   and should feel instant (--dur-state); disarming is the state letting go and gets the
   rung built for that (--dur-state-out), which is the same 160/240 pair tokens.css §6
   defines for hover in and hover out. index.html transitions both directions at 160 and
   therefore snaps back; the patch to bring it in line is in the audit doc.
   AND A DROP THAT WAS TAKEN RELAXES DIFFERENTLY FROM ONE THAT WAS ABANDONED. On a
   successful drop the mood performance is about to start, so the lean hands over on the
   settle spring instead of retracting -- the head is not returning to rest, it is going
   somewhere. .pMoodTaken is set for exactly that release and dropped again by the rig. */
#stageMorph{transition:transform var(--dur-state-out) var(--ease-out)}
body.catchReady #stageMorph{transform:scale(1.055);transition-duration:var(--dur-state)}
body.pMoodTaken #stageMorph{transition:transform var(--sp-settle-dur) var(--sp-settle)}

/* REDUCED MOTION KEEPS THE DROP TARGET, AND DRAWS IT INSTEAD OF PERFORMING IT.
   The lean is the only thing telling you the head accepts the word, so switching it off
   and stopping there would leave the affordance with no signal at all -- which is the
   failure Apple's reduced-motion criteria name directly (don't remove it, substitute
   something that does not move). The substitute is the drop zone itself, drawn: a hairline
   outline of the shape play-games.js's onHead() actually tests, cross-faded in while the
   head is armed. It is more informative than the lean, not less -- the lean says "I will
   take this", the ring says "I will take this, from anywhere inside here" -- and it is
   built from opacity alone, which is the substitution Apple asks for.

   THE SHAPE IS THE REAL ONE AND THESE NUMBERS ARE NOT DECORATIVE. onHead() tests an ellipse
   inscribed in #face's data-head-bounds ("0.1933 0.0616 0.8484 0.9234") with a 1.06 pad, so:
     centre   x (.1933+.8484)/2 = 52.09%   y (.0616+.9234)/2 = 49.25%
     size     w (.8484-.1933)*1.06 = 69.44%   h (.9234-.0616)*1.06 = 91.35%
     offset   left 52.09-69.44/2 = 17.37%    top 49.25-91.35/2 = 3.58%
   It is hung on .stage rather than on #stageMorph because .stage is the box onHead()
   measures -- #stageMorph also contains the floor shadow and is therefore taller, which
   would have drawn a ring that lied by the height of the shadow.
   IF THE HIT TEST MOVES, THIS MUST MOVE WITH IT. There is no way to derive one from the
   other in CSS, so the coupling is this comment. */
@media(prefers-reduced-motion:reduce){
 #stageMorph,body.pMoodTaken #stageMorph{transition:none}
 body.catchReady #stageMorph{transform:none}
 .stage::after{content:"";position:absolute;left:17.37%;top:3.58%;width:69.44%;height:91.35%;
  border-radius:var(--r-full);border:1px solid var(--c400);
  opacity:0;transition:opacity var(--dur-state) linear;pointer-events:none;z-index:6}
 body.catchReady .stage::after{opacity:1}
}
.stage{position:relative;width:100%;aspect-ratio:1;will-change:transform;z-index:5}
.face{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none;cursor:pointer}
.stage img,.face{-webkit-user-drag:none;-khtml-user-drag:none;user-select:none}
.mouth{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;opacity:0;pointer-events:none;z-index:2;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}
.tongue{position:absolute;left:41%;top:56%;width:12%;height:auto;transform-origin:50% 100%;transform:scaleY(0);opacity:0;z-index:4;pointer-events:none;filter:url(#inkSm) drop-shadow(0 1px 1px rgba(0,0,0,.3));will-change:transform,opacity}
.glasses{position:absolute;inset:0;width:100%;height:100%;z-index:7;pointer-events:none;opacity:0;will-change:transform,opacity;filter:url(#inkSm) drop-shadow(0 3px 3px rgba(0,0,0,.45))}
.gl-red{fill:rgba(200,45,45,.5)}.gl-cyan{fill:rgba(35,150,165,.46)}.gl-plast{opacity:.5;pointer-events:none}
.floorshadow{position:absolute;left:50%;bottom:3%;width:60%;height:12%;transform:translateX(-50%);background:radial-gradient(ellipse 50% 50% at 50% 50%,rgba(8,8,8,.42),rgba(8,8,8,.28) 30%,rgba(8,8,8,.10) 58%,transparent 78%);filter:blur(7px);z-index:-1;pointer-events:none;opacity:.5}
.speech{position:absolute;inset:0;z-index:9;pointer-events:none;font-family:var(--sans);font-weight:600}
.dlogo{border-radius:var(--r-2xs);position:absolute;width:21%;height:21%;opacity:0;backface-visibility:hidden;will-change:transform,left,top,opacity;filter:drop-shadow(0 3px 8px rgba(0,0,0,.22))}
.dshadow{position:absolute;border-radius:var(--r-full);background:radial-gradient(ellipse 50% 50% at 50% 50%,rgba(8,8,8,.5),rgba(8,8,8,.3) 40%,rgba(8,8,8,.12) 62%,transparent 76%);transform:translate(-50%,-50%);z-index:-1;pointer-events:none;opacity:0;will-change:transform,left,top,width,height,opacity}
.queueDots{position:absolute;left:50%;top:-7%;transform:translateX(-50%);display:flex;gap:.42rem;z-index:8;opacity:0;pointer-events:none;transition:opacity var(--ease-out-dur) var(--ease-out)}

/* The Home mood performances are shared JavaScript, so Play carries the same presentation
   classes for every node that hero-engine.js and party.js create. This is not a second
   animation implementation: the engine remains the sole state/timing owner. */
.cookieDot{display:inline-block;width:.62em;height:.62em;vertical-align:baseline;position:relative;overflow:visible;color:transparent}
.cookieDot::before{content:"";position:absolute;inset:0;background:center/contain no-repeat;background-image:url(images/cookie.webp);filter:url(#inkSm);animation:cookieBite 1.8s steps(1,end) infinite}
@keyframes cookieBite{0%{background-image:url(images/cookie.webp)}25%{background-image:url(images/cookie_b1.webp)}50%{background-image:url(images/cookie_b2.webp)}75%{background-image:url(images/cookie_b3.webp)}100%{background-image:url(images/cookie.webp)}}
.discoDot{display:inline-block;width:.62em;height:.62em;vertical-align:baseline;position:relative;overflow:visible;color:transparent;filter:drop-shadow(0 0 .05em rgba(150,180,255,.55))}
.discoDot::before{content:"";position:absolute;inset:0;background:url(images/discoball.webp) center/contain no-repeat;filter:url(#inkSm)}
.discoDot::after{content:"";position:absolute;left:3%;top:10%;width:94%;height:88%;border-radius:50%;background:url(images/discoglints.webp) 0 50%/130% auto repeat;opacity:.85;animation:discoSweep 1.6s steps(13,end) infinite}
@keyframes discoSweep{from{background-position:0 50%}to{background-position:-200% 50%}}
.heartDot{display:inline-block;width:.62em;height:.62em;vertical-align:baseline;position:relative;overflow:visible;color:transparent}
.heartDot::before{content:"";position:absolute;inset:0;background:url(images/heart.webp) center/contain no-repeat;filter:url(#inkSm) drop-shadow(0 1px 1px rgba(0,0,0,.28));transform-origin:50% 50%;animation:heartDotBeat 1s steps(1,end) infinite}
@keyframes heartDotBeat{0%{transform:scale(1)}12.5%{transform:scale(1.14)}25%{transform:scale(1.07)}37.5%{transform:scale(1)}100%{transform:scale(1)}}
.camDot{display:inline-block;width:.92em;height:.66em;vertical-align:baseline;position:relative;color:transparent}
.camDot::before{content:"";position:absolute;inset:0;background:center/contain no-repeat;background-image:url(images/cam.webp);filter:url(#inkSm);animation:camSnap 2.8s steps(1,end) infinite;transform-origin:50% 70%}
@keyframes camSnap{0%,88%{transform:scale(1)}91%{transform:scale(1.12)}94%{transform:scale(.95)}97%,100%{transform:scale(1)}}
.glasses.on{opacity:1;animation:glassesOn .62s steps(5,end) forwards}
.glasses.off{opacity:0;animation:glassesOff .46s steps(4,end) forwards}
@keyframes glassesOn{0%{transform:translateY(40%) scale(1.1) rotate(-3.5deg);opacity:0}18%{opacity:1}46%{transform:translateY(11%) scale(1.04) rotate(2deg);opacity:1}70%{transform:translateY(-3.5%) scale(.995) rotate(-1deg);opacity:1}86%{transform:translateY(1.4%) scale(1.004) rotate(.4deg);opacity:1}100%{transform:translateY(0) scale(1) rotate(0);opacity:1}}
@keyframes glassesOff{0%{transform:translateY(0) scale(1) rotate(0);opacity:1}22%{transform:translateY(-3%) scale(1.01) rotate(1.2deg);opacity:1}100%{transform:translateY(34%) scale(1.12) rotate(-4deg);opacity:0}}
.popcrumb{position:absolute;width:2.2%;height:2.2%;background:#e7ddc7;z-index:5;opacity:0;pointer-events:none;will-change:transform,opacity}
.popbucket{position:absolute;left:50%;bottom:-24%;width:44%;height:auto;transform:translateX(-50%);transform-origin:50% 78%;z-index:8;opacity:0;pointer-events:none;will-change:transform,opacity;filter:url(#inkSm) drop-shadow(0 2px 2px rgba(0,0,0,.32))}
.kernel{position:absolute;width:9%;height:auto;z-index:8;opacity:0;pointer-events:none;will-change:transform,opacity;filter:url(#inkSm)}
.crumb{position:absolute;background-image:url(images/crumbtex.webp);background-size:150px 150px;pointer-events:none;z-index:6;will-change:transform,left,top,opacity}
.crumbshadow{position:absolute;border-radius:50%;background:radial-gradient(ellipse,rgba(12,9,6,.55),transparent 72%);pointer-events:none;z-index:6;will-change:transform,left,top,opacity;filter:blur(1px)}
.hungerdrag{position:fixed;pointer-events:none;font-family:var(--sans);font-weight:600;letter-spacing:-.02em;text-transform:none;font-size:var(--fs-h1);transform:translate(-50%,-50%);transform-origin:50% 18%;z-index:70;will-change:transform,left,top;color:var(--theme-ink);display:inline-flex;align-items:baseline;gap:.04em;white-space:nowrap}
.hungerdrag .bitn{animation:hglBite .16s steps(3,end) forwards;transform-origin:50% 38%}
@keyframes hglBite{0%{transform:scale(1.16);opacity:1}55%{transform:scale(.5) translateY(.12em);opacity:.6}100%{transform:scale(0) translateY(.3em);opacity:0}}
.stachecrumb{animation:stachePop .5s steps(4,end) forwards}
@keyframes stachePop{0%{transform:rotate(var(--r)) scale(0);opacity:0}55%{transform:rotate(var(--r)) scale(1.22);opacity:1}80%{transform:rotate(var(--r)) scale(.93);opacity:1}100%{transform:rotate(var(--r)) scale(1);opacity:1}}
.queueDots.on{opacity:.92}
.queueDots i{width:.5rem;height:.5rem;border-radius:50%;background:var(--theme-ink);filter:url(#inkSm);animation:qbounce .92s steps(7,end) infinite;animation-play-state:paused}
.queueDots.on i{animation-play-state:running}.queueDots i:nth-of-type(2){animation-delay:.12s}.queueDots i:nth-of-type(3){animation-delay:.24s}
@keyframes qbounce{0%{transform:translateY(0)}30%{transform:translateY(-95%)}60%{transform:translateY(0)}100%{transform:translateY(0)}}
.qsr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
@media(prefers-reduced-motion:reduce){.cookieDot::before,.discoDot::after,.heartDot::before,.camDot::before,.queueDots i,.stachecrumb,.glasses.on,.glasses.off{animation:none}.glasses.on{opacity:1}.glasses.off{opacity:0}}
#party{position:absolute;top:0;left:0;right:0;height:112vh;-webkit-mask-image:linear-gradient(#000 90%,transparent 100%);mask-image:linear-gradient(#000 90%,transparent 100%);z-index:63;pointer-events:none;opacity:0;transition:opacity 1.15s cubic-bezier(.33,0,.2,1)}
#party:not(.on){transition:opacity .42s var(--ease-exit)}#party.on{opacity:1}
.partyDark{position:absolute;inset:0;background:radial-gradient(circle var(--hr,180px) at var(--hx,50%) var(--hy,38%),rgba(6,5,16,0) 0%,rgba(6,5,16,.55) 44%,rgba(2,1,8,.96) 100%)}
.partyHaze{position:absolute;left:var(--bx,50%);top:var(--by,30%);width:120vmax;height:120vmax;margin:-60vmax 0 0 -60vmax;background:radial-gradient(circle,rgba(120,150,255,.16),rgba(180,90,220,.06) 40%,transparent 66%);mix-blend-mode:screen;animation:hazePulse 1.5s steps(12,end) infinite,hueShift 5.6s steps(7,end) infinite}
.beamWrap{position:absolute;left:var(--bx,50%);top:var(--by,30%);width:0;height:0}
.beamSpin{position:absolute;left:0;top:0;width:300vmax;height:300vmax;margin:-150vmax 0 0 -150vmax;transform-origin:center;mix-blend-mode:screen;opacity:.5;background:repeating-conic-gradient(from 0deg,rgba(255,255,255,.10) 0deg 3deg,transparent 3deg 11deg,rgba(180,210,255,.09) 11deg 13deg,transparent 13deg 26deg);animation:spinB 8s steps(64,end) infinite,hueShift 5.6s steps(7,end) infinite}
.partyLights{position:absolute;left:var(--bx,50%);top:var(--by,30%);width:0;height:0;transform-origin:0 0;mix-blend-mode:screen;animation:spinL 6s steps(48,end) infinite,hueShift 5.6s steps(7,end) infinite}
.pspot{position:absolute;left:0;top:0;border-radius:1px;transform-origin:0 0;opacity:0;animation-name:twinkle;animation-iteration-count:infinite;animation-timing-function:steps(8,end)}
.partyFlash{position:absolute;inset:0;background:radial-gradient(circle at var(--bx,50%) var(--by,30%),rgba(190,200,255,.10),transparent 60%);mix-blend-mode:screen;opacity:0;animation:beat 1s steps(8,end) infinite}
.partyGrain{position:absolute;inset:-40px;background:url(images/overlay.webp);background-size:150px 150px;mix-blend-mode:overlay;opacity:.4}
@keyframes spinL{to{transform:rotate(360deg)}}@keyframes spinB{to{transform:rotate(360deg)}}
@keyframes twinkle{0%,100%{opacity:0}14%{opacity:.1}50%{opacity:1}}@keyframes hazePulse{0%,100%{opacity:.55}50%{opacity:1}}@keyframes beat{0%,62%,100%{opacity:0}70%{opacity:.5}82%{opacity:.12}}@keyframes hueShift{to{filter:hue-rotate(360deg)}}
#discoWrap{position:absolute;z-index:64;pointer-events:none;opacity:0;transform:translate(-50%,-140%) scale(.82);transition:opacity .7s var(--ease-out),transform .95s cubic-bezier(.22,1.15,.32,1)}
#discoWrap:not(.on){transition:opacity .38s var(--ease-exit),transform .5s var(--ease-exit)}#discoWrap.on{transform:translate(-50%,-50%) scale(1);opacity:1}
.dbGlow{position:absolute;left:50%;top:50%;width:300%;height:300%;transform:translate(-50%,-50%);background:radial-gradient(circle,rgba(190,210,255,.45),rgba(150,120,220,.12) 30%,transparent 60%);mix-blend-mode:screen;animation:hazePulse 1.25s steps(10,end) infinite}
#discoBall{position:relative;width:clamp(104px,12.5vw,156px);height:clamp(104px,12.5vw,156px);border-radius:50%;overflow:hidden;clip-path:circle(50%);filter:drop-shadow(0 7px 18px rgba(0,0,0,.55))}
#discoBall .dbPhoto{position:absolute;inset:0;background:url(images/discoball.webp) center/cover no-repeat}
#discoBall .dbGlints{position:absolute;inset:-12%;background:url(images/discoglints.webp) repeat-x;background-size:150px 150px;mix-blend-mode:screen;animation:dbScroll 1.5s steps(12,end) infinite}
#discoBall .dbSweep{position:absolute;inset:0;background:linear-gradient(102deg,transparent 38%,rgba(255,255,255,.55) 50%,transparent 62%);mix-blend-mode:screen;animation:dbSweep 2s steps(16,end) infinite}
#discoBall .dbShade{position:absolute;inset:0;background:radial-gradient(circle at 33% 28%,rgba(255,255,255,.5),rgba(255,255,255,0) 44%),radial-gradient(circle at 50% 50%,transparent 62%,rgba(2,2,12,.6) 100%)}
@keyframes dbScroll{to{background-position-x:-150px}}@keyframes dbSweep{0%{transform:translateX(-65%)}100%{transform:translateX(65%)}}
body.partyLock{overflow:hidden}
#loveScene{position:absolute;top:0;left:0;right:0;height:100vh;z-index:64;pointer-events:none;opacity:0;transition:opacity .45s var(--ease-out)}#loveScene.on{opacity:1}
.heartLayer{position:absolute;inset:0}.heartEye{position:absolute;width:17%;transform:translate(-50%,-50%);transform-origin:50% 50%;z-index:5;pointer-events:none;will-change:transform,opacity;filter:url(#inkSm) drop-shadow(0 1px 2px rgba(0,0,0,.32))}
.loveHeart{position:fixed;transform:translate(-50%,-50%);will-change:transform,opacity;opacity:0;filter:url(#inkSm) drop-shadow(0 2px 4px rgba(0,0,0,.3))}
@keyframes heartFloat{0%{opacity:0;transform:translate(-50%,-50%) scale(.4) rotate(0)}16%{opacity:1}100%{opacity:0;transform:translate(calc(-50% + var(--dx,0px)),calc(-50% - 132px)) scale(1) rotate(var(--rot,0deg))}}
.blush{position:absolute;inset:0;pointer-events:none;z-index:4}.blush i{position:absolute;top:54%;width:22%;height:14.5%;border-radius:50%;background:radial-gradient(ellipse,rgba(194,72,90,.5),rgba(194,72,90,.27) 42%,transparent 73%);filter:blur(4px);mix-blend-mode:multiply}.blush i:first-child{left:28%;transform:rotate(-13deg)}.blush i:last-child{left:51%;transform:rotate(13deg)}
#photorain{position:absolute;top:0;left:0;right:0;height:100vh;-webkit-mask-image:linear-gradient(#000 86%,transparent 100%);mask-image:linear-gradient(#000 86%,transparent 100%);z-index:95;pointer-events:none;overflow:hidden}
body.heroEmpathy .jbStick .jbNav{--nav-mat:var(--ctl-ground);--nav-rim:var(--ctl-container-rim);transition:none}
#camflash{position:absolute;top:0;left:0;right:0;height:100vh;z-index:98;pointer-events:none;opacity:0;background:radial-gradient(circle at 50% 42%,#fff 0%,#fff 56%,rgba(255,255,255,.8) 100%)}
#photorain .pgrain{position:absolute;inset:0;background:url(images/overlay.webp) repeat;opacity:.18;mix-blend-mode:multiply;animation:rainGrain 1s steps(8,end) infinite}
@keyframes rainGrain{0%{background-position:0 0}12.5%{background-position:-13px 7px}25%{background-position:9px -11px}37.5%{background-position:-7px -6px}50%{background-position:12px 8px}62.5%{background-position:-11px 10px}75%{background-position:6px -9px}87.5%{background-position:-9px 4px}100%{background-position:0 0}}
.rainphoto{position:absolute;display:block;background:#f7f6f1 url(images/paper.webp) center/cover;padding:8px 8px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,.16),0 11px 26px rgba(0,0,0,.17);transform:translate(-50%,-50%);will-change:transform,opacity}.rainphoto img{display:block;width:100%;height:auto;filter:grayscale(1) contrast(1.03)}.rainphoto::after{content:"";position:absolute;left:6px;right:6px;top:6px;bottom:16px;background:url(images/paper.webp) center/cover;opacity:.5;mix-blend-mode:multiply;pointer-events:none}
@media(prefers-reduced-motion:reduce){.partyLights,.beamSpin,.partyHaze,.partyFlash,.dbGlints,.dbSweep,.dbGlow,.loveHeart{display:none}#photorain .pgrain{animation:none}}

/* .moodHeads now comes from header.css so both pages share one geometry -- this
   wrapping flex row was the other half of the split. Only this selector was touched
   in this file. */
.mhItem{position:relative}.mhItem img{width:34px;height:40px;object-fit:contain;background:var(--ctl-ground-hover);border-radius:var(--r-2xs);display:block}
.mhPick{padding:0;border:0;background:none;cursor:pointer;display:block;border-radius:var(--r-2xs);transition:box-shadow var(--dur-state) var(--ease-out)}
.mhPick:hover,.mhPick:focus-visible{box-shadow:0 0 0 1.5px var(--ctl-ink)}
.mhX{position:absolute;top:-6px;right:-6px;width:16px;height:16px;border-radius:var(--r-full);border:1px solid var(--theme-rim);background:var(--ctl-ground);color:var(--ctl-ink);font-size:var(--fs-micro);line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
.mhX::before{content:"";position:absolute;inset:-4px;border-radius:var(--r-full)}.mhX:hover{color:var(--ctl-ink-strong);border-color:var(--theme-rim-strong)}

.moodMenu .moodItem[hidden]{display:none}
.gIco{width:18px;height:18px;flex:0 0 18px;display:block;color:var(--ctl-ink);transition:color var(--dur-state) var(--ease-out)}
.moodItem:hover .gIco{color:var(--ctl-ink-strong)}
#endGame{color:#b3402e}#endGame .gIco{color:#b3402e}#endGame:hover{color:#8f3323}#endGame:hover .gIco{color:#8f3323}
/* The maker links (Create head, Gradient maker) are <a>, not <button>, so they take .moodGo --
   the same class index.html's "Add your head" link uses. play.css referenced .moodGo in two
   game-gating rules (above) but never carried its base rule, because until now play.html had no
   link in the menu. Copied verbatim from index.html's own .moodGo/.moodSep declarations so the
   two menus stay one design. */
.moodGo{display:flex;align-items:center;gap:var(--sp-12);width:100%;padding:var(--sp-12) var(--sp-16);border:0;border-radius:var(--r-2xs);background:none;color:var(--ctl-ink-strong);font-family:var(--sans);font-weight:400;font-size:var(--fs-small);line-height:1.2;text-decoration:none;transition:background-color var(--ease-out-dur) var(--ease-out)}
.moodMenu .moodGo{color:var(--ctl-ink-strong)}
.moodGo:hover,.moodGo:focus-visible{background:var(--ctl-ground-hover)}
.moodGo svg{flex:0 0 18px;width:18px;height:18px;color:currentColor}
.moodSep{height:1px;background:var(--theme-rim);margin:var(--sp-8) var(--sp-4) var(--sp-8)}
.battleBadge{display:none;position:absolute;left:50%;top:14px;transform:translateX(-50%);z-index:47;align-items:center;gap:var(--sp-8);padding:var(--sp-8) var(--sp-12);border:1px solid var(--theme-rim);border-radius:var(--r-2xs);background:var(--ctl-ground);font-family:var(--sans);font-size:var(--fs-label);font-weight:600;color:var(--ctl-ink);filter:url(#inkSm);pointer-events:none}
.battleBadge i{width:7px;height:7px;border-radius:var(--r-full);background:#b3402e;animation:bbPulse 1.1s steps(2,end) infinite}
@keyframes bbPulse{0%,100%{opacity:1}50%{opacity:.35}}
body.hmBattle .moodMenu .moodItem:not(#endGame),body.hmBattle .moodMenu .moodGo{opacity:.38;pointer-events:none}
@media(pointer:coarse){.mhX{width:26px;height:26px;font-size:var(--fs-small);top:-9px;right:-9px}.mhItem img{width:44px;height:52px}}
body.hmSoccer .moodMenu .moodItem:not(#endGame),body.hmSoccer .moodMenu .moodGo{opacity:.38;pointer-events:none}
.hmBall{position:absolute;left:0;top:0;width:48px;height:48px;z-index:48;opacity:0;pointer-events:none;transition:opacity .3s;transform-origin:50% 50%}
.hmBallSkin{position:absolute;inset:0;background:var(--ballSkin,url(images/soccerball.webp)) center/contain no-repeat;transform-origin:50% 50%;will-change:transform}
.hmBallShade{position:absolute;inset:0;border-radius:var(--r-full);pointer-events:none;background:radial-gradient(circle at 35% 29%,rgba(255,255,255,.92) 0%,rgba(255,255,255,.32) 7%,rgba(255,255,255,0) 20%),radial-gradient(circle at 38% 32%,rgba(255,255,255,.3) 0%,rgba(255,255,255,0) 46%),radial-gradient(circle at 35% 32%,rgba(0,0,0,0) 28%,rgba(0,0,0,.2) 70%,rgba(0,0,0,.46) 100%),radial-gradient(circle at 50% 50%,rgba(0,0,0,0) 78%,rgba(0,0,0,.46) 100%);mix-blend-mode:normal}
.hmBallShadow{position:absolute;left:0;top:0;width:48px;height:13px;border-radius:var(--r-full);background:radial-gradient(ellipse at center,rgba(8,8,8,.26),rgba(8,8,8,0) 70%);opacity:0;pointer-events:none;z-index:1;transition:opacity .3s}
.hmGoal{position:absolute;left:0;top:0;width:42px;height:162px;z-index:2;opacity:0;pointer-events:none;transition:opacity .3s;border-radius:3px;background:repeating-linear-gradient(45deg,rgba(26,26,26,.16) 0 1px,transparent 1px 8px),repeating-linear-gradient(-45deg,rgba(26,26,26,.16) 0 1px,transparent 1px 8px)}
.hmGoalShadow{position:absolute;left:0;top:0;width:60px;height:15px;border-radius:var(--r-full);background:radial-gradient(ellipse at center,rgba(8,8,8,.24),rgba(8,8,8,0) 72%);opacity:0;pointer-events:none;z-index:1;transition:opacity .3s}
.hmGoalR{box-shadow:inset 20px 0 26px -14px rgba(16,16,16,.5),inset 0 12px 16px -12px rgba(16,16,16,.35)}
.hmGoalB{box-shadow:inset -20px 0 26px -14px rgba(16,16,16,.5),inset 0 12px 16px -12px rgba(16,16,16,.35)}
.hmGoal::before{content:"";position:absolute;top:-3px;bottom:-3px;width:6px;border-radius:var(--r-2xs)}
.hmGoalR::before{right:-1px;background:linear-gradient(90deg,rgba(var(--tc1,224,90,78),.5),rgba(var(--tc1,224,90,78),1) 60%);box-shadow:0 0 7px rgba(var(--tc1,224,90,78),.4),inset -1.5px 0 1px rgba(255,255,255,.55)}
.hmGoalB::before{left:-1px;background:linear-gradient(270deg,rgba(var(--tc2,90,160,216),.5),rgba(var(--tc2,90,160,216),1) 60%);box-shadow:0 0 7px rgba(var(--tc2,90,160,216),.4),inset 1.5px 0 1px rgba(255,255,255,.55)}
.hmGoal::after{content:"";position:absolute;inset:0;border-radius:3px;border-top:2.5px solid;border-bottom:2.5px solid;pointer-events:none}
.hmGoalR::after{border-color:rgba(var(--tc1,224,90,78),.72)}
.hmGoalB::after{border-color:rgba(var(--tc2,90,160,216),.72)}
.hmGoal.hmGoalHit{animation:hmNet .55s cubic-bezier(.2,.9,.3,1)}
@keyframes hmNet{0%{box-shadow:inset 0 0 0 0 rgba(232,181,74,0)}28%{box-shadow:inset 0 0 24px 3px rgba(232,181,74,.55),0 0 0 3px rgba(232,181,74,.5)}100%{box-shadow:inset 0 0 0 0 rgba(232,181,74,0)}}
/* THE GOAL GRAMMAR camera: two plain wrappers. .hmCamBack holds the ball's shadow, both
   goals and both goal shadows (z:1-2, belongs BELOW the heads); .hmCamFront holds ONLY the
   ball (z:48, belongs ABOVE the heads) -- the ball's shadow does not ride along with it,
   or giving the wrapper the ball's z-index during a punch would drag the shadow above the
   heads too. Idle, both wrappers carry no z-index or transform of their own, which per spec
   makes them invisible to normal stacking (a position:absolute box with z-index:auto and
   transform:none never opens a new stacking context, so its children keep competing
   directly against the heads exactly as before). Only .hmCamPunch -- applied for one
   animation's length -- claims the z-index the contents already had, so nothing z-fights
   the heads mid-punch. .hmScore is never a descendant of either wrapper, so the board
   cannot be moved by this. */
.hmCam{position:absolute;inset:0;pointer-events:none}
.hmCamBack.hmCamPunch{z-index:2;animation:hmCamPunch .5s cubic-bezier(.22,.7,.24,1) both}
.hmCamFront.hmCamPunch{z-index:48;animation:hmCamPunch .5s cubic-bezier(.22,.7,.24,1) both}
@keyframes hmCamPunch{
 0%{transform:scale(1) translate(0,0)}
 16%{transform:scale(1.05) translate(var(--camDX,0px),var(--camDY,0px))}
 42%{transform:scale(1.03) translate(calc(var(--camDX,0px) * -0.42),calc(var(--camDY,0px) * -0.32))}
 68%{transform:scale(1.012) translate(calc(var(--camDX,0px) * 0.16),calc(var(--camDY,0px) * 0.12))}
 100%{transform:scale(1) translate(0,0)}
}
@media(prefers-reduced-motion:reduce){.hmCamBack.hmCamPunch,.hmCamFront.hmCamPunch{animation:none}}

/* ================= THE WATER UNDER THE HEADS (and a pitch curve that is switched off) ============
   Jayden asked for a gradient planet the heads stand on, plus a reflection. Then, on 2026-08-03, he
   corrected the first half of that: "For the planet, I wanted the planet to be in the lobby, not the
   match." So the PLANET moves to the Play hub as its own surface (NASA Blue Marble there, which is
   the one place the research always said it belonged), and what stays here is the second half he
   asked for and never withdrew -- the water and the reflection under the heads on the pitch.

   What is left of the planet on this page is the drawn ground below, held behind one flag. The pitch
   curve is OFF (play-engine.js: CURVE=false, so ARC's sag is 0 and the six render sites emit exactly
   the strings they emitted before any of this existed) and .hmFlatPitch takes the drawn ground and
   sky down with it. The water and the reflections are NOT gated on the curve -- they never depended
   on it -- so with sag 0 the waterline is simply straight. __hmSag(25.6) turns the whole thing on
   live if the curve is ever wanted back.

   The geometry is settled in docs/superpowers/specs/2026-08-02-planet-pitch-research.md and it is
   render-curved / physics-flat: play-engine.js adds arcY(x) as a render offset at six transform
   sites and never tells survey(), floorY, __hmFeetY or soccer's geo() about it. The four --pl-*
   custom properties below are written on .hero by planetGeo(), once per layout, and they are shared
   by all three elements so the drawn horizon and the arc the heads walk are the SAME ellipse rather
   than two things tuned to look alike. --pl-b is solved from the sag, so they cannot drift apart.

   Not NASA Blue Marble. images/earth-map-src.jpg is 2.51 MB and is reserved for §3.9's lobby globe,
   where a recognisable Earth is the joke; continents under a football pitch read as a map, not as
   ground, and a photographic Earth breaks nearly every clause of gradient-reference-notes.md §A at
   once. This surface is Jayden's own Amethyst preset instead -- his Gradient Lab export, judged the
   best planet of the three he saved (one dominant light zone, few masses, real limb shading). It is
   built here as ordered gradient passes so the taste call can be made live; the moment a baked
   .webp lands it replaces the `background` on .hmPlanet and the mask, tokens and geometry all stay.

   Checked against the contract clause by clause:
     A1 layered passes, ordered occlusion -- five passes, not one linear-gradient.
     A2 tension edges -- the lit flank is taut (a 26% stop), the trailing side feathers to 72%.
     A4 the rim escapes the silhouette -- .hmSky carries a warm arc that straddles the horizon and
        bleeds ABOVE it, which is the single strongest "this is a planet, not a hill" cue.
     A5 grain is static and chunky, concentrated in the transition, never animated.
     A6 spherical form -- the surface taper IS the ellipse, and it darkens toward the limb.
     B1 corner-anchored, partially cropped -- the limb sits a full pitch width off-screen on each
        side. A centred orb is a poster, a cropped orb is UI, and an on-screen limb is exactly what
        makes it read as "a ball they are standing on", the named failure mode.
   Cost: three elements, zero JS per frame, zero canvas, zero WebGL, zero rAF, and no new blur --
   play.css still declares exactly two filter:blur() rules and neither of them is here. */
:root{
 /* Amethyst, gradientlab.html:866-870 -- Jayden's own export, kept as tokens so a rebake is a
    five-value edit. Definitions have to be literal; every USE below goes through the token. */
 --planet-lit:250,249,255;    /* #f0eff7, the light zone */
 --planet-mid:160,122,228;    /* #a07ae4, the body of the surface */
 --planet-core:158,53,229;    /* #9e35e5, the one small accent mass */
 --planet-deep:147,21,164;    /* #9315a4, the flank away from the light */
 --planet-rim:255,246,232;    /* the specular. It belongs to the light, not to a stroke. */
 --planet-lx:38%;             /* the dominant light, deliberately OFF pitch centre: a symmetrically
                                 lit arch reads as an arch (A2), and the arc has to read as a world. */
 --planet-h:340px;            /* the shared box: 120px of sky above the horizon, 220px of ground below */

 /* THE CONTACT SHADOW'S INK, made reachable. play-engine.js writes the head shadow as an inline
    cssText string, and nothing in a stylesheet can reach an inline style attribute -- so on a dark
    ground every head would have kept a black smudge under it and silently stopped being grounded.
    The engine now writes rgba(var(--head-shadow,8,8,8),var(--head-shadow-a,.26)), and an inline var()
    still resolves against inherited custom properties, so rebinding these two on body is all a dark
    surface has to do. The values here are the ones the engine has always used, so this changes
    nothing today; it only makes the change possible. The heads keep their shadow -- it is the one
    shadow this site keeps, because they stand on something. */
 --head-shadow:8,8,8;
 --head-shadow-a:.26;

 /* THE REFLECTION'S STRENGTH, for the same reason. The reflection reads at this fraction of the
    contact shadow's opacity -- the one number separating two readings of the same fact -- and the
    engine picks it up once per layout, not per frame. A dark surface that wants a lighter, weaker
    reflection changes this rather than forking the engine. Everything else about how the reflection
    LOOKS (its saturate(), its mask ramp) is already in a stylesheet below and overridable there;
    this was the only piece of its appearance stranded in JS. */
 --head-refl-gain:0.9;
}
.hmPlanet,.hmSky,.hmWater{position:absolute;left:0;right:0;top:var(--pl-top,62%);height:var(--planet-h);
 pointer-events:none;opacity:0;transition:opacity .6s var(--ease-out)}
body.hmSoccer .hmPlanet,body.hmSoccer .hmSky,body.hmSoccer .hmWater{opacity:1}
/* sag 0 means no curve, so it has to mean no DRAWN curve either -- and a zero-height mask ellipse is
   degenerate anyway. The water stays: the reflection's waterline is not a curvature feature, it just
   becomes a straight line, so its arc mask comes off rather than the element going away. */
.hero.hmFlatPitch .hmPlanet,.hero.hmFlatPitch .hmSky{display:none}
.hero.hmFlatPitch .hmWater{-webkit-mask-image:none;mask-image:none}

/* THE GROUND. Masked to the ellipse whose top edge IS arcY, so the horizon and the feet cannot
   disagree -- the failure the research names as fatal is heads floating a quarter of a head above a
   drawn curve. Passes are listed topmost-first, the way CSS stacks them. */
.hmPlanet{z-index:0;
 background:
  /* 5 - the specular, ON the horizon and just inside the silhouette */
  radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),
   rgba(var(--planet-rim),0) 99.52%,rgba(var(--planet-rim),.9) 99.74%,rgba(var(--planet-rim),0) 100%),
  /* 4 - the light zone: taut on the lit flank, long feathered dissolve behind it */
  radial-gradient(54% 42% at var(--planet-lx) 36%,
   rgba(var(--planet-lit),.95) 0%,rgba(var(--planet-lit),.42) 26%,rgba(var(--planet-lit),0) 72%),
  /* 3 - the one small accent mass */
  radial-gradient(15% 20% at 69% 41%,rgba(var(--planet-core),.26) 0%,rgba(var(--planet-core),0) 74%),
  /* 2 - the deep flank, opposite the light, carrying the limb shading */
  radial-gradient(62% 48% at 88% 44%,rgba(var(--planet-deep),.30) 0%,rgba(var(--planet-deep),0) 70%),
  /* 1 - the surface itself, falling away from the horizon to nothing well before the box ends */
  linear-gradient(180deg,rgba(var(--planet-mid),0) 33%,rgba(var(--planet-mid),.46) 36%,
   rgba(var(--planet-mid),.20) 62%,rgba(var(--planet-mid),0) 92%);
 -webkit-mask-image:radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),#000 99.6%,transparent 100%);
         mask-image:radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),#000 99.6%,transparent 100%)}
/* A5: static, CHUNKY (CSS-pixel cells), concentrated in the transition, cores clean, never animated.
   Two co-prime cell sizes rather than one, so the pattern does not moire against itself, and no
   feTurbulence data URI: the contract asks for pixel cells, and a nested url(#id) inside a data URI
   is also what makes tools/token-audit.py's paren matcher lose the rest of the declaration (the same
   artifact .iris::before at line 96 already produces). When the baked .webp lands the grain comes
   with it -- A5 says it belongs in the bake -- and this rule is deleted. */
.hmPlanet::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.10;
 mix-blend-mode:soft-light;background-blend-mode:overlay;
 background-image:
  repeating-conic-gradient(rgba(18,18,18,.5) 0% 25%,rgba(253,253,253,.5) 0% 50%),
  repeating-conic-gradient(rgba(18,18,18,.5) 0% 25%,rgba(253,253,253,.5) 0% 50%);
 background-size:3px 3px,5px 5px;
 -webkit-mask-image:linear-gradient(180deg,transparent 33%,#000 40%,transparent 74%);
         mask-image:linear-gradient(180deg,transparent 33%,#000 40%,transparent 74%)}

/* A4 + the fifth "reads as a planet" cue: the rim that escapes the silhouette upward, and a band of
   sky that is not the page background. Unmasked on purpose -- everything here lives OUTSIDE the
   ellipse, which is precisely what .hmPlanet's mask cuts away. */
.hmSky{z-index:0;
 background:
  radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),
   rgba(var(--planet-rim),0) 99.98%,rgba(var(--planet-rim),.5) 100.12%,rgba(var(--planet-rim),0) 101.4%),
  radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),
   rgba(var(--planet-mid),0) 100%,rgba(var(--planet-mid),.085) 101.2%,rgba(var(--planet-mid),0) 109%)}

/* THE WATER. One shared overlay whose ripple is a background-position keyframe: the motion lives in
   the water, so the reflection elements only ever translate and never re-rasterise, and this needs
   no rAF slot and no JS at all. Two nested masks rather than one composited pair -- the outer
   element takes the same ellipse as the ground (so the waterline follows the horizon), the inner
   ::before takes the vertical falloff. mask-composite is still prefix soup; nesting is not. */
.hmWater{z-index:1;
 -webkit-mask-image:radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),#000 99.6%,transparent 100%);
         mask-image:radial-gradient(var(--pl-a,1920px) var(--pl-b,460px) at 50% var(--pl-cy,580px),#000 99.6%,transparent 100%)}
.hmWater::before{content:"";position:absolute;inset:0;
 background-image:repeating-linear-gradient(180deg,
  rgba(var(--planet-rim),.20) 0 1px,rgba(var(--planet-rim),0) 1px 7px,
  rgba(var(--planet-deep),.055) 7px 8px,rgba(var(--planet-deep),0) 8px 16px);
 background-size:100% 16px;
 -webkit-mask-image:linear-gradient(180deg,transparent 33%,rgba(0,0,0,.9) 41%,rgba(0,0,0,.34) 64%,transparent 86%);
         mask-image:linear-gradient(180deg,transparent 33%,rgba(0,0,0,.9) 41%,rgba(0,0,0,.34) 64%,transparent 86%);
 animation:hmWaterDrift 9s linear infinite}
/* -64px is exactly four 16px periods, so the loop is seamless. background-position only: compositor
   work, no layout, no paint of anything else, and the browser throttles it off-screen for free. */
@keyframes hmWaterDrift{to{background-position:0 -64px}}
@media(prefers-reduced-motion:reduce){.hmWater::before{animation:none}}

/* THE REFLECTION. One silhouette per head, on the head's own cut-out -- see play-engine.js for why
   this is neither a clone of the head rig nor -webkit-box-reflect. transform-origin matches the head
   root's 50%/94% so the two rotate about the same point and the mirror composes as
   translate() scale(1,-1) rotate(theta) with no negated angle anywhere.
   The falloff is a mask, never a blur. The mask runs `to top` because after the flip the element's
   BOTTOM is the waterline and its top is the crown -- the reflection has to be solid where it meets
   the water and gone at the far end, not the other way round.
   saturate(.5) is the "colour, not detail" rule: detail in a reflection is what makes it look like a
   copy rather than a reflection. It is static, so it rasterises once and never again -- the per-frame
   writes are transform and opacity only, both compositor-only.
   opacity is written by the engine every frame (0 off the pitch), so it carries no transition here:
   a transition would fight the air-height falloff. */
/* transform-origin here is only the value BEFORE the engine's pixel scan lands; play-engine.js then
   writes this head's own scanned foot line (0.945 for an egghead, 0.75 for mini-Jayden) so the pivot
   sits exactly on the waterline and the anchor survives any rotation or squash.
   position/left/top/pointer-events deliberately live in the engine's inline cssText instead of here:
   index.html loads play-engine.js and does NOT load play.css, so a reflection that depended on this
   file for `position:absolute` was a static full-height block in the home page's flow -- one per
   head. Visibility stays here (opacity:0, and the two overrides below), because inline beats a
   stylesheet and inlining it would have made those overrides unreachable. */
/* WHY IT READ AS A STICKER, and what each line does about it. Jayden: "I realize the reflections
   aren't real at all -- like they are fake reflections." He was right and the cause was geometric:
   a straight scale(1,-1) is a MIRROR, and a mirror is not what someone standing above a reflective
   surface sees. Four fixes, in order of how much each is worth:

   1 FALLOFF, and it has to be CONVEX. Reflection intensity drops with distance from the surface, so
     the pixels at the waterline are nearly as strong as the head and the crown is gone. The old ramp
     was linear (100% -> 40% -> 0 over even steps), and a linear ramp reads as a FADE, which is a
     different thing entirely. This one holds 78% at 12% of the way up and is down to 12% by 56%:
     strong where it touches, collapsing fast. `to top` because after the flip the element's BOTTOM
     is the waterline.
   2 THE SURFACE EATS INTO IT (see the @supports block below). The ripple used to live only in
     .hmWater, painted OVER the reflection -- which is an image with texture on top of it, not an
     image in water. The same 16px band period is now a second MASK layer on the reflection itself,
     intersected with the falloff, so the surface breaks the reflected image up instead of sitting
     on it. Deliberately STATIC: mask-position is not a compositor-animated property, and animating
     it would repaint every reflection every frame -- the one thing this whole build is designed
     never to do. The motion stays in .hmWater passing over the top. If the break-up ever needs to
     move as well, that is the known lever and it costs one raster per head per frame.
   3 LOWER CONTRAST, NOT JUST LOWER OPACITY. A reflection loses contrast; it does not simply become
     see-through. contrast(.72) with a small brightness lift is the difference between "dimmer copy"
     and "reflection". Static, so it rasterises once and never again -- the per-frame writes are
     still transform and opacity only.
   4 NOT SHARPER THAN THE WATER -- handled by 2 and 3 rather than by a blur, on purpose. This file
     still declares exactly two filter:blur() rules and neither is here. If it still reads crisp,
     a 0.6px static blur is the next lever and the first thing that should be tried.

   The vertical compression that goes with all this is a scale factor in the engine, not here --
   see REFL_PERSP in play-engine.js for the camera it is derived from. */
.hmRefl{position:absolute;left:0;top:0;z-index:1;pointer-events:none;opacity:0;
 transform-origin:50% 94%;will-change:transform,opacity;
 background-repeat:no-repeat;background-size:100% 100%;
 filter:saturate(.42) contrast(.72) brightness(1.06);
 -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.78) 12%,rgba(0,0,0,.34) 34%,rgba(0,0,0,.12) 56%,transparent 74%);
         mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.78) 12%,rgba(0,0,0,.34) 34%,rgba(0,0,0,.12) 56%,transparent 74%)}
/* The ripple layer is added only where the compositing keyword actually works. Without
   mask-composite the two layers would default to `add` -- a UNION, which would undo the falloff and
   make the reflection more solid rather than less. So the base rule above stands alone as the
   fallback and this is purely additive. */
@supports ((mask-composite:intersect) or (-webkit-mask-composite:source-in)){
 .hmRefl{
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.78) 12%,rgba(0,0,0,.34) 34%,rgba(0,0,0,.12) 56%,transparent 74%),
   repeating-linear-gradient(180deg,#000 0 9px,rgba(0,0,0,.42) 10px,rgba(0,0,0,.72) 12px,#000 16px);
  -webkit-mask-composite:source-in;
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.78) 12%,rgba(0,0,0,.34) 34%,rgba(0,0,0,.12) 56%,transparent 74%),
   repeating-linear-gradient(180deg,#000 0 9px,rgba(0,0,0,.42) 10px,rgba(0,0,0,.72) 12px,#000 16px);
  mask-composite:intersect}}
/* ============ THE LOBBY FLOOR IS REFLECTIVE TOO ============================================
   Jayden: "I do want the room to be reflective like the soccer." The pitch has had honest
   reflections since the water landed and the lobby had none -- play-engine.js's one gate read
   `soccerOn`. That gate now also passes in the lobby, and every claim it was built on still holds
   there unchanged (the anchor, the gap that opens on a jump, the falloff off the contact shadow's
   own numbers). What CANNOT carry over is the MATERIAL, and pretending otherwise is precisely the
   thing he objected to when he said the reflections "aren't real":

   THE LOBBY IS NOT WATER, SO IT MUST NOT RIPPLE. The pitch reflection is broken up by a 16px band
   mask because it is sitting in moving water. There is no water in the lobby; a ripple here would
   be a decoration imitating a physical cause that does not exist, which is the same class of lie
   as a reflection glued to the feet of a head in mid-air. So the band layer comes OFF and the
   image stays continuous -- a hard polished floor returns an unbroken image.

   A DRY FLOOR REFLECTS SHORTER AND TRUER. Two consequences, both in the opposite direction from
   water and both deliberate:
     falloff  water runs to 74% of the head; this dies by 52%. A polished floor returns a much
              shorter image than water does -- water's long streak comes from the surface tilting
              toward the eye across the ripple, and a flat floor has no such tilt. Still convex
              (62% at 8%, 7% by 38%): strong where it touches, collapsing fast.
     colour   saturate(.72), not water's .42. Water is a tinted MEDIUM and drains colour; dry
              stone does not. It loses CONTRAST (.66, slightly more than the pitch) and gains
              brightness -- on a paper-white ground a reflection is a faint tint of the head, and
              the difference between "reflection" and "grey ghost" is that it keeps its hue.
   No blur, for the reason this file has always given: it declares exactly two filter:blur() rules
   and neither is here. Micro-roughness is the honest next lever if it ever reads too crisp, and it
   costs one raster per head per frame -- which is why it is named rather than taken.

   THERE IS NO DRAWN FLOOR, AND THAT IS THE RESULT OF BUILDING ONE AND LOOKING AT IT. A band of
   ink under the feet line was tried first, anchored to the shared feet plane so it could not drift
   from the crowd. Measured at 1440 it did exactly what this page keeps refusing: its top edge is a
   full-width horizontal line, which reads as a drawn horizon, and below it the bottom fifth of the
   page goes grey and heavy right where four white cards have to sit. Softening the edge only trades
   a line for a smudge.
   The reflection IS the floor. That is not a compromise, it is what a polished white surface in a
   white room actually looks like -- the surface itself is invisible and the only evidence of it is
   what it returns. Side by side the version with no band is cleaner, quieter, and the reflections
   stop competing with a ground and start being the ground. Subtract, don't add.
   The heads keep their own contact shadows on top of it: those are the one shadow this site keeps,
   because they stand on something, and the reflection is now the second reading of that same fact. */
body.pHubOn .hmRefl{
 filter:saturate(.72) contrast(.66) brightness(1.12);
 -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.62) 8%,rgba(0,0,0,.24) 22%,rgba(0,0,0,.07) 38%,transparent 52%);
         mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.62) 8%,rgba(0,0,0,.24) 22%,rgba(0,0,0,.07) 38%,transparent 52%);
 -webkit-mask-composite:source-over;mask-composite:add}
/* Reflections are the first thing to drop on a phone (research §5), where the full-screen arena also
   puts the waterline on the viewport floor with nothing under it to reflect into. That call is kept
   for the lobby too: five reflections are five more composited layers on the device least able to
   pay for them, on the screen where the crowd is smallest and a reflection reads least. */
@media(pointer:coarse){.hmRefl{display:none}}
/* Every piece of this degrades to nothing. Without mask-image the arc becomes a rectangle and the
   reflection becomes a hard-edged duplicate -- both worse than the pitch as it was, so take the
   pitch as it was. */
@supports not ((-webkit-mask-image:radial-gradient(#000,#0000)) or (mask-image:radial-gradient(#000,#0000))){
 .hmPlanet,.hmSky,.hmWater,.hmRefl{display:none}}

/* Scorer lower-third: reads only from the goal event + head roster, never an invented name.
   Enters from the left (the punch's own motion axis), holds, leaves. Positioned low on the
   pitch so it never overlaps .hmScore, which stays put at the top throughout. */
.hmLowerThird{position:absolute;left:16px;bottom:13%;z-index:49;display:flex;align-items:center;gap:var(--sp-10);max-width:min(74vw,360px);padding:var(--sp-8) var(--sp-14) var(--sp-8) var(--sp-8);border-radius:var(--r-xs);background:rgba(22,22,24,.88);color:#f4f2ec;font-family:var(--sans);box-shadow:0 14px 32px -12px rgba(0,0,0,.5);transform:translateX(-130%);opacity:0;transition:transform .4s cubic-bezier(.2,.85,.25,1),opacity .3s linear;pointer-events:none}
.hmLowerThird.in{transform:translateX(0);opacity:1}
/* THE GOAL BELONGS TO A SIDE. The card used to be pinned bottom-left and always
   slid in from the left, whichever end the ball went in -- Jayden flagged it as
   not making sense, and he's right: with two nets on screen a fixed corner reads
   as disconnected from the action. Red attacks the RIGHT net (netCatch maps
   team 1 -> goalR), Blue the LEFT, so the card now appears on the end where the
   goal actually happened and slides in from that edge. The one motion axis the
   spec asked for still holds per side -- it just mirrors. */
.hmLowerThird.right{left:auto;right:16px;transform:translateX(130%)}
.hmLowerThird.right.in{transform:translateX(0)}
.hmLowerThird .hmL3Face{width:34px;height:34px;border-radius:var(--r-full);overflow:hidden;flex:0 0 auto;background:#333}
.hmLowerThird .hmL3Face img{width:100%;height:100%;object-fit:cover;display:block}
.hmLowerThird .hmL3Txt{display:flex;flex-direction:column;line-height:1.22;min-width:0}
.hmLowerThird .hmL3Tag{font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:rgb(var(--l3c,255,206,74))}
.hmLowerThird .hmL3Nm{font-size:var(--fs-nav);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:640px){.hmLowerThird{left:10px;bottom:9%;max-width:min(82vw,300px);padding:7px var(--sp-12) 7px 7px}}
@media(prefers-reduced-motion:reduce){.hmLowerThird{transition:none}}
.battleCount{position:absolute;left:50%;top:12px;transform:translateX(-50%);z-index:47;display:flex;align-items:center;gap:var(--sp-8);padding:var(--sp-8) var(--sp-16);border:1px solid var(--theme-rim);border-radius:var(--r-2xs);background:var(--ctl-ground);font-family:var(--sans);font-weight:600;font-size:var(--fs-body);color:var(--ctl-ink-strong);opacity:0;pointer-events:none;transition:opacity .3s;filter:url(#inkSm)}
/* Was z-index:47 -- above the heads (root, z:3), the goals (.hmGoal, z:2) and their shadows
   (z:1), so the board painted OVER the pitch instead of sitting on it. Jayden: "score board
   should be behind everything." 0 puts it below every pitch element (heads, ball z:48, goals,
   shadows) while staying above the plain .hero background, so it's still legible where nothing
   crosses it -- it just no longer wins against a head or the ball passing through. Position is
   untouched (left/top/transform, all unrelated to z-index) and it stays a direct child of
   `hero`, never a .hmCamBack/.hmCamFront descendant, so stagePunch() still can't move it. */
.hmScore{position:absolute;left:50%;top:var(--heroTitleTop,40px);transform:translateX(-50%);z-index:0;display:flex;align-items:center;gap:var(--sp-12);padding:var(--sp-8) var(--sp-16);border:1px solid var(--c100);border-radius:var(--r-2xs);background:var(--c50);font-family:var(--sans);font-weight:600;font-size:var(--fs-body);color:var(--c950);opacity:0;pointer-events:none;transition:opacity .3s;filter:url(#inkSm)}
.hmScore .sTeam{display:flex;align-items:center;gap:var(--sp-8)}
.hmScore .sTeam i{width:9px;height:9px;border-radius:var(--r-full)}
.hmScore .sTr i{background:#e05a4e}.hmScore .sTb i{background:#5aa0d8}
.hmScore em{color:var(--c400);font-style:normal}
.hmScore small{margin-left:var(--sp-8);padding-left:var(--sp-12);border-left:1px solid var(--c100);font-weight:400;font-size:var(--fs-small);color:var(--c500)}
/* Below the scoreboard (47), not above it. At 48 the shout landed on top of the board and
   the two illegible-ed each other. Dropped down the pitch too, clear of the board's box. */
.hmCount{position:absolute;left:50%;top:52%;transform:translate(-50%,-50%);z-index:44;font-family:var(--sans);font-weight:600;font-size:clamp(64px,12vw,140px);line-height:1;color:var(--theme-ink);opacity:0;pointer-events:none;filter:url(#inkBig);white-space:nowrap}
.hmCount.hmMsg{font-size:clamp(32px,6vw,72px);font-weight:600;letter-spacing:-.01em}   /* a word, not a digit: fits "Blue wins!" */
.hmScoreEnd{margin-left:var(--sp-8);padding:var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-12);border:0;border-left:1px solid var(--c100);border-radius:0;background:none;display:inline-flex;align-items:center;gap:var(--sp-4);cursor:pointer;pointer-events:auto;color:#b3402e;font-family:var(--sans);font-weight:600;font-size:var(--fs-label);line-height:1;transition:color var(--dur-state) var(--ease-out)}
.hmScoreEnd:hover{color:#8f3323}.hmScoreEnd svg{width:12px;height:12px;flex:0 0 auto}
/* battle arena platforms: light Apple-clean slabs with a hair of 2.5D thickness -- the heads leap up for height and pounce down */
.hmPlat{position:absolute;left:0;top:0;height:12px;border-radius:var(--r-xs);background:var(--c50);border:1px solid rgba(22,22,28,.14);box-shadow:0 1px 2px rgba(22,22,28,.08);border:1px solid rgba(22,22,28,.16);opacity:0;pointer-events:none;transition:opacity .45s var(--ease-out);z-index:2;will-change:transform}
body.hmBattle .hmPlat.arrived{opacity:1}
body.hmPodium .hmPlat{opacity:0}   /* platforms clear off once the podium rises */
/* the winners' podium: three light blocks, 1st tallest in the middle, that rise at the end of a battle */
.hmPodium{position:absolute;left:0;top:0;border-radius:var(--r-sm) var(--r-sm) 3px 3px;background:linear-gradient(180deg,#fefeff 0%,#eeeef2 62%,#e1e1e8 100%);box-shadow:inset 0 1px 0 rgba(255,255,255,.92),0 12px 24px -11px rgba(22,22,28,.34),0 2px 5px rgba(22,22,28,.12);border:1px solid rgba(22,22,28,.06);opacity:0;pointer-events:none;transition:opacity var(--dur-enter) var(--ease-out),transform var(--dur-enter) var(--ease-out);z-index:2;display:flex;align-items:flex-start;justify-content:center;overflow:hidden}
.hmPodium b{margin-top:var(--sp-8);font-family:var(--sans);font-weight:600;font-size:var(--fs-label);color:var(--c400);line-height:1}
.hmPodium.show{opacity:1}
/* --- "Play a mood" relocated into the hero CTA row (moodInCta) --- */
body.moodInCta .heroCtas{align-items:center}
body.moodInCta .faceMoodCorner{position:relative;right:auto;left:auto;top:auto;bottom:auto;transform:none;z-index:auto;display:inline-flex}   /* z-index:auto (not a stacking context) so the little heads (z3) pass IN FRONT of the button, while the open menu can still float above them */
body.moodInCta .faceMoodCorner .moodBtn{position:relative;border:1px solid var(--c100)!important;background:var(--c50)!important;color:var(--c950);font-weight:400;font-size:var(--fs-body);padding:var(--sp-8) var(--sp-16);border-radius:var(--r-2xs)!important;opacity:1!important}
body.moodInCta .faceMoodCorner .moodBtn:hover{border-color:var(--c500)!important;color:var(--c950)}
body.moodInCta .faceMoodCorner .moodLbl{opacity:1!important;transform:none!important}
body.moodInCta .faceMoodCorner .moodCar{opacity:1!important;transform:rotate(0deg)!important;transition:transform .32s cubic-bezier(.34,1.4,.5,1),color var(--dur-state) var(--ease-out)}   /* menu opens UP in the CTA -> caret points up when closed */
body.moodInCta .faceMoodCorner .moodbar.open .moodCar{transform:rotate(180deg)!important}   /* ...and flips to point down when the menu is open */
body.moodInCta .faceMoodCorner .moodMenu{bottom:calc(100% + 12px)!important;top:auto!important;left:0!important;right:auto!important;z-index:60!important}
body.moodInCta .heroCtas:not(.in) .faceMoodCorner{opacity:0!important;visibility:hidden!important;pointer-events:none!important}   /* stay hidden through the intro until the CTA row reveals -- !important so it wins over the mobile force-visible rule too, keeping Play in lockstep with "View my work" on every viewport */
@media(max-width:760px){body.moodInCta .faceMoodCorner{position:relative!important;right:auto!important;left:auto!important;top:auto!important;bottom:auto!important;transform:none!important}}   /* keep it inline in the CTA row on mobile (override the corner media query) */
.hmCount.hmCountPulse{animation:hmCountP .78s var(--ease-out) forwards}
@keyframes hmCountP{0%{opacity:0;transform:translate(-50%,-50%) scale(1.35)}18%{opacity:1}70%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-50%) scale(.86)}}
@media(prefers-reduced-motion:reduce){.hmCount.hmCountPulse{animation:none;opacity:1}}
.hmHp{position:absolute;left:0;top:0;width:44px;height:4px;border-radius:var(--r-hair);background:rgba(8,8,8,.13);z-index:4;pointer-events:none;opacity:0;transition:opacity .3s var(--ease-out)}
.hmHp i{display:block;height:100%;border-radius:var(--r-hair);background:var(--theme-ink);transition:width var(--dur-state) var(--ease-out),background-color .3s}
/* ===== mood shortcut menu — triggers the head animations; sharp, Instrument Sans, 8fps stepped ===== */
.moodbar{position:relative;display:inline-block;z-index:45}
@keyframes moodIn{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}
.moodBtn:not(.ctl){display:inline-flex;align-items:center;gap:var(--sp-8);background:var(--ctl-ground);border:1px solid var(--theme-rim);border-radius:var(--r-2xs);padding:var(--sp-8) var(--sp-16);line-height:1;cursor:pointer;font-family:var(--sans);font-weight:400;font-size:var(--fs-body);color:var(--ctl-ink-strong);transition:border-color var(--dur-state) var(--ease-out),color var(--dur-state) var(--ease-out)}.moodBtn:not(.ctl):hover{border-color:var(--theme-rim-strong)}
.moodLbl{font-size:var(--fs-body);font-weight:400;letter-spacing:0}
.moodCar{width:12px;height:12px;display:inline-block;flex:0 0 auto;color:var(--ctl-ink);transition:transform var(--dur-state) var(--ease-out),color var(--dur-state) var(--ease-out)}
.moodbar.open .moodCar{width:12px;height:12px;display:inline-block;flex:0 0 auto;color:var(--ctl-ink);transition:transform var(--dur-state) var(--ease-out),color var(--dur-state) var(--ease-out)}
.moodMenu:not(.ctl-menu){position:absolute;min-width:var(--menu-w);background:var(--ctl-menu-ground);border:1px solid var(--theme-rim);border-radius:var(--r-2xs);padding:var(--sp-8);box-shadow:var(--ctl-container-rim);opacity:0;visibility:hidden;transition:opacity var(--ease-out-dur) var(--ease-out),transform var(--dur-state) var(--ease-out),visibility 0s var(--dur-state)}
.moodbar.open .moodMenu:not(.ctl-menu){opacity:1;visibility:visible;transition:opacity var(--ease-out-dur) var(--ease-out),transform var(--dur-state) var(--ease-out)}
.moodItem:not(.ctl){display:flex;align-items:center;gap:var(--sp-12);width:100%;padding:var(--sp-8) var(--sp-12);border-radius:var(--r-2xs);font-family:var(--sans);font-weight:400;font-size:var(--fs-body);color:var(--ctl-ink-strong);background:none;border:0;cursor:pointer;text-align:left;transition:background-color var(--dur-state) var(--ease-out),color var(--dur-state) var(--ease-out)}
.moodbar.open .moodItem:not(.ctl){display:flex;align-items:center;gap:var(--sp-12);width:100%;padding:var(--sp-8) var(--sp-12);border-radius:var(--r-2xs);font-family:var(--sans);font-weight:400;font-size:var(--fs-body);color:var(--ctl-ink-strong);background:none;border:0;cursor:pointer;text-align:left;transition:background-color var(--dur-state) var(--ease-out),color var(--dur-state) var(--ease-out)}
/* 8fps staggered reveal — mirrors the site's 0.05s per-character cascade */
.moodbar.open .moodItem:not(.ctl):nth-child(1){transition-delay:.03s,.03s,0s}
.moodbar.open .moodItem:not(.ctl):nth-child(2){transition-delay:.08s,.08s,0s}
.moodbar.open .moodItem:not(.ctl):nth-child(3){transition-delay:.13s,.13s,0s}
.moodbar.open .moodItem:not(.ctl):nth-child(4){transition-delay:.18s,.18s,0s}

/* ---- 5 + 6. MOBILE: a running game owns the screen.
   All three games take their height from hero.clientHeight -- race heroH(), lava hh(), soccer
   geo(). They take WIDTH from innerWidth, but height never got the same treatment, because on
   desktop .hero{min-height:calc(100vh - 80px)} made the two nearly identical. On mobile .hero is
   overridden to min-height:auto, so the arena measured 491px of an 844px viewport: every game
   played in the top 58% of the screen, the lava plane clipped at a hard horizontal line, and
   heads/rungs/health bars rendered over the nav. Hand the hero the rest of the screen while a
   game runs and all three inherit a real arena -- both height fns are called live, not cached.
   Scroll is locked for the duration: a single 500px flick used to push the whole match off
   screen, still running, with no way back.

   PLAY.HTML DIVERGES HERE: .hero is always the arena, so mobile keeps its explicit 65vh
   resting band. The page now has a footer below that first viewport, which means hmFull can
   no longer mean "lock Play forever". Only the full-screen team picker or an active game locks
   scroll; the resting page is a normal document centred by .playViewport in play.html. */
@media(max-width:760px){body.hmFull .hero{height:65vh;min-height:0}}
body.hmFull{height:auto;min-height:100%;overflow-x:clip;overflow-y:auto;overscroll-behavior-y:auto}
body.hmFull.playViewportOwned{height:100%;overflow:hidden;overscroll-behavior:none}
body.playViewportOwned .siteFoot{display:none}
/* ---- 14. #soccerTeams measured 39x40 -- under the 44px minimum (WCAG 2.5.5 AAA, Apple HIG
   "at least 44x44 pt"). It sits inside a flex row with align-self:stretch, so it inherited the
   40px moodItem height. Floor both axes. ---- */
.moodTeamsBtn{min-width:44px;min-height:44px}
/* ---- THE GAMES SPEAK IN THE HERO'S VOICE ----------------------------------------------
   The scoreboard was a bordered chip set in --fs-body, and the countdown/messages ran up to
   140px. Jayden wants the games to read like the rest of the site, and the site's voice is the
   h1. So the scoreboard becomes a HEADLINE: the word "Soccer" set exactly like the hero line,
   with the ball sitting after it the way the cookie sits after "hunger" -- same .62em, same
   baseline as .camDot/.heartDot/.discoDot. No card, no border, no fill.
   The ball in the title is the REAL ball: it drops from here at every kickoff and after every
   goal (ballHome()), and the title copy hides while it is on the pitch so there is only ever
   one ball on screen.
   Team identity moves from two coloured dots onto the numerals themselves -- same information,
   one less object, and it matches the red/blue rings already on the players' heads. ---- */
.hmScore{border:0;background:none;padding:0;filter:url(#inkSm);
 flex-direction:column;align-items:center;text-align:center;
 top:var(--heroTitleTop,40px);gap:clamp(var(--sp-2),.6vh,var(--sp-6));
 font-family:var(--sans);font-weight:600;font-size:var(--fs-h1);line-height:1.06;letter-spacing:-.015em;color:var(--theme-ink)}
.hmScore .sTitle{display:inline-flex;align-items:center;white-space:nowrap}
/* The ball rests in the title at the size Jayden liked (.62em, the .discoDot metric), and the
   drop is a SCALE TWEEN on the live ball rather than a swap between two images -- it used to read
   as "one png goes in and then another". On the way out the ball collapses its WIDTH as well as
   its opacity, so "Soccer" glides back to true centre using the same eased min-width move as the
   hero cycling word, instead of sitting off-centre around an invisible gap. */
/* The ball, not the lockup, is centred while it is in the title -- it drops straight down, so
   wherever it sits IS the kickoff spot. Centring "Soccer + ball" as a group put the ball 64.6px
   right of the pitch centre on desktop and 48.4px on mobile (a quarter of the way to the right
   goal), handing blue the kickoff every time. --sShift is measured in JS because the offset is
   half the word width, which moves with the responsive type. Once the ball has fallen the shift
   is dropped and the word glides back to true centre. */
.hmScore .sTitle{gap:0}
.hmScore .sBall{width:.62em;height:.62em;margin-left:.2em;object-fit:contain;flex:0 0 auto;
 transition:opacity var(--ease-out-dur) var(--ease-out),width var(--dur-reveal) var(--ease-out),margin-left var(--dur-reveal) var(--ease-out)}
/* Two stages, deliberately. ballOut only hides the ball; the SLOT stays, so during the frozen
   countdown the title still reserves its space and the live ball sits exactly in it. Collapsing
   the slot immediately re-centred "Soccer" while the ball was still parked there, sliding the word
   under it -- measured 7.9px of overlap. ballFell lands once the ball has actually dropped clear,
   so the word glides back into space the ball has already left. */
.hmScore.ballOut .sBall{opacity:0}
.hmScore.ballOut.ballFell .sBall{width:0;margin-left:0}
@media(prefers-reduced-motion:reduce){.hmScore .sBall{transition:opacity var(--ease-out-dur) linear}}
/* "First to N" and End still carried the border-left dividers from when this was a horizontal
   chip; stacked underneath the score they read as debris. Quiet row, no rules. */
.hmScore .sMeta{display:inline-flex;align-items:center;gap:clamp(var(--sp-14),2.4vw,22px);margin-top:clamp(var(--sp-2),.6vh,var(--sp-6))}
.hmScore .sMeta small{margin:0;padding:0;border:0}
.hmScore .sMeta .hmScoreEnd{margin:0;padding:0;border:0}
.hmScore.ballOut .sBall{opacity:0}
.hmScore .sNums{display:inline-flex;align-items:baseline;gap:.3em;font-variant-numeric:tabular-nums}
.hmScore .sNums .sR{color:var(--tcol1,#e05a4e)}
.hmScore .sNums .sB{color:var(--tcol2,#5aa0d8)}
.hmScore .sNums em{color:var(--c400);font-style:normal;font-weight:400}
.hmScore small{font-family:var(--sans);font-weight:400;font-size:var(--fs-small);color:var(--c500);letter-spacing:0;align-self:center}
.hmScore .hmScoreEnd{align-self:center}

/* Countdown + messages: the 140px shout becomes hero-scale type. */
.hmCount{font-size:clamp(40px,6vw,64px);font-weight:600;letter-spacing:-.02em}
.hmCount.hmMsg{font-size:var(--fs-h1);font-weight:600;letter-spacing:-.015em}
/* ---- Floor is Lava is PARKED, not deleted. Jayden wants attention on soccer and marble; the
   module, its CSS and the whole crate/ability system stay in the file untouched, so bringing it
   back is deleting this one rule. Nothing else references #lavaRow. ---- */
#lavaRow{display:none}
#raceGo{display:none!important}   /* !important because battleGate() writes display:flex INLINE on
   ["battleGo","soccerGo","raceGo"] every tick -- hiding #lavaRow works without it only because that
   targets the ROW, which the gate never touches. Marble Race parked alongside Floor is Lava while
   soccer gets fleshed out; both modules stay in the file, so bringing either back is these rules. */
/* Once the match is decided the rule line and End are moot -- the result is the only thing left
   to say, and it is announced in the middle of the screen. */
.hmScore.won .sMeta{opacity:0;pointer-events:none;transition:opacity .28s var(--ease-out)}
/* ---- TOURNAMENT PANEL. A bracket TREE cannot collapse vertically in CSS -- that is a known,
   documented dead end -- and at 8 teams this is only 3 rounds and 7 fixtures. So the bracket is a
   round-grouped vertical LIST: no connector lines, no horizontal scroll, and it reads correctly to
   a screen reader because it is genuinely just headings and rows. Type-led, same as the rest of
   the site. Leaderboards carry a face, a team dot, the rating and one number -- at 390px there is
   room for a name column plus about three numerics, and that is the whole budget. ---- */
/* Broadcast digits: Archivo, tabular, condensed. The display face touches numerals
   and moments ONLY -- names, labels and UI stay Instrument Sans 400/600. */
.bcNum{font-family:'Archivo',var(--sans);font-variant-numeric:tabular-nums lining-nums;
  font-variation-settings:'wdth' 68;font-weight:800;letter-spacing:.01em}
/* ---- MATERIALS. Two shadows one light (down-right, matching the head photos --
   verify direction against a head cutout before changing), one grain layer per
   composite, jitter breaks symmetry. Jitter is transform-only: filter on a
   container would create a stacking context and break future 3D flap cells. */
:root{--bc-contact:0 1px 0 rgba(24,20,12,.5);--bc-cast:6px 10px 22px rgba(24,20,12,.26)}
.bcPlate{box-shadow:var(--bc-contact),var(--bc-cast)}
.bcGrain{position:absolute;inset:0;z-index:9;pointer-events:none;opacity:.06;
  mix-blend-mode:soft-light;background-repeat:repeat}
/* ---- SPLIT-FLAP. The one mechanical component; score digits only. Four layers
   per cell: static top (current char upper half), static bottom (NEXT char lower
   half), flip-front (old char upper), flip-back (new char lower). Spin-through:
   always forward through the drum, never backward, never a direct jump. NO
   ancestor of a cell may carry filter (3D transform stacking trap).

   THE em RADII BELOW ARE A SANCTIONED EXEMPTION, NOT AN OVERSIGHT. Design-tokens
   sec 1.4 exempts split-flap cells from the radius ladder because they scale with
   the flap's own type size: a cell is .72em x 1.16em, so a fixed px radius would
   be a different proportion at every score size. Measured at 1440 the cell is
   29 x 46 and .06em resolves to 2.4px -- one rung below --r-hair, and correct.
   No corner-shape either: at 2.4px a superellipse is sub-pixel, and this is the
   one component where an extra paint property on a rotateX'd layer is not worth
   a change nobody can see. If a later sweep "tokenises" these, it will make the
   digits wrong at every size but the default one. */
.bcCell{position:relative;display:inline-block;width:.72em;height:1.16em;
  border-radius:.06em;background:var(--cupStock,#f1ece0);color:#221f18;
  box-shadow:var(--bc-contact),var(--bc-cast);perspective:5em;
  text-align:center;vertical-align:baseline}
.bcCell+.bcCell{margin-left:.06em}
.bcCell i{position:absolute;left:0;right:0;overflow:hidden;font-style:normal;
  line-height:1.16em;pointer-events:none}
.bcCell .bcT{top:0;height:.58em}
.bcCell .bcB{bottom:0;height:.58em}
.bcCell .bcB span{position:relative;top:-.58em;display:block}
/* seam: shadow above the split, highlight below, 1px misregistration */
.bcCell::before{content:"";position:absolute;left:0;right:0;top:.57em;height:2px;
  background:linear-gradient(rgba(24,20,12,.34),rgba(255,255,255,.28));z-index:4;
  transform:translateY(.5px)}
/* pin ticks */
.bcCell::after{content:"";position:absolute;top:.5em;bottom:.5em;left:-.045em;right:-.045em;
  background:linear-gradient(90deg,rgba(24,20,12,.5) .045em,transparent .045em,
    transparent calc(100% - .045em),rgba(24,20,12,.5) calc(100% - .045em));
  border-radius:.03em;z-index:0}
.bcCell .bcFlip{position:absolute;left:0;right:0;top:0;height:.58em;z-index:3;
  transform-origin:bottom center;backface-visibility:hidden;overflow:hidden;
  background:var(--cupStock,#f1ece0);border-radius:.06em .06em 0 0;
  line-height:1.16em;font-style:normal;display:none}
.bcCell.bcGo .bcFlip{display:block;animation:bcFlipDown .15s ease-in forwards}
@keyframes bcFlipDown{from{transform:rotateX(0)}to{transform:rotateX(-88deg)}}
@media (prefers-reduced-motion: reduce){.bcCell.bcGo .bcFlip{animation:none;display:none}}
/* ---- MOBILE MENU ------------------------------------------------------------------
   This is a dropdown anchored under the burger, not the full-height edge drawer it used to
   be. The old drawer rules were left behind when it was redesigned and the two fought: the
   close button came back to life (the button-unification pass re-declared it
   display:inline-flex, beating the display:none that the dropdown wanted) and sat
   align-self:flex-end on a negative margin, which is the empty band across the top.
   A dropdown closes by clicking the burger or outside it, so the X goes -- for good. */
body .navDrawer .navDrawerX{display:none!important}
/* One padding for every row. Work and About me were taking 12px 16px from the shared button
   base while Contact took 8px 12px from .navDrawer .ndTalk, which outranks it -- so one row
   sat 4px left of the other two. */
body .navDrawer .ndLink{padding:var(--sp-8) var(--sp-12);text-align:left}

/* ================= LIVE SCOREBOARD =================
   Started as a faithful translation of SportyBlocks' Scoreboard1, then a materials rebuild
   (painted plate, split-flap Archivo digits, grain, --bc-contact/--bc-cast light + shadow)
   that kept Scoreboard1's vertical-stack LAYOUT. Superseded again 2026-07-30 (Jayden's call --
   see docs/superpowers/specs/2026-07-30-tournament-broadcast-design.md, Scoreboard section):
   the layout is now a single horizontal bar, radically simplified -- per side exactly head,
   name, one colour signal (the head's own team-colour ring), flap digit. Nothing else. The
   MATERIAL -- paint, grain, jitter, shadows, radius -- is unchanged by this pass. */
/* ---- THE PLATE'S CORNER. Jayden: "can you make sure the corner roundness and
   squircle for the scoreboard is tokened."

   Measured live, at 1440x900, during a real fixture: the plate is 367 x 119 and
   its radius was ALREADY --r-sm (10) -- on the ladder, just missing the other
   half of the site's corner language. corner-shape is what every other rounded
   surface on the site carries (header.css:303/387/692/728, about.html:121/180/265)
   and the plate did not, so it was the one object still drawing a plain circular
   arc while the chrome around it drew a superellipse. That is the whole change
   here: no radius moves, nothing else is touched.

   WHY NOT --r-lg (20), which is the rung for "cards and images": the plate is
   119px tall, so 20 is a third of its height and reads as a lozenge rather than
   a bar -- and Jayden has twice said the scoreboard is the thing he likes. 10 is
   a real rung, it is the right size class for a dense horizontal bar, and moving
   it would be a redesign. Flagged rather than taken. ---- */
.hmScore .sbCard{box-sizing:border-box;width:max-content;max-width:94vw;
 border-radius:var(--r-sm);corner-shape:var(--corner);
 background:var(--sb-material,var(--cupPaint,#2b2f2a));padding:0;
 box-shadow:var(--sb-rest-shadow,var(--bc-contact),var(--bc-cast),inset 0 0 46px rgba(0,0,0,.30));
 font-family:var(--sans);color:#ece9dd;position:relative}
/* THE TROPHY rides on top of the board for the FINAL only, then is handed over at the whistle.
   Standing the prize on the scoreboard before it is won is the sourced move -- CS Majors put
   the physical trophy on a pedestal in front of the players during playoffs, NBA Cup courts
   put it huge at centre court. It is the cheapest possible anticipation beat: the thing being
   played for is visible while it is still being contested. */
.sbTrophy{position:absolute;left:50%;bottom:calc(100% - 10px);transform:translateX(-50%);
 width:clamp(46px,7vw,66px);height:auto;pointer-events:none;z-index:2;display:none;
 filter:drop-shadow(0 6px 14px rgba(15,23,42,.26))}
body.hmFinal .sbTrophy{display:block;animation:sbTrophyIn var(--dur-enter) var(--ease-out) both}
@keyframes sbTrophyIn{from{opacity:0;transform:translateX(-50%) translateY(10px) scale(.9)}
 to{opacity:1;transform:translateX(-50%) translateY(0) scale(1)}}
/* Handed over: lifted out of the board and flown to the winner, then it rides with them.
   OFF TO THE SIDE, never centred on the face -- the head is the subject and the eyes and brow
   are the part worth never covering. */
.hmTrophyFly{position:fixed;z-index:66;width:clamp(46px,7vw,66px);height:auto;
 pointer-events:none;filter:drop-shadow(0 8px 18px rgba(15,23,42,.32))}
@media(prefers-reduced-motion:reduce){body.hmFinal .sbTrophy{animation:none}}

/* SCOREBOARD -- single horizontal bar, content-hugging pill. Refined 2026-07-30 (Jayden,
   second pass): "more compact and minimal and balanced... it doesn't look like it's in the
   middle and there is a lot of unnecessary spacing." Root cause of the off-centre read: the
   old flex row used justify-content:space-between with unequal name widths, so the digit
   group drifted wherever the names happened to push it. Fixed with a symmetric 3-column grid
   -- 1fr auto 1fr -- rather than flex: two EQUAL-fr side columns each size to the WIDER
   side's content (the grid "find the size of an fr" algorithm), so the dash in the auto
   centre column always sits at true bar-centre no matter how the two names differ in length.
   Each side is its own flex row, pulled TOWARD the dash with justify-content (flex-end on the
   left, flex-start on the right) so the shorter name's slack collapses toward the middle
   instead of toward the outer edge. .sbCard itself is width:max-content -- the bar hugs its
   content instead of claiming a fixed panel width. */
.sbInner{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:var(--sp-12);
 padding:var(--sp-12) var(--sp-14)}
.sbTeam{display:flex;align-items:center;gap:var(--sp-12);min-width:0}
.sbInner>.sbTeam:first-child{justify-content:flex-end}
.sbInner>.sbTeam:last-child{justify-content:flex-start}
/* --tc is the bare "r,g,b" triplet (see the rgba(var(--tc),a) convention documented at the
   top of the file) -- wrapping it in rgb() is required, var(--tc) alone is not a colour. */
.sbNm{color:#ece9dd;font-weight:600;font-size:var(--fs-label);letter-spacing:.09em;
 text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
 min-width:0;flex:1 1 auto}
.sbScore{font-size:40px;display:flex;align-items:center;flex:0 0 auto}
/* dim divider glyph only -- no rule lines, no "multiple colour lines and all that" */
.sbVs{flex:0 0 auto;color:#77746a;font-size:20px;font-weight:600;line-height:1;padding:0 var(--sp-2)}
/* Mobile is the acceptance bar for this layout -- it previously failed here at a bigger scale
   (that's why it was a stack before); the simplification is what makes one row viable: faces
   shrink, names ellipsize, nothing needs to wrap. Scoped, never bare. */
@media(max-width:480px){
 .hmScore:has(.sbCard) .sbInner{gap:var(--sp-8);padding:var(--sp-10) var(--sp-12)}
 .hmScore:has(.sbCard) .sbTeam{gap:var(--sp-8)}
 .hmScore:has(.sbCard) .sbFace{flex:0 0 24px;width:24px;height:24px}
 .hmScore:has(.sbCard) .sbDot{flex:0 0 20px;width:20px;height:20px}
 /* MOBILE AUDIT P3. These were 10.5px for the team names and 10px for the
    "First to 5" line -- and this bar is the ONLY chrome on screen during a
    match, so the smallest type on the page was carrying the one thing you are
    watching. The type goes up, the FACES come down to pay for it: 24 -> 22px,
    which is where the row's width budget actually is. Archivo stays; the
    typeface was never the problem. --fs-caption 12px is the floor the site
    already accepts for a caption, and a scoreboard label is a caption. */
 .hmScore:has(.sbCard) .sbFace{flex:0 0 22px;width:22px;height:22px}
 .hmScore:has(.sbCard) .sbNm{font-size:var(--fs-caption);letter-spacing:.05em}
 .hmScore:has(.sbCard) .sbScore{font-size:28px}
 .hmScore:has(.sbCard) .sbVs{font-size:var(--fs-small)}
}


/* No square at all. The outline traces the HEAD, the way it does on the pitch -- a box, even
   an empty one, draws a rectangle around a shape that is not a rectangle. Four offsets rather
   than a stack: CSS filters CHAIN, so a long stack compounds the thickness instead of adding
   to it, and at 32px one pixel is the whole line. */
.sbFace{flex:0 0 30px;width:30px;height:30px;display:inline-flex;align-items:center;
 justify-content:center;overflow:visible;background:none}
.sbFace img{filter:
  drop-shadow(1px 0 0 rgb(var(--tc,148,163,184)))
  drop-shadow(-1px 0 0 rgb(var(--tc,148,163,184)))
  drop-shadow(0 1px 0 rgb(var(--tc,148,163,184)))
  drop-shadow(0 -1px 0 rgb(var(--tc,148,163,184)))}
/* THE GOAL FLASH. The board lights up in the scorer's colour for a beat, then settles.
   A ring plus a soft bloom rather than a fill, so the score stays readable through it --
   the number is the thing you are looking at when a goal goes in. */
.hmScore .sbCard{transition:box-shadow var(--dur-enter) var(--ease-out)}
.hmScore.sbHit .sbCard{
 box-shadow:0 0 0 3px rgb(var(--sbFlash,148,163,184)),
            0 0 44px -4px rgba(var(--sbFlash,148,163,184),.72);
 animation:sbFlashPulse .42s var(--ease-out) 3}
@keyframes sbFlashPulse{
 0%{box-shadow:0 0 0 0 rgb(var(--sbFlash,148,163,184)),0 0 0 0 rgba(var(--sbFlash,148,163,184),0)}
 22%{box-shadow:0 0 0 4px rgb(var(--sbFlash,148,163,184)),0 0 54px 2px rgba(var(--sbFlash,148,163,184),.85)}
 100%{box-shadow:0 0 0 0 rgba(var(--sbFlash,148,163,184),0),0 0 0 0 rgba(var(--sbFlash,148,163,184),0)}}
@media(prefers-reduced-motion:reduce){.hmScore.sbHit .sbCard{animation:none}}
.sbFace img{max-width:100%;max-height:100%;width:auto;height:auto;display:block}
.sbDot{flex:0 0 22px;width:22px;height:22px;border-radius:var(--r-full);background:rgb(var(--tc,230,230,230))}

/* Leader full ink, trailer dimmed -- the digit AND the name. board2() toggles .sbTrail on the
   .sbScore element (unchanged from before the rebuild), which is a descendant of the digit
   but a SIBLING of .sbNm, not its ancestor -- so the name needs the :has() route back up to
   the shared .sbTeam row rather than a plain descendant selector. */
.sbTrail .bcCell{opacity:.62}
.sbTeam:has(.sbTrail) .sbNm{opacity:.62}
/* clutch: the plate whose NEXT goal ends the match takes a thin emerald edge -- match point,
   named on the plate itself rather than in a shout that scrolls past and is gone. */
.sbClutch .bcCell{box-shadow:var(--bc-contact),var(--bc-cast),
 0 0 0 1.5px rgba(23,164,90,.85),0 0 12px rgba(23,164,90,.35)}




/* The round name, on the ledge. Sits left of the win condition and separated from it by the
   same hairline the VS divider uses, so it reads as a second field rather than a badge. */
/* nowrap on BOTH fields. The ledge is a flex row on a 340px card, so a long round name
   ("Quarter-final") wrapped to a second line and shoved the ledge taller, while a short one
   ("Final") never did -- the row changed height depending on which round you were in.
   Neither field may wrap; the label sheds its letter-spacing before it sheds a line. */
.sbRound{font-weight:600;font-size:var(--fs-micro);line-height:1;letter-spacing:.14em;
 text-transform:uppercase;color:#b9b6a8;white-space:nowrap;flex:0 0 auto;
 padding-right:9px;margin-right:1px;border-right:1px solid rgba(236,233,221,.14)}
.sbFoot .sbMeta{white-space:nowrap;flex:0 0 auto}
/* #9E6C00 read fine as ink-on-light-card gold; on the painted plate it is dark-on-dark and
   fails contrast, so the final's label takes a brighter warm gold instead -- same idea
   (the round that matters gets its own colour), tuned for the new material. */
body.hmFinal .sbRound{color:#e8b347}
.sbFoot{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-8);
 padding:7px var(--sp-14);font-weight:600;font-size:10px;line-height:1.1;color:#b9b6a8;
 /* The card owns its own 10px radius and the ledge no longer paints its own background --
    it sits directly on the plate now, so it has no corners of its own left to carry. The
    ledge's own horizontal padding matches .sbInner's (14px) -- round·rule sits at the same
    inset as the head above it, End at the same inset as the far head, so the two rows read as
    one balanced object rather than two independently-margined bars. Width follows the card's
    hugged width automatically; nothing here sets it explicitly. */
 border-top:1px solid rgba(236,233,221,.14)}
.sbMeta{display:inline-flex;align-items:center;gap:var(--sp-6)}
.sbMeta .sBall{width:14px;height:14px;object-fit:contain}
/* THE ONE OFF-LADDER VALUE ON THE BOARD, and it was a raw 8px. Measured: the
   control is 61 x 25, and its hover wash is the only thing that ever paints the
   corner. --r-xs (6) is the nearest rung -- a 2px change on a hover-only fill.

   NOT --r-md (14), which is the rung for controls: on a 25px-tall box that is
   56% of the height and turns the chip into a pill, which is a visible change to
   a board Jayden likes. The size class that governs here is "small part", not
   "control", because the box is a third of a normal control's height.

   Concentric also points down rather than up: the chip sits inside a plate whose
   corner is 10, inset 14px horizontally and 7px vertically by .sbFoot's padding,
   so an inner corner must be SMALLER than 10 -- 6 is, 8 was closer to the
   parent's own radius than the nesting allows. */
.hmScore .hmScoreEnd{color:#b9b6a8;font-weight:600;font-size:var(--fs-label);padding:var(--sp-6) var(--sp-8);min-height:0;
 border-radius:var(--r-xs);corner-shape:var(--corner);
 transition:color var(--ease-out-dur) var(--ease-out),background-color var(--ease-out-dur) var(--ease-out)}
.hmScore .hmScoreEnd:hover,.hmScore .hmScoreEnd:focus-visible{color:#ece9dd;background:rgba(236,233,221,.12)}
.hmScore .hmScoreEnd svg{width:13px;height:13px;stroke-width:1.6;fill:none}
/* THE BOARD IS A PRINTED OBJECT: matte painted plane, light plates, no ink filter -- an ink
   filter here would ALSO break the 3D flap cells (filter on an ancestor creates a stacking
   context that flattens their rotateX). Cup identity paints the plate during tournaments;
   the defaults above are the plain-soccer board. */
.hmScore:has(.sbCard){width:auto;align-items:center;gap:0;filter:none}

/* NIGHT SCORE MATERIAL. The backing light belongs to the board alone: .hmScore's
   existing transform makes a local stacking context, and isolation keeps the
   negative layer behind the card without letting it fall behind the arena.
   Resting shadow is a variable so .sbHit's authored goal/clutch/trophy box-shadow
   declarations remain the winning properties during play. */
:root[data-theme="dark"] body[data-theme-page="play"]{
 --sb-material:rgba(17,19,24,.90);
 --sb-rest-shadow:0 16px 44px -28px rgba(0,0,0,.88),
  inset 0 0 0 var(--hair-w) rgba(255,255,255,.15),
  inset 0 1px 0 rgba(255,255,255,.08),
  inset 0 0 46px rgba(0,0,0,.34);
}
:root[data-theme="dark"] body[data-theme-page="play"] .hmScore:has(.sbCard){isolation:isolate}
:root[data-theme="dark"] body[data-theme-page="play"] .hmScore:has(.sbCard)::before{
 content:"";position:absolute;inset:-28px -44px;z-index:-1;pointer-events:none;
 background:radial-gradient(ellipse at 50% 55%,rgba(233,232,255,.28),rgba(103,99,228,.13) 42%,transparent 72%);
 filter:blur(12px)
}


/* Live shared control selectors, rewritten from the portfolio's long selector
   lists (index.html 1284-1456) down to the four that are actually reachable
   on play.html: .tGo, .hmBtn, .hmScoreEnd, .sbBtn.

   THESE FOUR NOW READ --ctl-*, NOT THE RAW --c* RAMP, and that is the fix for
   the one measured contrast failure in Night. The --c* ramp is NOT rebound by
   the dark theme on this page -- only builder-theme.css rebinds it, and only
   for the two builder pages -- so `color:var(--c700)` stayed #525252 at night
   and the End control inside .battleCount measured 2.50:1 against the
   #111318 it sits on (pixel-sampled, not composited: 4.5 needed). Every
   --ctl-* below resolves through --theme-*, so one declaration is now correct
   in all six time-of-day states instead of one being correct in five.
   The PRIMARY pair matters as much as the ink. --c950 on --c50 painted a
   #121212 button on a #0B0C0F page at night: 1.05:1 of ground against ground,
   i.e. a primary action with no visible edge at all. --ctl-primary-ground
   inverts with the theme, which is what every other primary on the site
   already does. Its hover moves to --ctl-primary-ground-hover rather than
   --c900 so the game bar breathes on the same rung as the rest of the ladder. */
.tGo,.hmScoreEnd,.hmBtn{
 font-family:var(--sans);font-weight:400;font-size:var(--fs-small);line-height:1;
 color:var(--ctl-ink);background:none;
 border:0;
 border-radius:var(--r-2xs);
 padding:var(--sp-12) var(--sp-16);min-height:44px;box-sizing:border-box;
 align-items:center;justify-content:center;gap:var(--sp-8);
 cursor:pointer;
 transition:background-color var(--ease-out-dur) var(--ease-out),color var(--ease-out-dur) var(--ease-out);
}
.sbBtn{
 font-family:var(--sans);font-size:var(--fs-small);color:var(--ctl-ink);
 background:none;border:0;border-radius:var(--r-2xs);
 width:44px;height:44px;min-height:44px;padding:0;
 display:inline-flex;align-items:center;justify-content:center;
 cursor:pointer;
 transition:background-color var(--ease-out-dur) var(--ease-out),color var(--ease-out-dur) var(--ease-out);
}
:where(.tGo,.hmScoreEnd,.hmBtn){display:inline-flex}
.tGo:hover,.hmScoreEnd:hover,.hmBtn:hover,.sbBtn:hover,
.tGo:focus-visible,.hmScoreEnd:focus-visible,.hmBtn:focus-visible,.sbBtn:focus-visible{
 background:var(--ctl-ground-hover);color:var(--ctl-ink-strong);border-color:transparent;
}
.tGo:not(.tGoQuiet),.hmBtn.hmBtnPrimary{
 background:var(--ctl-primary-ground);color:var(--ctl-primary-ink);
}
.tGo:not(.tGoQuiet):hover,.hmBtn.hmBtnPrimary:hover{
 background:var(--ctl-primary-ground-hover);color:var(--ctl-primary-ink);
}
.tGo svg,.hmBtn svg{color:currentColor}

/* .playBack is retired. It was a one-off chip invented for this page because
   play.css carried no rule for it at all; the site header now covers the same job
   on all nine pages, as a centred fixed pill inside a pointer-events:none wrapper
   so it never eats a tap meant for the pitch. See header.css / header.js. */
/* The header's tokens are no longer duplicated here: the flagged top-up block that used
   to sit at this spot (radius / materials / motion / the twelve gap tokens, plus the
   @media(max-width:760px) blur zeroing) came out when play.html linked tokens.css.
   tokens.css carries every one of those, byte for byte, and its own 760px rule. */



/* ═══ MOBILE AUDIT B6 / P7 — the delete stops sitting on the head ═════════════
   .mhX was position:absolute at top:-6/right:-6 on a ~45px thumbnail: 16px, or
   26 under (pointer:coarse), overlapping its own .mhPick by up to 37x37. A
   destructive control inside a benign one's hit area. Its ::before target made
   it worse by expanding further into the thumbnail.
   The tile becomes two rows -- head, then delete -- and .mhX goes static, so the
   two controls cannot intersect by construction. Same fix, same shape and the
   same reasoning as index.html's Play panel; the two rosters are one component
   rendered in two places and they must not diverge again. */
.mhItem{display:grid;grid-template-rows:auto auto;gap:var(--sp-2)}
.mhX{position:relative;top:auto;right:auto;
 width:100%;height:var(--sp-24);min-width:0;min-height:0;padding:0;
 border:0;background:none;color:var(--ctl-ink);
 border-radius:var(--r-2xs);corner-shape:var(--corner);
 display:flex;align-items:center;justify-content:center;
 font-family:var(--sans);font-size:var(--fs-micro);line-height:1;cursor:pointer}
.mhX::before{content:"";position:absolute;left:0;right:0;top:0;
 height:var(--tap-min);border-radius:0}
.mhX:hover,.mhX:focus-visible{color:var(--ctl-ink-strong);border-color:transparent}
@media(pointer:coarse){.mhX{width:100%;height:var(--sp-28);top:auto;right:auto;
 font-size:var(--fs-label)}}
