:root{
  --surface: #18181c;
  --surface-2: #1c1c21;
  --input-bg: #0c0c0e;
  --border: #2a2a2e;
  --border-soft: rgba(255,255,255,.08);
  --text-soft: #9a9aa2;
  --text-faint: #6c6c74;
  --danger: #e05555;
  --radius-s: 8px;
  --radius-m: 12px;
}
*{ box-sizing: border-box; }
@media (prefers-reduced-motion: reduce){
  *{ transition: none !important; animation: none !important; scroll-behavior: auto !important; }
  /* These only become visible through their entrance animation, which is off here. */
  .winner-announce-card{ opacity:1; }
}

.hidden{ display:none !important; }

body{
  overflow-x:hidden; min-height:100vh; display:flex; align-items:center; justify-content:center;
  background:var(--ink); transition:padding-left .25s ease;
}
body:not(.live-mode){ padding-left:360px; }
@media (max-width:900px){ body:not(.live-mode){ padding-left:0; } }
/* Live mode goes edge-to-edge for capture; the stage is otherwise always
   sized to the available space (dock included) with a small margin, so
   opening/closing the settings panel never makes everything jump size —
   same fix already applied to the other three games. */
body.live-mode .stage{ margin:0; max-width:100%; width:100vw; height:100vh; height:100dvh; border-radius:0; }

.stage{
  position:relative; overflow:hidden;
  width:calc(100% - 48px); height:calc(100vh - 48px); margin:24px;
  display:flex; flex-direction:column; gap:10px;
  padding:56px 22px 6px;
  /* Open-air route sky instead of a flat brand-color card — the grass track
     below reads as "standing in it", not just a UI panel. */
  background: linear-gradient(180deg, #6ec3ea 0%, #a6ddf2 32%, #cdeccb 58%, #cdeccb 100%);
  border-radius:20px;
  box-shadow:0 20px 50px rgba(0,0,0,.45);
  /* Lets the track floor below tilt away in real 3D space instead of
     staying a flat card — the sky/hills backdrop stays flat behind it,
     the way a painted horizon sits behind a tilted foreground floor in
     these games. */
  perspective:1500px;
}
.watermark{
  position:absolute; top:8px; right:22px;
  height:48px; width:auto;
  opacity:1; pointer-events:none; z-index:5;
  filter:drop-shadow(0 2px 6px rgba(0,0,0,.5));
}

/* --- Scenery: sky band behind the track (sun/clouds drift, hills + treeline
   sit right at the horizon where the grass track begins) — pure CSS shapes,
   no image assets, so it's as dependency-free as the rest of the app. --- */
.scenery{ position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; }
.sun{
  position:absolute; top:14px; right:64px; width:34px; height:34px; border-radius:50%;
  background:radial-gradient(circle at 35% 35%, #fffbe6, #ffe07a 70%);
  box-shadow:0 0 26px 10px rgba(255,224,122,.55);
}
.cloud{
  position:absolute; top:10px; width:52px; height:18px; background:#fff; border-radius:999px;
  opacity:.9; animation:cloudDrift linear infinite;
}
.cloud::before, .cloud::after{
  content:""; position:absolute; background:#fff; border-radius:50%;
}
.cloud::before{ width:26px; height:26px; top:-12px; left:6px; }
.cloud::after{ width:20px; height:20px; top:-8px; left:28px; }
.cloud-1{ left:8%; top:14px; animation-duration:48s; }
.cloud-2{ left:42%; top:6px; transform:scale(.8); animation-duration:60s; animation-delay:-20s; }
.cloud-3{ left:70%; top:20px; transform:scale(.65); animation-duration:40s; animation-delay:-8s; }
@keyframes cloudDrift{
  from{ transform:translateX(0) scale(var(--cs, 1)); }
  to{ transform:translateX(60px) scale(var(--cs, 1)); }
}
/* Hazy, low-saturation ridge further back than the hills, for a sense of
   depth beyond the immediate track — a distant range, not another hill. */
.mountains{
  position:absolute; left:-5%; width:110%; height:38px; top:0; opacity:.55;
  background:linear-gradient(180deg, #b7c4e6, #9fb0da);
  clip-path:polygon(
    0% 100%, 0% 62%, 9% 20%, 18% 58%, 27% 12%, 36% 55%, 45% 5%, 54% 50%,
    63% 22%, 72% 52%, 81% 15%, 90% 48%, 100% 24%, 100% 100%
  );
}
.hills{ position:absolute; left:-5%; width:110%; height:46px; border-radius:100% 100% 0 0 / 100% 100% 0 0; }
.hills-back{ top:14px; background:#8fcf9a; opacity:.75; }
.hills-front{ top:28px; height:34px; background:#7bc487; opacity:.9; }
.treeline{
  position:absolute; left:0; right:0; top:44px; height:16px; background:#4f9c5a;
  clip-path:polygon(
    0% 100%, 0% 40%, 4% 0%, 8% 40%, 12% 10%, 16% 40%, 20% 0%, 24% 40%, 28% 10%, 32% 40%,
    36% 0%, 40% 40%, 44% 10%, 48% 40%, 52% 0%, 56% 40%, 60% 10%, 64% 40%, 68% 0%, 72% 40%,
    76% 10%, 80% 40%, 84% 0%, 88% 40%, 92% 10%, 96% 40%, 100% 0%, 100% 100%
  );
}
/* Small foliage clumps sitting right where the treeline meets the field —
   ties the illustrated horizon into the actual playing field below it. */
.bush{
  position:absolute; top:56px; width:22px; height:13px; border-radius:50% 50% 40% 40%;
  background:#4a9c4f; box-shadow:-7px 2px 0 -1px #5cae60, 7px 2px 0 -1px #427f45;
}
.bush-1{ left:6%; }
.bush-2{ left:29%; width:16px; height:10px; }
.bush-3{ left:61%; width:19px; height:11px; }
.bush-4{ left:85%; width:15px; height:9px; }
/* Two-stroke chevron birds drifting past at their own pace, higher and
   faster than the clouds so the sky reads as having real depth. */
.bird{ position:absolute; left:0; width:16px; height:8px; opacity:.6; animation:birdFly linear infinite; }
.bird::before, .bird::after{
  content:""; position:absolute; top:0; width:9px; height:2px; background:#33404a; border-radius:2px;
}
.bird::before{ left:0; transform:rotate(24deg); transform-origin:right center; }
.bird::after{ right:0; transform:rotate(-24deg); transform-origin:left center; }
.bird-1{ top:20px; animation-duration:24s; }
.bird-2{ top:34px; transform:scale(.75); animation-duration:32s; animation-delay:-14s; }
/* `translate`, not `left` — animating left re-lays-out the element on every
   frame, the one thing in the scenery that wasn't compositor-only. It's the
   standalone property (not `transform`) so it composes with .bird-2's static
   scale. vw instead of the old % because % in translate is the bird's own
   16px width, not the sky's; the stage clips it, so overshooting is free. */
@keyframes birdFly{ from{ translate:-40px 0; } to{ translate:calc(100vw + 40px) 0; } }

/* --- Crowd: fills the flat strip of sky between the horizon (treeline/
   bushes above) and the tilted track's own top edge below — empty space
   otherwise. Looping layers built in JS (buildCrowd()) as two identical
   back-to-back halves, so a plain translateX(-50%) scroll loops with no
   seam: three interleaved rows of fans behind, and a nearly-continuous run
   of sponsor banners in front, low enough that only the crowd's raised arms
   and heads clear the top of the banner rail — the classic "ad boards in
   front of the stands" racing-game read. Lives as a sibling right after
   .track-wrap (not inside .scenery) specifically so .track-wrap.race-started
   can gate it below in plain CSS.

   Deliberately few moving parts: this used to give every one of ~200 fans
   its own bounce and every banner its own sway — 245 animations running at
   once, even on the idle screen. Now the only things that animate are the
   scrolling layers themselves (4) and one bounce per fan row (3), all
   transform/translate-only, so the browser can run them on the compositor
   without re-styling anything per frame. */
.crowd-strip{
  position:absolute; left:0; right:0; top:60px; height:88px;
  overflow:hidden; z-index:0; pointer-events:none;
}
.crowd-fans-track{ position:absolute; left:0; top:6px; height:30px; width:100%; z-index:0; }
.crowd-fans-row, .crowd-banners-track{
  position:absolute; display:flex; align-items:flex-end; width:max-content;
  animation:crowdScroll linear infinite;
  will-change:transform;
  /* Everything sits still until the race has actually been started once —
     the idle "presiona iniciar" screen shouldn't already look like a race
     is under way. .race-started is added the moment startRace() fires and,
     deliberately, stays put through a pause/resume and past the finish
     (endRace() doesn't touch it) — only resetRace() clears it, back to
     this same paused idle look. */
  animation-play-state:paused;
}
.crowd-fans-row{ top:0; height:30px; animation-duration:38s, .5s; }
/* One row per phase of the bounce; the row is the unit that moves, not each
   fan. rowBounce is `translate` (not `transform`) so it layers on top of the
   scroll instead of replacing it. margin-right, not gap, between fans: with
   gap the last item has no trailing space, so -50% lands a few px off the
   true loop length and the wrap jumps. --bounce-delay is negative so each
   row starts already part-way through its bounce. */
.crowd-fans-row{
  animation-name:crowdScroll, rowBounce;
  animation-timing-function:linear, ease-in-out;
  animation-iteration-count:infinite, infinite;
  animation-direction:normal, alternate;
  animation-delay:0s, var(--bounce-delay, 0s);
}
.crowd-banners-track{ left:0; bottom:0; height:64px; z-index:1; animation-duration:24s; }
.track-wrap.race-started ~ .crowd-strip .crowd-fans-row,
.track-wrap.race-started ~ .crowd-strip .crowd-banners-track{ animation-play-state:running; }
@keyframes crowdScroll{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
@keyframes rowBounce{ from{ translate:0 0; } to{ translate:0 -4px; } }
/* One flat-color blob per fan (head+body in one shape, two arm pseudo-
   elements raised in a V) — same level of abstraction as .bush/.bird, not
   a literal illustrated person. --fan-color is set per-instance in JS so
   the rail reads as a real mixed crowd, not one repeated fan. Static: the
   liveliness comes from its row's bounce. */
.fan{
  position:relative; width:16px; height:24px; flex:0 0 auto;
  margin-right:var(--fan-gap, 50px);
  background:var(--fan-color, #df2030);
  border-radius:50% 50% 40% 40%;
}
.fan::before, .fan::after{
  content:""; position:absolute; top:3px; width:3.5px; height:12px; border-radius:2px;
  background:inherit;
}
.fan::before{ left:-3.5px; transform:rotate(-40deg); transform-origin:bottom center; }
.fan::after{ right:-3.5px; transform:rotate(40deg); transform-origin:bottom center; }
/* The banners doing the actual separating — long sponsor boards hung edge
   to edge along the rail. --banner-accent (set per-instance in JS, cycling
   between a neutral/red/purple tint) is the cheap way two banners showing
   the same artwork still read as distinct signs, not one repeated stamp.
   No per-board sway and no filter on the logo: 36 of each was a lot of
   per-frame work for a detail that's moving past at speed anyway. */
.crowd-banner{
  flex:0 0 auto; display:flex; align-items:center; justify-content:center;
  width:150px; height:64px; padding:8px 12px; margin-right:3px;
  background:rgba(10,10,14,.74); border-radius:8px;
  box-shadow:0 4px 12px rgba(0,0,0,.4), inset 0 0 0 1.5px var(--banner-accent, rgba(255,255,255,.16));
}
.crowd-banner img{ width:100%; height:100%; object-fit:contain; }

/* --- Track: a mown-grass field (alternating stripes, like a real pitch)
   framed by a dirt/sand border, sitting right below the scenery's horizon. --- */
.track-wrap{
  position:relative; z-index:1; flex:1; min-height:0;
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,.05) 0 34px, rgba(0,0,0,.05) 34px 68px),
    linear-gradient(180deg, #5cb860, #4a9c4f);
  border:5px solid #cdb87c; border-radius:18px; padding:14px 26px;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.15), 0 10px 24px rgba(0,0,0,.25);
  display:flex; flex-direction:column;
  /* A straight-on tilt, no diagonal lean — the reference's steep diagonal
     angle only shows up in ITS pre-race staging shot; once its own race is
     actually running the camera is basically flat. A pure rotateX keeps
     the field centered and symmetric under the (also centered) scenery
     above it — adding rotateZ on top of it (an earlier attempt at more
     "3D") skewed the whole rectangle sideways, leaving it visibly
     off-center with a lot of dead space on one side and the horizon/hills
     above no longer relating to the tilted ground below. Racers/tags below
     counter-rotate in reverse ("billboarding") so only the ground itself
     reads as tilted. */
  transform-style:preserve-3d;
  transform:rotateX(16deg);
  transform-origin:50% 100%;
  /* The stripe pattern itself creeps sideways forever — a flat, motionless
     field read as a static card; a field that's always drifting reads as
     ground the racers are actually running over. One tile (68px) per loop
     so it wraps with no seam. Paused once the race ends so the world stops
     along with the racers instead of looking like it's still carrying them
     forward. */
  animation:groundFlow 2.4s linear infinite;
}
.track-wrap.race-over, .track-wrap:not(.race-running){ animation-play-state:paused; }
@keyframes groundFlow{
  from{ background-position:0 0, 0 0; }
  to{ background-position:-68px 0, 0 0; }
}
/* Both lines are children of the SAME .lane a racer's own progress-driven
   left comes from (built alongside the racers in buildLanes(), not static
   markup) — same containing block, same calc(100% - width) shape, same
   billboard counter-rotation. That's deliberate: the line markers and the
   racers used to live in different parents with independently-guessed
   offsets, which only looked aligned by coincidence — a racer at progress
   100 was landing tens of px short of a finish line positioned by eye.
   Sharing the exact math a racer's own left uses is what actually
   guarantees the sprite reaches the line, at any window size or roster
   size, instead of a magic-number patch tuned for one screenshot. */
.start-line, .finish-line{
  position:absolute; top:0; bottom:0; width:24px;
  border-radius:4px; z-index:2;
  transform:rotateX(-16deg);
}
.start-line{
  /* Sits a bit AFTER local 0%, not at it — a racer's own resting position
     at progress 0 is local 0, so parking the line past that (scaled to
     the current sprite size, so the gap holds up at any roster size)
     is what makes a racer look like it's lined up BEHIND the gate,
     without moving the racer itself off its normal, safe coordinate
     range (see the long comment on .racer's left calc for why that
     matters). */
  left:calc(var(--racer-size, 44px) * 1.15);
  /* Green, opposite diagonal from the finish tape below — a "go" gate,
     not a second copy of the checkered finish line. */
  background:repeating-linear-gradient(-45deg, #2fa84f 0 6px, #eafbea 6px 12px);
  box-shadow:0 0 10px rgba(47,168,79,.5);
  /* Marks the gate before the race, same as the checkered line in the
     reference's own starting shot — then falls away the moment the race
     actually starts, exactly like theirs does, instead of lingering as a
     marker nobody's racing toward anymore. */
  opacity:1; transition:opacity .5s ease;
}
.start-line.left-behind{ opacity:0; }
.finish-line{
  left:calc(100% - 16px);
  background:repeating-linear-gradient(45deg, #fff 0 6px, #222 6px 12px);
  box-shadow:0 0 12px rgba(0,0,0,.4);
  /* Stays hidden until the leader is nearly home — showing it the whole
     time gives away how much track is left; a race that never lets you
     see the tape until someone's about to hit it is more suspenseful. */
  opacity:0; transition:opacity .7s ease;
}
.finish-line.visible{ opacity:1; }
.lanes{ flex:1; display:flex; flex-direction:column; gap:6px; min-height:0; }
/* Lives directly on .stage, not inside the tilted .track-wrap — it's a
   plain instructional message, not part of the 3D scene, so it doesn't
   need to fight the track's rotation (or its own counter-rotation) to
   stay flat and legible; it just sits centered over the track area. */
.empty-track-msg{
  position:absolute; left:50%; top:calc(50% + 20px); transform:translate(-50%, -50%);
  width:calc(100% - 120px); z-index:3; pointer-events:none;
  color:rgba(255,255,255,.9); font-size:14px; text-align:center; text-shadow:0 2px 5px rgba(0,0,0,.6);
  font-family:var(--font); text-transform:uppercase; letter-spacing:.5px;
}

.lane{
  position:relative; flex:1; min-height:0; display:flex; align-items:center;
  background:rgba(255,255,255,.08);
  border-top:2px dashed rgba(255,255,255,.4); border-bottom:2px dashed rgba(255,255,255,.4);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.06);
}
.lane:first-child{ border-top-left-radius:12px; border-top-right-radius:12px; }
.lane:last-child{ border-bottom-left-radius:12px; border-bottom-right-radius:12px; }
.racer{
  /* --progress (0-1) is written every animation frame from JS — this is what
     actually makes the race move. Driven through transform:translateX, not
     left, on purpose: left is a layout property, so animating it every
     frame for every racer (how this used to work) forced the browser to
     re-layout the whole lane on every single racer's every frame — the
     "pista laggeada" the whole rewrite below is for. transform is
     compositor-only, the one category of property a browser can animate
     smoothly without re-measuring anything.
     --lane-width (px, not %) is what makes that translateX possible at all
     — percentages inside translateX() resolve against the element's OWN
     box, not its container's, so there's no way to express "move across
     100% of the parent" without actually knowing the parent's pixel width.
     syncLaneSizing() keeps it in sync on resize/layout changes only, never
     per frame, so this costs nothing during the race itself.
     No CSS transition on the transform on purpose: JS is already
     interpolating every frame, so a transition here would just lag a beat
     behind the real value instead of helping.
     --slot-frac (0-1) places a racer at its own evenly-spaced position
     across the shared lane's height, side by side with its neighbors
     instead of grouped into repeating clusters.
     Kept to the plain 0%-100% span on purpose (no negative offset to fake
     "starting behind the line") — pushing a racer to a local X outside its
     normal range ran into the tilt's perspective doing something far more
     extreme on screen than the local math implied, up to shoving it clean
     off the visible stage. The start line is positioned a bit AFTER local
     0% instead (see .start-line), so racers still visibly start behind it
     without ever leaving their normal, safe coordinate range. */
  position:absolute; top:calc(var(--slot-frac, .5) * 100%); left:0;
  width:var(--racer-size, 44px); height:var(--racer-size, 44px);
  /* translateX first, in the lane's own unrotated coordinate space, so
     progress always reads as plain horizontal motion; rotateX (the
     billboard correction — the floor is tilted rotateX(16deg), each racer
     applies the exact inverse to stand upright facing the camera instead
     of leaning with the tilted ground) and the vertical centering come
     after, same as before. */
  transform:translateX(calc((var(--lane-width, 600px) - var(--racer-size, 44px)) * var(--progress, 0))) rotateX(-16deg) translate(0, -50%);
  display:flex; align-items:center; justify-content:center; z-index:2;
}
.racer-shadow{
  position:absolute; left:50%; bottom:2%; width:62%; height:18%;
  transform:translateX(-50%);
  background:radial-gradient(ellipse, rgba(0,0,0,.42), rgba(0,0,0,0) 72%);
  border-radius:50%; z-index:0; pointer-events:none;
}
.racer-tag{
  position:absolute; left:50%; bottom:100%; margin-bottom:2px;
  transform:translateX(-50%);
  font-family:var(--font); font-weight:800; font-size:var(--lane-font, 11px); color:#fff;
  text-transform:uppercase; text-shadow:0 1px 2px rgba(0,0,0,.6); letter-spacing:.2px;
  white-space:nowrap; pointer-events:none; z-index:3;
  background:rgba(0,0,0,.4); padding:1px 6px; border-radius:999px; line-height:1.5;
  max-width:12em; overflow:hidden; text-overflow:ellipsis;
}
.racer img{
  position:relative; z-index:1;
  width:100%; height:100%; object-fit:contain; image-rendering:pixelated;
  filter:drop-shadow(0 3px 6px rgba(0,0,0,.5));
}
.racer.hit img{ animation:hitFlash .65s ease; }
@keyframes hitFlash{
  0%{ transform:scale(1) rotate(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
  15%{ transform:scale(.78) translateX(-14px) rotate(-8deg); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(2.6) saturate(0) drop-shadow(0 0 12px var(--fx-color, #e05555)); }
  35%{ transform:scale(1.1) translateX(8px) rotate(6deg); }
  55%{ transform:scale(.95) translateX(-4px) rotate(-3deg); }
  80%{ transform:scale(1.03) rotate(1deg); }
  100%{ transform:scale(1) rotate(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
}
/* The attacker gets its own lunge-forward pulse — without this, only the
   *target* visibly reacted and it was hard to tell at a glance who actually
   threw the attack. */
.racer.attacking img{ animation:attackLunge .55s cubic-bezier(.3,0,.4,1.6); }
@keyframes attackLunge{
  0%{ transform:scale(1) translateX(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
  35%{ transform:scale(1.22) translateX(14px); filter:drop-shadow(0 0 14px var(--fx-color, #fff)) brightness(1.6); }
  60%{ transform:scale(1.05) translateX(4px); }
  100%{ transform:scale(1) translateX(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
}
/* A rarer, bigger hit — see CRIT_CHANCE in JS — reuses the same shake/pop
   language as a normal hit but pushed further, plus a starburst ring, so
   it reads as "that one really landed" without needing a whole separate
   effect system. */
.racer.crit-hit img{ animation:critHitFlash .85s ease; }
@keyframes critHitFlash{
  0%{ transform:scale(1) rotate(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
  12%{ transform:scale(.62) translateX(-20px) rotate(-14deg); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(3.2) saturate(0) drop-shadow(0 0 20px var(--fx-color, #e05555)); }
  30%{ transform:scale(1.24) translateX(12px) rotate(10deg); }
  48%{ transform:scale(.88) translateX(-8px) rotate(-6deg); }
  66%{ transform:scale(1.08) translateX(4px) rotate(3deg); }
  85%{ transform:scale(.98) rotate(-1deg); }
  100%{ transform:scale(1) rotate(0); filter:drop-shadow(0 3px 6px rgba(0,0,0,.5)) brightness(1); }
}
.racer.crit-hit::after{
  content:""; position:absolute; inset:-14%; border-radius:50%; z-index:4; pointer-events:none;
  box-shadow:0 0 0 3px var(--fx-color, #fff);
  animation:critRing .6s ease-out forwards;
}
@keyframes critRing{
  0%{ transform:scale(.4); opacity:1; }
  100%{ transform:scale(1.7); opacity:0; }
}
.racer .impact-fx{
  position:absolute; left:50%; top:-6px; transform:translate(-50%, 0);
  font-size:calc(var(--racer-size, 44px) * .55); pointer-events:none; z-index:5;
  animation:impactPop .7s ease forwards;
  /* Colored per the attacking move's own type (set inline as --fx-color)
     so the glow reads as "this power," not just a generic flash. */
  text-shadow:0 0 12px var(--fx-color, #fff), 0 0 24px var(--fx-color, #fff);
}
.racer .impact-fx.impact-crit{
  font-size:calc(var(--racer-size, 44px) * .85);
  animation:impactCritPop .95s ease forwards;
}
@keyframes impactCritPop{
  0%{ transform:translate(-50%, 6px) scale(.3) rotate(-8deg); opacity:0; }
  20%{ transform:translate(-50%, -10px) scale(1.4) rotate(6deg); opacity:1; }
  45%{ transform:translate(-50%, -22px) scale(1.15) rotate(-3deg); opacity:1; }
  75%{ transform:translate(-50%, -30px) scale(1.2) rotate(2deg); opacity:1; }
  100%{ transform:translate(-50%, -42px) scale(1) rotate(0); opacity:0; }
}
@keyframes impactPop{
  0%{ transform:translate(-50%, 6px) scale(.4); opacity:0; }
  25%{ transform:translate(-50%, -6px) scale(1.15); opacity:1; }
  70%{ transform:translate(-50%, -18px) scale(1); opacity:1; }
  100%{ transform:translate(-50%, -30px) scale(.9); opacity:0; }
}

/* --- Attack projectile: the power actually flies from attacker to target
   (a bolt for Eléctrico, a fireball for Fuego, a water jet for Agua...)
   instead of the hit just flashing in place. Positioned in plain viewport
   coordinates (fixed, computed from each racer's real getBoundingClientRect
   at cast time) rather than the track's own tilted coordinate space —
   after the finish-line saga, reading real screen rects for two arbitrary
   racers is far more reliable than re-deriving another shared local calc.
   JS drives the actual from→to motion via the Web Animations API since
   the path is different every time; this CSS only styles the look. */
.projectile-fx{ position:fixed; z-index:150; pointer-events:none; left:0; top:0; }
.projectile-trail{
  position:absolute; left:0; top:-2.5px; width:34px; height:5px;
  background:linear-gradient(90deg, var(--fx-color, #fff), transparent);
  border-radius:999px; filter:blur(1.5px); opacity:.85;
  transform-origin:0 50%;
  transform:rotate(calc(var(--angle, 0deg) + 180deg));
}
.projectile-emoji{
  position:absolute; left:0; top:0; transform:translate(-50%, -50%);
  font-size:24px; line-height:1;
  filter:drop-shadow(0 0 8px var(--fx-color, #fff)) drop-shadow(0 0 3px rgba(0,0,0,.4));
}
/* Frozen only pauses the racer's clock — it must never look like the setback
   effect we removed, so this stays a subtle sustained tint rather than
   anything that reads as knockback or damage. No badge/icon here on
   purpose — the only icon an attack shows is the type effect itself (see
   .impact-fx), not a second, unrelated "frozen" symbol layered on top of
   it. filter lives on the wrapper, not the img, so it never fights the
   hit/attacking keyframes which already own the img's own animation
   property. */
.racer.frozen{ filter:saturate(.55) brightness(1.06); }
.racer.frozen::after{
  content:""; position:absolute; inset:-8px; border-radius:50%; z-index:0; pointer-events:none;
  background:radial-gradient(circle, rgba(150,225,255,.5) 0%, rgba(150,225,255,.12) 55%, transparent 75%);
  box-shadow:0 0 0 2px rgba(200,240,255,.9), inset 0 0 10px rgba(255,255,255,.55);
  animation:frozenGlow 1.1s ease-in-out infinite;
}
@keyframes frozenGlow{
  0%,100%{ opacity:.7; transform:scale(1); }
  50%{ opacity:1; transform:scale(1.08); }
}
.racer.finished img{ animation:winnerBounce .6s ease infinite alternate; }
@keyframes winnerBounce{ from{ transform:translateY(0) scale(1); } to{ transform:translateY(-10%) scale(1.05); } }
.racer .rank-badge{
  position:absolute; top:-6px; right:-6px; width:18px; height:18px; border-radius:50%;
  background:#f4c430; color:#3a2600; font-family:var(--font); font-weight:800; font-size:10px;
  display:flex; align-items:center; justify-content:center; box-shadow:0 2px 5px rgba(0,0,0,.5); z-index:4;
}

/* Victory huddle — see spawnFinishCelebration() in JS. Lives on #stage, not
   in the tilted .lane, and floats flat (no rotateX) above the track's own
   ground plane near the finish-line end — deliberately reads as a podium
   popping up past the race, not as more sprites running the actual course. */
.finish-celebration{
  position:absolute; top:60px; right:24px; z-index:4; pointer-events:none;
  max-width:200px;
  background:rgba(10,10,14,.6); backdrop-filter:blur(3px);
  border-radius:16px; padding:10px 10px 8px;
  box-shadow:0 10px 26px rgba(0,0,0,.45), inset 0 0 0 1px rgba(255,255,255,.14);
  animation:celebrationPanelPop .45s cubic-bezier(.2,1.4,.4,1) both;
}
.celebration-huddle{
  display:flex; flex-wrap:wrap; justify-content:center; gap:2px;
  width:132px;
}
/* Negative margin instead of a bigger gap is what actually makes it read as
   a "huddle" — sprites overlapping their neighbors, not a tidy evenly
   spaced grid of individuals. */
.celebration-member{
  position:relative; width:30px; height:30px; margin:-4px;
  animation:celebrationHop .7s ease infinite alternate;
}
.celebration-member img{
  width:100%; height:100%; object-fit:contain; image-rendering:pixelated;
  filter:drop-shadow(0 2px 4px rgba(0,0,0,.5));
}
.celebration-member.holds-trophy{ z-index:1; }
.celebration-trophy{
  position:absolute; left:50%; bottom:78%; transform:translateX(-50%);
  font-size:16px; line-height:1; z-index:2; pointer-events:none;
  filter:drop-shadow(0 2px 5px rgba(0,0,0,.5)) drop-shadow(0 0 8px rgba(244,196,48,.85));
  animation:trophyBounce 1.1s ease-in-out infinite alternate;
}
@keyframes celebrationPanelPop{
  0%{ transform:scale(.4) translateY(10px); opacity:0; }
  70%{ transform:scale(1.08) translateY(-2px); opacity:1; }
  100%{ transform:scale(1) translateY(0); opacity:1; }
}
@keyframes celebrationHop{
  from{ transform:translateY(0); }
  to{ transform:translateY(-5px); }
}
@keyframes trophyBounce{
  from{ transform:translateX(-50%) translateY(0) rotate(-6deg); }
  to{ transform:translateX(-50%) translateY(-4px) rotate(6deg); }
}

/* --- Attack toasts --- */
.atk-toasts{
  position:absolute; top:10px; left:16px; right:170px; z-index:20;
  display:flex; flex-direction:column; gap:6px; align-items:center; pointer-events:none;
}
.atk-toast{
  background:#000; color:#fff; font-family:var(--font); font-weight:800; font-size:13px;
  padding:8px 18px; border-radius:999px; text-align:center; max-width:100%;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  box-shadow:0 6px 16px rgba(0,0,0,.4), inset 0 0 0 2px #e05555;
  animation:toastPop .3s cubic-bezier(.2,1.4,.4,1), toastFade .4s ease 3.1s forwards;
}
@keyframes toastPop{ from{ transform:translateY(-8px) scale(.9); opacity:0; } to{ transform:translateY(0) scale(1); opacity:1; } }
@keyframes toastFade{ to{ opacity:0; transform:translateY(-6px); } }

/* --- Bottom row --- */
.stage-bottom{
  position:relative; z-index:10; display:flex; gap:14px; align-items:stretch; height:64px;
}
.banner-pill{
  flex:1; min-width:0; background:#000; border-radius:999px;
  display:flex; align-items:center; justify-content:center; padding:0 26px;
  box-shadow:0 8px 22px rgba(0,0,0,.35), inset 0 0 0 2px rgba(255,255,255,.08);
}
.banner-text{
  color:var(--text-color, #fff); font-family:var(--font); font-weight:900; letter-spacing:.6px;
  font-size:clamp(14px,2vw,20px); text-align:center; text-transform:uppercase;
  text-shadow:0 2px 4px rgba(0,0,0,.5);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}
.banner-placeholder{ color:rgba(255,255,255,.5); letter-spacing:2px; }
.next-atk-pill{
  background:linear-gradient(180deg,var(--green),var(--green-dark));
  border-radius:999px; padding:8px 22px; min-width:150px;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  box-shadow:var(--shadow-soft);
}
.next-atk-pill span{ font-family:var(--font); font-weight:900; font-size:clamp(18px,2.6vw,28px); color:#0c2a10; line-height:1; }
.next-atk-pill small{ font-weight:800; color:#0c2a10; letter-spacing:1px; font-size:10px; margin-top:2px; }
.next-atk-pill.paused{ opacity:.5; }

/* --- Corner gear + dock (shared pattern) --- */
.corner-hover{ position:fixed; top:0; right:0; width:76px; height:76px; display:flex; align-items:flex-start; justify-content:flex-end; padding:12px; opacity:0; transition:opacity .2s ease; z-index:100; }
.corner-hover:hover{ opacity:1; }
.dock-toggle{ width:38px; height:38px; border-radius:50%; border:none; background:rgba(0,0,0,.65); color:#fff; font-size:18px; cursor:pointer; }

.dock{
  position:fixed; top:0; left:0; width:360px; height:100vh; overflow-y:auto;
  background:#141417; border-right:1px solid rgba(255,255,255,.08);
  padding:20px; display:flex; flex-direction:column; gap:16px; z-index:80;
}
body.live-mode .dock{ display:none; }
.dock .back-link{ font-size:12px; color:#888; text-decoration:none; }
.dock .back-link:hover{ color:#fff; }
.dock h2{ margin:0; font-size:16px; font-family:var(--font); color:#fff; }
.dock .hint{ margin:0; font-size:11px; color:#777; }
.live-toggle{ appearance:none; cursor:pointer; border:none; border-radius:12px; padding:12px; font-weight:800; font-size:13px; background:#232327; color:#fff; }

button.start-btn{
  appearance:none; border:none; cursor:pointer; width:100%; background:var(--brand-red); color:#fff;
  font-family:var(--font); font-weight:800; font-size:16px; border-radius:12px; padding:12px;
  transition:background .15s ease;
}
button.start-btn:hover{ background:var(--brand-red-dark); }
button.start-btn:disabled{ opacity:.5; cursor:not-allowed; }
button.start-btn.stop{ background:#232327; }
button.start-btn.stop:hover{ background:#2e2e34; }
.reset-btn{ border:none; border-radius:12px; padding:12px; font-weight:800; font-size:13px; cursor:pointer; background:#232327; color:#e0b84c; border:1px solid #3a331a; }
.reset-btn:disabled{ opacity:.5; cursor:not-allowed; }

.seg-details{ border-top:1px solid rgba(255,255,255,.08); padding-top:12px; }
.seg-details summary{ cursor:pointer; font-size:13px; font-weight:800; font-family:var(--font); letter-spacing:.02em; text-transform:uppercase; color:#ddd; list-style:none; display:flex; align-items:center; gap:6px; }
.seg-details summary::-webkit-details-marker{ display:none; }
.seg-details summary::before{ content:'▸'; color:var(--brand-red); font-size:12px; transition:transform .15s ease; }
.seg-details[open] summary::before{ transform:rotate(90deg); }
.count-badge{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:18px; padding:0 6px; height:18px; border-radius:999px;
  background:var(--brand-red); color:#fff; font-size:10px; font-weight:800;
}
.section-body{ margin-top:12px; }
.hint-text{ font-size:11.5px; color:var(--text-faint); margin:0 0 12px; line-height:1.5; }

/* --- Roster picker --- */
.selected-row{ display:flex; flex-direction:column; gap:6px; margin-bottom:10px; min-height:26px; }
.sel-chip{
  display:flex; align-items:center; gap:7px; background:var(--surface-2); border:1px solid var(--border);
  border-radius:var(--radius-m); padding:5px 8px 5px 6px; font-size:11.5px; color:#eee;
}
.sel-chip img{ width:24px; height:24px; object-fit:contain; image-rendering:pixelated; flex:none; }
.sel-chip .sel-species{
  font-size:10px; color:var(--text-faint); white-space:nowrap; flex:none; max-width:62px;
  overflow:hidden; text-overflow:ellipsis;
}
.sel-chip .sel-name-input{
  flex:1; min-width:0; font-size:12px; padding:5px 8px; border-radius:var(--radius-s);
  border:1px solid var(--border); background:var(--input-bg); color:#fff;
}
.sel-chip .sel-name-input::placeholder{ color:var(--text-faint); }
.sel-chip .sel-name-input:disabled{ opacity:.6; }
.sel-chip button{
  appearance:none; border:none; background:none; color:#e05555; font-size:13px; cursor:pointer; padding:2px 3px; line-height:1; flex:none;
}
.sel-chip.empty{ color:var(--text-faint); border-style:dashed; justify-content:center; padding:8px; }

.roster-btn-row{ display:flex; gap:8px; }
.open-search-btn{
  flex:1; min-width:0; appearance:none; cursor:pointer; background:var(--surface-2); color:#fff;
  border:1px solid var(--border); font-weight:700; font-size:13.5px; padding:11px; border-radius:var(--radius-m);
  transition:border-color .15s ease, background .15s ease;
}
.open-search-btn:hover{ border-color:var(--brand-red); background:rgba(223,32,48,.12); }
.clear-roster-btn{
  flex:0 0 auto; width:42px; appearance:none; cursor:pointer; background:var(--surface-2); color:#e0b84c;
  border:1px solid var(--border); font-size:15px; border-radius:var(--radius-m);
  transition:border-color .15s ease, background .15s ease;
}
.clear-roster-btn:hover:not(:disabled){ border-color:#e0b84c; background:rgba(224,184,76,.12); }
.clear-roster-btn:disabled{ opacity:.35; cursor:not-allowed; }

/* --- Pokémon search modal: a real "open a finder" moment (big, centered,
   dims the whole page) instead of a cramped field+grid stuffed in the
   360px-wide dock — the roster grew to 1025, and 3 thumbnails' width of
   browsing room stopped being enough to make picking feel easy. --- */
.poke-search-overlay{
  position:fixed; inset:0; z-index:220; background:rgba(0,0,0,.7);
  display:flex; align-items:center; justify-content:center; padding:24px;
}
.poke-search-box{
  width:100%; max-width:680px; max-height:min(720px, 88vh);
  background:#18181c; border:1px solid #2e2e34; border-radius:20px;
  box-shadow:0 24px 60px rgba(0,0,0,.55);
  display:flex; flex-direction:column; overflow:hidden;
  animation:pokeSearchPop .2s cubic-bezier(.2,1.4,.4,1);
}
@keyframes pokeSearchPop{ from{ transform:scale(.95); opacity:0; } to{ transform:scale(1); opacity:1; } }
.poke-search-header{ display:flex; gap:8px; padding:18px 18px 12px; flex:0 0 auto; }
.poke-search{
  flex:1; min-width:0; font-size:15px; padding:12px 16px; border-radius:var(--radius-m);
  border:1px solid var(--border); background:var(--input-bg); color:#fff;
}
.poke-search-close{
  flex:0 0 auto; width:42px; height:42px; border-radius:var(--radius-m); border:1px solid var(--border);
  background:var(--surface-2); color:#ccc; font-size:16px; cursor:pointer;
}
.poke-search-close:hover{ border-color:var(--brand-red); color:#fff; }
.poke-grid{
  display:grid; grid-template-columns:repeat(auto-fill, minmax(88px, 1fr)); gap:8px;
  flex:1; min-height:0; overflow-y:auto; padding:0 18px 18px;
}
.poke-item{
  display:flex; flex-direction:column; align-items:center; gap:2px;
  background:var(--surface-2); border:1px solid var(--border); border-radius:var(--radius-s);
  padding:8px 4px 6px; cursor:pointer; transition:border-color .12s ease, background .12s ease;
}
.poke-item:hover{ border-color:var(--brand-red); }
.poke-item.picked{ background:rgba(223,32,48,.16); border-color:var(--brand-red); }
.poke-item.disabled{ opacity:.35; cursor:not-allowed; }
.poke-item img{ width:52px; height:52px; object-fit:contain; image-rendering:pixelated; }
.poke-item .pi-name{ font-size:11px; font-weight:700; color:#eee; text-align:center; line-height:1.15; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
.poke-item .pi-num{ font-size:9.5px; color:var(--text-faint); }
.poke-empty-msg{ grid-column:1/-1; text-align:center; font-size:12.5px; color:var(--text-faint); padding:30px 0; }

.interval-row{ display:flex; align-items:center; gap:8px; margin-bottom:14px; }
.interval-row label{ font-size:12.5px; color:var(--text-soft); white-space:nowrap; }
.interval-row input[type="number"]{
  width:64px; font-size:13px; padding:8px 10px; border-radius:var(--radius-m);
  border:1px solid var(--border); background:var(--input-bg); color:#fff;
}
.duration-row{ flex-direction:column; align-items:flex-start; gap:8px; }
.duration-fields{ display:flex; gap:10px; }
.duration-field{ display:flex; align-items:center; gap:5px; }
.duration-field input[type="number"]{
  width:48px; font-size:13px; padding:8px 6px; border-radius:var(--radius-m); text-align:center;
  border:1px solid var(--border); background:var(--input-bg); color:#fff;
}
.duration-field span{ font-size:11.5px; color:var(--text-soft); }

.chips{ display:flex; flex-wrap:wrap; gap:8px; }
.chip{
  appearance:none; cursor:pointer; font-size:13px; font-weight:700;
  padding:8px 13px; border-radius:999px; border:1px solid var(--border); background:var(--surface-2); color:#ddd;
  transition:background .15s ease, border-color .15s ease, color .15s ease;
}
.chip:hover{ border-color:var(--brand-red); }
.chip .check{ display:inline-block; width:13px; height:13px; border-radius:4px; border:1.5px solid var(--border); margin-right:7px; vertical-align:-2px; position:relative; }
.chip[aria-pressed="true"]{ background:rgba(223,32,48,.16); border-color:var(--brand-red); color:#fff; }
.chip[aria-pressed="true"] .check{ background:var(--brand-red); border-color:var(--brand-red); }
.chip[aria-pressed="true"] .check::after{
  content:""; position:absolute; left:2.5px; top:-0.5px; width:4px; height:7px;
  border-right:2px solid #fff; border-bottom:2px solid #fff; transform:rotate(40deg);
}

.color-field{ display:flex; align-items:center; gap:10px; margin-bottom:12px; }
.color-field label{ flex:0 0 auto; font-size:12.5px; font-weight:600; color:var(--text-soft); min-width:88px; }
.color-field input[type="text"]{
  flex:1; min-width:0; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:13px; padding:9px 10px;
  border-radius:var(--radius-s); border:1px solid var(--border); background:var(--input-bg); color:#fff;
}
.color-field input[type="color"]{
  width:34px; height:34px; padding:0; border-radius:8px; border:1px solid var(--border); background:none; cursor:pointer; flex:0 0 auto;
}

.modal-overlay{ position:fixed; inset:0; z-index:300; background:rgba(0,0,0,.65); display:flex; align-items:center; justify-content:center; padding:20px; }
.modal-box{ background:#18181c; border:1px solid #2e2e34; border-radius:16px; padding:22px; max-width:360px; width:100%; box-shadow:0 20px 50px rgba(0,0,0,.5); }
.modal-box p{ margin:0 0 18px; color:#fff; font-size:14px; line-height:1.4; }
.modal-actions{ display:flex; gap:10px; justify-content:flex-end; }
.modal-btn{ border:none; border-radius:10px; padding:10px 16px; font-weight:700; font-size:13px; cursor:pointer; }
.modal-btn-ghost{ background:#232327; color:#ccc; }
.modal-btn-ghost:hover{ background:#2e2e34; }
.modal-btn-primary{ background:var(--brand-red); color:#fff; }
.modal-btn-primary:hover{ background:var(--brand-red-dark); }

#confettiLayer{ position:fixed; inset:0; pointer-events:none; overflow:hidden; z-index:9999; }
.confetti-piece{ position:absolute; top:-12px; width:8px; height:14px; opacity:.95; animation:confettiFall linear forwards; }
@keyframes confettiFall{ to{ transform:translateY(105vh) translateX(var(--dx)) rotate(var(--rot)); opacity:.25; } }
.firework-spark{ position:absolute; width:6px; height:6px; border-radius:50%; animation:fireworkBurst 1s cubic-bezier(.15,.6,.35,1) forwards; }
@keyframes fireworkBurst{
  0%{ transform:translate(0,0) scale(1); opacity:1; }
  65%{ opacity:1; }
  100%{ transform:translate(var(--fx), var(--fy)) scale(.3); opacity:0; }
}

/* --- Winner reveal --- */
.winner-announce{
  position:fixed; inset:0; z-index:210; display:flex; align-items:center; justify-content:center;
  pointer-events:none;
}
.winner-announce-card{
  background:rgba(10,10,12,.94); border-radius:24px; padding:26px 40px 28px;
  text-align:center; box-shadow:0 24px 60px rgba(0,0,0,.6), 0 0 0 3px #f4c430;
  opacity:0; display:flex; flex-direction:column; align-items:center; gap:6px;
  pointer-events:auto; max-width:min(420px, 90vw); max-height:88vh;
}
/* No more auto-fade-out — with a full standings list to read (up to 100
   racers), a fixed timer either cuts it off mid-scroll or isn't long
   enough for a big roster. It stays up until the host closes it or starts
   the next race. */
.winner-announce-card.play{ animation:winnerPop .5s cubic-bezier(.2,1.4,.4,1) forwards; }
.winner-announce-title{
  font-family:var(--font); font-weight:800; font-size:clamp(30px,5vw,52px); color:#f4c430;
  letter-spacing:2px; text-shadow:0 4px 12px rgba(0,0,0,.5);
}
.winner-announce-sprite{ width:96px; height:96px; object-fit:contain; filter:drop-shadow(0 6px 12px rgba(0,0,0,.5)); }
.winner-announce-sub{
  margin-top:2px; font-family:var(--font); font-weight:800; font-size:clamp(15px,2.2vw,22px); color:#fff;
  text-shadow:0 2px 4px rgba(0,0,0,.5); text-transform:uppercase; letter-spacing:.5px;
}
@keyframes winnerPop{
  0%{ transform:scale(.6); opacity:0; }
  60%{ transform:scale(1.06); opacity:1; }
  100%{ transform:scale(1); opacity:1; }
}
.results-heading{
  margin-top:14px; font-family:var(--font); font-weight:800; font-size:12px; color:var(--text-faint, #9a9aa2);
  text-transform:uppercase; letter-spacing:1.5px;
}
.results-list{
  list-style:none; margin:6px 0 4px; padding:6px; width:100%;
  max-height:38vh; overflow-y:auto;
  background:rgba(255,255,255,.04); border-radius:12px;
  display:flex; flex-direction:column; gap:2px;
}
.results-list li{
  display:flex; align-items:center; gap:8px; padding:5px 8px; border-radius:8px;
  background:rgba(255,255,255,.03);
}
.results-list li.results-you{ background:rgba(244,196,48,.14); }
.results-rank{
  flex:none; width:24px; font-family:var(--font); font-weight:800; font-size:12.5px;
  color:var(--text-faint, #9a9aa2); text-align:right;
}
.results-list li:nth-child(1) .results-rank{ color:#f4c430; }
.results-list li:nth-child(2) .results-rank{ color:#d8d8d8; }
.results-list li:nth-child(3) .results-rank{ color:#d69355; }
.results-sprite{ flex:none; width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
.results-name{
  flex:1; min-width:0; text-align:left; font-family:var(--font); font-weight:700; font-size:13px; color:#eee;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.results-dnf{ flex:none; font-size:10.5px; color:var(--text-faint, #9a9aa2); text-transform:uppercase; }
.results-close-btn{
  margin-top:10px; appearance:none; border:none; border-radius:999px; padding:9px 26px;
  background:#f4c430; color:#2a1d00; font-family:var(--font); font-weight:800; font-size:13px;
  letter-spacing:.4px; cursor:pointer;
}
.results-close-btn:hover{ background:#ffd34d; }

/* --- Espejo dentro del live de Chasers (shared/espejo.js): la pista se queda con su cielo y su
   pasto (una carrera encima del video no se leería). Las posiciones llegan unas ocho veces por
   segundo desde el control y el navegador rellena el camino entre una y otra. El botón de
   cerrar el resultado es del vendedor: en la sala se cierra cuando él lo cierra. --- */
html.modo-espejo .racer{ transition: transform .15s linear; }
html.modo-espejo .results-close-btn, html.modo-espejo .empty-track-msg{ display: none !important; }
