const JUMPY_RACE_PHASE = {
  CLOSED: "closed",
  WAITING: "waiting",
  COUNTDOWN: "countdown",
  RUNNING: "running",
  FINISH: "finish",
  PODIUM: "podium",
};

const JUMPY_RACE_ASSETS = {
  trophy: "public/race/jumpy-race-winner-trophy.png",
};

const JUMPY_RACE_CSS = `
  @keyframes jy-race-overlay-in {
    from { opacity: 0; transform: translateX(70px); }
    to { opacity: 1; transform: translateX(0); }
  }
  @keyframes jy-race-hop {
    0% { transform: translate(-50%, -50%) scale(1); }
    35% { transform: translate(-50%, -68%) scale(1.13) rotate(-2deg); }
    72% { transform: translate(-50%, -44%) scale(0.97) rotate(1deg); }
    100% { transform: translate(-50%, -50%) scale(1); }
  }
  @keyframes jy-race-burst {
    0% { opacity: 0.9; transform: translate(-50%, -50%) scale(0.35); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
  }
  @keyframes jy-race-count {
    0% { opacity: 0; transform: scale(1.5) rotate(-5deg); }
    28% { opacity: 1; transform: scale(0.92) rotate(1deg); }
    100% { opacity: 1; transform: scale(1) rotate(0); }
  }
  @keyframes jy-race-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.06); }
  }
  @keyframes jy-race-podium-up {
    from { opacity: 0; transform: translateY(90px); }
    to { opacity: 1; transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .jy-race-overlay, .jy-race-hop, .jy-race-burst,
    .jy-race-count, .jy-race-pulse, .jy-race-podium-entry {
      animation: none !important;
      transition: none !important;
    }
  }
`;

function readJumpyRaceOptions() {
  const params = new URLSearchParams(window.location.search);
  const raceMode = String(params.get("race") || "").toLowerCase();
  const demo = raceMode === "demo";
  const fixture = String(params.get("mode") || "").toLowerCase() === "fixture";
  const durationValue = params.get("raceDuration");
  const requestedDuration = Number(durationValue);
  const durationSeconds = fixture && durationValue !== null && Number.isFinite(requestedDuration)
    ? Math.min(120, Math.max(8, Math.round(requestedDuration)))
    : 60;
  const playersValue = params.get("racePlayers");
  const requestedPlayers = Number(playersValue);
  const maxRacers = fixture && playersValue !== null && Number.isFinite(requestedPlayers)
    ? Math.min(JumpyRaceEngine.DEFAULT_MAX_RACERS, Math.max(1, Math.round(requestedPlayers)))
    : JumpyRaceEngine.DEFAULT_MAX_RACERS;
  return {
    autoStart: demo || raceMode === "auto",
    durationSeconds,
    maxRacers,
  };
}

function useReducedMotion() {
  const [reduced, setReduced] = React.useState(() => window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches || false);
  React.useEffect(() => {
    const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
    if (!media) return undefined;
    const onChange = () => setReduced(media.matches);
    media.addEventListener?.("change", onChange);
    return () => media.removeEventListener?.("change", onChange);
  }, []);
  return reduced;
}

function useVisibleRaceScore(score, identity, reducedMotion) {
  const target = React.useMemo(() => ({
    jumpCount: score?.jumpCount || 0,
    distanceMeters: score?.distanceMeters || 0,
  }), [score?.jumpCount, score?.distanceMeters]);
  const [visible, setVisible] = React.useState(target);
  const visibleRef = React.useRef(target);
  const identityRef = React.useRef(identity);

  React.useEffect(() => {
    if (identityRef.current !== identity || reducedMotion) {
      identityRef.current = identity;
      visibleRef.current = target;
      setVisible(target);
      return undefined;
    }
    if (target.jumpCount <= visibleRef.current.jumpCount || target.distanceMeters < visibleRef.current.distanceMeters) {
      visibleRef.current = target;
      setVisible(target);
      return undefined;
    }

    let cancelled = false;
    let timer;
    const advance = () => {
      if (cancelled) return;
      const next = JumpyRaceEngine.nextVisualRaceScore(visibleRef.current, target);
      visibleRef.current = next;
      setVisible(next);
      if (next.jumpCount < target.jumpCount) timer = window.setTimeout(advance, 82);
    };
    timer = window.setTimeout(advance, 0);
    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [identity, target.jumpCount, target.distanceMeters, reducedMotion]);

  return visible;
}

function raceDurationLabel(seconds) {
  if (seconds === 60) return "1 MINUT";
  return `${seconds} SEKUNDER`;
}

function raceMeters(value) {
  return `${Math.max(0, Number(value) || 0).toLocaleString("sv", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} m`;
}

function JumpyRaceCloseButton({ onClose, dark = false }) {
  return (
    <button
      type="button"
      onClick={onClose}
      title="Stäng lopp"
      aria-label="Stäng lopp"
      style={{
        position: "absolute",
        top: 28,
        right: 30,
        zIndex: 90,
        width: 56,
        height: 56,
        border: `3px solid ${dark ? jyStyles.white : jyStyles.black}`,
        borderRadius: "50%",
        background: dark ? jyStyles.black : jyStyles.white,
        color: dark ? jyStyles.white : jyStyles.black,
        cursor: "pointer",
        display: "grid",
        placeItems: "center",
        fontFamily: "Arial, sans-serif",
        fontWeight: 700,
        fontSize: 32,
        lineHeight: 1,
      }}
    >
      ×
    </button>
  );
}

function JumpyRaceMiniWordmark({ light = false }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <div style={{ width: 82, height: 82, display: "grid", placeItems: "center" }}>
        <JYLogo3D size={92} color={light ? jyStyles.white : jyStyles.red}/>
      </div>
      <div style={{
        ...jyFonts.display,
        color: light ? jyStyles.white : jyStyles.black,
        fontSize: 48,
        lineHeight: 0.82,
        letterSpacing: 0,
        transform: "skewX(-7deg)",
        textShadow: light ? `3px 3px 0 ${jyStyles.red}` : `3px 3px 0 ${jyStyles.red}`,
      }}>
        Jumpy Race
      </div>
    </div>
  );
}

function JumpyRaceHeader({ phase, countdown, timeLeftMs, durationSeconds, onClose }) {
  const urgent = phase === JUMPY_RACE_PHASE.RUNNING && timeLeftMs <= 10000;
  const phaseLabel = phase === JUMPY_RACE_PHASE.COUNTDOWN ? "START OM"
    : phase === JUMPY_RACE_PHASE.FINISH ? "TIDEN ÄR UTE"
    : urgent ? "SISTA 10" : "LÄNGST VINNER";
  const visibleTimeMs = phase === JUMPY_RACE_PHASE.COUNTDOWN ? countdown * 1000 : timeLeftMs;
  return (
    <div style={{
      position: "absolute",
      left: 0,
      right: 0,
      top: 0,
      height: 142,
      zIndex: 30,
      background: urgent ? jyStyles.red : jyStyles.black,
      color: jyStyles.white,
      borderBottom: `9px solid ${urgent ? jyStyles.yellow : jyStyles.red}`,
      display: "grid",
      gridTemplateColumns: "560px 1fr 330px",
      alignItems: "center",
      padding: "0 30px",
    }}>
      <JumpyRaceMiniWordmark light/>
      <div style={{ textAlign: "center" }}>
        <div style={{ ...jyFonts.label, fontSize: 16, color: urgent ? jyStyles.black : jyStyles.yellow }}>
          {phase === JUMPY_RACE_PHASE.COUNTDOWN ? "JUMPY RACE" : `KOM LÄNGST PÅ ${raceDurationLabel(durationSeconds)}`}
        </div>
        <div style={{ ...jyFonts.display, fontSize: 48, lineHeight: 0.9, letterSpacing: 0 }}>
          {phaseLabel}
        </div>
      </div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 18, paddingRight: 70 }}>
        <div className={urgent ? "jy-race-pulse" : ""} style={{
          animation: urgent ? "jy-race-pulse 700ms ease-in-out infinite" : "none",
          ...jyFonts.num,
          fontSize: 76,
          lineHeight: 0.8,
          color: urgent ? jyStyles.white : jyStyles.yellow,
          letterSpacing: 0,
          minWidth: 160,
          textAlign: "right",
        }}>
          {JumpyRaceEngine.formatRaceClock(visibleTimeMs)}
        </div>
      </div>
      <JumpyRaceCloseButton onClose={onClose} dark/>
    </div>
  );
}

function JumpyRaceRunner({ score, runId, phase, racerCount, reducedMotion }) {
  const visible = useVisibleRaceScore(score, `${runId}-${score.raceId}`, reducedMotion);
  const progress = JumpyRaceEngine.raceDistanceProgress(visible.distanceMeters);
  const compact = racerCount >= 8;
  const medium = racerCount >= 5;
  const mascotSize = compact ? 62 : medium ? 92 : 118;
  const nameWidth = compact ? 136 : 176;
  const isRunning = phase === JUMPY_RACE_PHASE.RUNNING;
  const isLeader = score.rank === 1 && score.jumpCount > 0;
  const placeColor = score.rank === 1 ? jyStyles.yellow : score.rank === 2 ? jyStyles.white : score.rank === 3 ? "#FFB06B" : jyStyles.white;
  return (
    <div style={{
      position: "absolute",
      left: `${13 + progress * 78}%`,
      top: "50%",
      width: 0,
      height: 0,
      zIndex: isLeader ? 14 : 10,
      transition: reducedMotion ? "none" : "left 82ms linear",
    }}>
      <div
        key={`${runId}-${score.raceId}-${visible.jumpCount}`}
        className={isRunning && visible.jumpCount > 0 ? "jy-race-hop" : ""}
        style={{
          position: "absolute",
          left: 0,
          top: compact ? -4 : -10,
          transform: "translate(-50%, -50%)",
          animation: isRunning && visible.jumpCount > 0 ? "jy-race-hop 270ms cubic-bezier(.2,.85,.25,1)" : "none",
        }}
      >
        {isRunning && visible.jumpCount > 0 && (
          <div className="jy-race-burst" style={{
            position: "absolute",
            left: "50%",
            top: "50%",
            width: mascotSize * 1.2,
            height: mascotSize * 1.2,
            border: `7px solid ${isLeader ? jyStyles.yellow : jyStyles.red}`,
            borderRadius: "50%",
            animation: "jy-race-burst 330ms ease-out both",
            zIndex: 0,
          }}/>
        )}
        <JYMascot
          item={score}
          size={mascotSize}
          imageScale={1.38}
          circleScale={0.82}
          bg={isLeader
            ? `radial-gradient(circle, ${hexA(jyStyles.yellow, 0.7)} 0%, #fff 72%)`
            : undefined}
          border={isLeader ? jyStyles.yellow : undefined}
        />
      </div>

      <div style={{
        position: "absolute",
        left: 0,
        top: compact ? 19 : medium ? 40 : 54,
        width: nameWidth,
        transform: "translateX(-50%)",
        background: jyStyles.white,
        border: `3px solid ${jyStyles.black}`,
        borderTop: `6px solid ${isLeader ? jyStyles.yellow : jyStyles.red}`,
        padding: compact ? "2px 6px 3px" : "5px 10px 6px",
        textAlign: "center",
        boxShadow: `4px 4px 0 ${hexA(jyStyles.black, 0.18)}`,
      }}>
        <div style={{
          ...jyFonts.display,
          color: jyStyles.black,
          fontSize: compact ? 15 : medium ? 20 : 24,
          lineHeight: 0.9,
          whiteSpace: "nowrap",
          overflow: "hidden",
          textOverflow: "ellipsis",
        }}>
          {score.name}
        </div>
        <div style={{
          ...jyFonts.num,
          color: jyStyles.black,
          fontSize: compact ? 12 : 17,
          lineHeight: 1,
          letterSpacing: 0,
          marginTop: 3,
          whiteSpace: "nowrap",
        }}>
          {visible.jumpCount} HOPP · {raceMeters(visible.distanceMeters)}
        </div>
      </div>

      <div style={{
        position: "absolute",
        left: compact ? -36 : -53,
        top: compact ? -32 : -53,
        width: compact ? 28 : 40,
        height: compact ? 28 : 40,
        borderRadius: "50%",
        display: "grid",
        placeItems: "center",
        background: placeColor,
        color: jyStyles.black,
        border: `3px solid ${jyStyles.black}`,
        ...jyFonts.num,
        fontSize: compact ? 17 : 24,
        letterSpacing: 0,
      }}>
        {score.rank}
      </div>
    </div>
  );
}

function JumpyRaceTrack({ scores, phase, runId, countdown, timeLeftMs, durationSeconds, onClose, reducedMotion }) {
  const leaderDistance = scores.reduce((largest, score) => Math.max(largest, score.distanceMeters), 0);
  const trackShift = Math.round(leaderDistance * 28);
  return (
    <div style={{ position: "absolute", inset: 0, background: jyStyles.white, overflow: "hidden" }}>
      <JumpyRaceHeader
        phase={phase}
        countdown={countdown}
        timeLeftMs={timeLeftMs}
        durationSeconds={durationSeconds}
        onClose={onClose}
      />

      <div style={{ position: "absolute", left: 0, right: 0, top: 142, bottom: 0, overflow: "hidden" }}>
        <div style={{
          position: "absolute",
          inset: 0,
          background: `repeating-linear-gradient(115deg, ${hexA(jyStyles.red, 0.035)} 0 26px, transparent 26px 54px)`,
          backgroundPosition: `${-trackShift}px 0`,
          transition: reducedMotion ? "none" : "background-position 160ms linear",
        }}/>

        <div style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: 0,
          height: 42,
          zIndex: 9,
          background: jyStyles.black,
          color: jyStyles.white,
          borderBottom: `4px solid ${jyStyles.red}`,
          display: "grid",
          gridTemplateColumns: "1fr 1fr 1fr",
          alignItems: "center",
          padding: "0 28px",
          ...jyFonts.label,
          fontSize: 15,
        }}>
          <span>{scores.length} HOPPARE</span>
          <span style={{ textAlign: "center", color: jyStyles.yellow }}>
            {phase === JUMPY_RACE_PHASE.COUNTDOWN ? `${raceDurationLabel(durationSeconds)} · LÄNGST VINNER` : "VARJE HOPP RÄKNAS"}
          </span>
          <span style={{ textAlign: "right" }}>LEDAREN {raceMeters(leaderDistance)}</span>
        </div>

        <div style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: 42,
          bottom: 18,
          display: "grid",
          gridTemplateRows: `repeat(${Math.max(1, scores.length)}, minmax(0, 1fr))`,
          borderTop: `4px solid ${jyStyles.black}`,
          borderBottom: `4px solid ${jyStyles.black}`,
          zIndex: 7,
        }}>
          {scores.map((score, index) => (
            <div key={score.raceId} style={{
              position: "relative",
              minHeight: 0,
              background: score.rank === 1 && score.jumpCount > 0 ? hexA(jyStyles.yellow, 0.18) : index % 2 ? "#FFF7F6" : jyStyles.white,
              borderBottom: index < scores.length - 1 ? `3px solid ${jyStyles.red}` : "none",
              overflow: "visible",
            }}>
              <div style={{
                position: "absolute",
                left: 22,
                top: "50%",
                transform: "translateY(-50%)",
                display: "flex",
                alignItems: "center",
                gap: 10,
              }}>
                <div style={{ ...jyFonts.num, fontSize: scores.length >= 8 ? 28 : 38, color: jyStyles.red, letterSpacing: 0 }}>
                  {String(index + 1).padStart(2, "0")}
                </div>
                <div style={{ width: 68, height: 8, background: jyStyles.black }}/>
              </div>
              <div style={{
                position: "absolute",
                left: "13%",
                top: "50%",
                width: "78%",
                height: 8,
                transform: "translateY(-50%)",
                background: hexA(jyStyles.black, 0.1),
                zIndex: 1,
              }}>
                <div style={{ width: `${score.progress * 100}%`, height: "100%", background: score.rank === 1 ? jyStyles.yellow : jyStyles.red }}/>
              </div>
              <JumpyRaceRunner
                score={score}
                runId={runId}
                phase={phase}
                racerCount={scores.length}
                reducedMotion={reducedMotion}
              />
            </div>
          ))}
        </div>

        {phase === JUMPY_RACE_PHASE.COUNTDOWN && (
          <div style={{
            position: "absolute",
            inset: 0,
            zIndex: 50,
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            gap: 24,
            background: hexA(jyStyles.white, 0.76),
          }}>
            <div style={{
              ...jyFonts.display,
              color: jyStyles.black,
              fontSize: 54,
              lineHeight: 0.9,
              letterSpacing: 0,
              textAlign: "center",
              textShadow: `4px 4px 0 ${jyStyles.yellow}`,
            }}>
              Hoppa så långt du kan
            </div>
            <div key={countdown} className="jy-race-count" style={{
              animation: "jy-race-count 360ms cubic-bezier(.15,.9,.2,1) both",
              width: 330,
              height: 330,
              borderRadius: "50%",
              background: countdown === 1 ? jyStyles.yellow : jyStyles.red,
              color: countdown === 1 ? jyStyles.black : jyStyles.white,
              border: `16px solid ${jyStyles.black}`,
              display: "grid",
              placeItems: "center",
              ...jyFonts.num,
              fontSize: countdown >= 10 ? 176 : 226,
              lineHeight: 1,
              letterSpacing: 0,
              boxShadow: `18px 18px 0 ${hexA(jyStyles.black, 0.24)}`,
            }}>
              {countdown}
            </div>
          </div>
        )}

        {phase === JUMPY_RACE_PHASE.FINISH && (
          <div style={{
            position: "absolute",
            inset: 0,
            zIndex: 55,
            display: "grid",
            placeItems: "center",
            background: hexA(jyStyles.red, 0.82),
          }}>
            <div className="jy-race-count" style={{
              animation: "jy-race-count 420ms cubic-bezier(.15,.9,.2,1) both",
              ...jyFonts.display,
              fontSize: 150,
              lineHeight: 0.75,
              color: jyStyles.white,
              letterSpacing: 0,
              textShadow: `13px 13px 0 ${jyStyles.black}`,
              transform: "skewX(-7deg)",
            }}>
              Tiden är ute!
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function JumpyRaceWaiting({ onClose, hasRacers }) {
  return (
    <div style={{ position: "absolute", inset: 0, background: jyStyles.white, display: "grid", placeItems: "center" }}>
      <div style={{ position: "absolute", inset: "0 0 auto 0", height: 138, background: jyStyles.black }} />
      <div style={{ position: "absolute", left: 38, top: 20 }}><JumpyRaceMiniWordmark light/></div>
      <JumpyRaceCloseButton onClose={onClose} dark/>
      <div style={{ textAlign: "center", width: 1100 }}>
        <div style={{ ...jyFonts.label, color: jyStyles.red, fontSize: 20 }}>TÄVLINGSLÄGE</div>
        <div style={{ ...jyFonts.display, color: jyStyles.black, fontSize: 100, lineHeight: 0.8, letterSpacing: 0 }}>
          {hasRacers ? "Alla är redo" : "Väntar på hoppare"}
        </div>
        <div style={{ ...jyFonts.body, color: jyStyles.black, fontSize: 24, marginTop: 30 }}>
          {hasRacers ? "Alla aktiva figurer är redo." : "Koppla minst ett band till en aktiv session."}
        </div>
      </div>
    </div>
  );
}

function JumpyRacePodiumPlace({ score, place, runId }) {
  if (!score) return <div/>;
  const heights = { 1: 330, 2: 245, 3: 205 };
  const colors = { 1: jyStyles.red, 2: jyStyles.black, 3: jyStyles.white };
  const textColors = { 1: jyStyles.white, 2: jyStyles.white, 3: jyStyles.black };
  const mascotSizes = { 1: 190, 2: 150, 3: 138 };
  return (
    <div className="jy-race-podium-entry" style={{
      alignSelf: "end",
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      justifyContent: "flex-end",
      animation: `jy-race-podium-up 560ms ${place * 110}ms cubic-bezier(.2,.85,.25,1) both`,
      position: "relative",
      zIndex: place === 1 ? 6 : 4,
    }}>
      {place === 1 && (
        <img
          src={JUMPY_RACE_ASSETS.trophy}
          alt=""
          aria-hidden="true"
          style={{
            position: "absolute",
            width: 320,
            height: 480,
            objectFit: "contain",
            left: "50%",
            bottom: 220,
            transform: "translateX(-50%)",
            opacity: 0.35,
            filter: "drop-shadow(0 18px 20px rgba(0,0,0,0.18))",
            zIndex: 0,
          }}
        />
      )}
      <div style={{ position: "relative", zIndex: 2, marginBottom: place === 1 ? -6 : 4 }}>
        <JYMascot item={score} size={mascotSizes[place]} imageScale={1.38} circleScale={0.82}/>
      </div>
      <div style={{
        ...jyFonts.display,
        color: jyStyles.black,
        fontSize: place === 1 ? 42 : 31,
        lineHeight: 0.9,
        width: 300,
        textAlign: "center",
        whiteSpace: "nowrap",
        overflow: "hidden",
        textOverflow: "ellipsis",
        position: "relative",
        zIndex: 2,
      }}>
        {score.name}
      </div>
      <div style={{ ...jyFonts.num, fontSize: place === 1 ? 29 : 23, letterSpacing: 0, color: jyStyles.red, margin: "6px 0 12px", position: "relative", zIndex: 2 }}>
        {raceMeters(score.distanceMeters)} · {score.jumpCount} HOPP
      </div>
      <div style={{
        width: place === 1 ? 410 : 350,
        height: heights[place],
        background: colors[place],
        color: textColors[place],
        border: `7px solid ${jyStyles.black}`,
        borderBottom: "none",
        display: "grid",
        placeItems: "start center",
        paddingTop: 24,
        boxShadow: place === 1 ? `0 0 0 9px ${jyStyles.yellow}` : "none",
        ...jyFonts.num,
        fontSize: place === 1 ? 124 : 94,
        lineHeight: 0.8,
        letterSpacing: 0,
      }}>
        {place}
      </div>
    </div>
  );
}

function JumpyRacePodium({ scores, runId, onClose }) {
  const ranked = [...scores].sort((left, right) => left.rank - right.rank);
  const winner = ranked[0];
  return (
    <div style={{ position: "absolute", inset: 0, overflow: "hidden", background: jyStyles.white }}>
      <Confetti count={180}/>
      <div style={{ position: "absolute", inset: "0 0 auto 0", height: 142, background: jyStyles.black, borderBottom: `10px solid ${jyStyles.yellow}` }}/>
      <div style={{ position: "absolute", left: 36, top: 20, zIndex: 10 }}><JumpyRaceMiniWordmark light/></div>
      <JumpyRaceCloseButton onClose={onClose} dark/>
      <div style={{ position: "absolute", left: 420, right: 420, top: 18, zIndex: 8, textAlign: "center" }}>
        <div style={{ ...jyFonts.label, color: jyStyles.yellow, fontSize: 16 }}>VINNARE</div>
        <div style={{ ...jyFonts.display, color: jyStyles.white, fontSize: 65, lineHeight: 0.83, letterSpacing: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
          {winner?.name || "Jumpy Race"}
        </div>
      </div>

      <div style={{ position: "absolute", left: 0, right: 0, top: 142, height: 150, background: jyStyles.red, borderBottom: `7px solid ${jyStyles.black}`, display: "grid", placeItems: "center" }}>
        <div style={{ ...jyFonts.display, fontSize: 82, lineHeight: 0.8, color: jyStyles.white, letterSpacing: 0, textShadow: `6px 6px 0 ${jyStyles.black}` }}>
          Vilket lopp!
        </div>
      </div>

      <div style={{
        position: "absolute",
        left: 260,
        right: 260,
        top: 294,
        bottom: 74,
        display: "grid",
        gridTemplateColumns: "1fr 1.15fr 1fr",
        alignItems: "end",
        gap: 28,
      }}>
        <JumpyRacePodiumPlace score={ranked[1]} place={2} runId={runId}/>
        <JumpyRacePodiumPlace score={ranked[0]} place={1} runId={runId}/>
        <JumpyRacePodiumPlace score={ranked[2]} place={3} runId={runId}/>
      </div>

      {ranked.length > 3 && (
        <div style={{
          position: "absolute",
          left: 22,
          top: 320,
          width: 300,
          background: jyStyles.white,
          border: `4px solid ${jyStyles.black}`,
          borderTop: `10px solid ${jyStyles.red}`,
          padding: "13px 15px",
          zIndex: 12,
        }}>
          <div style={{ ...jyFonts.label, fontSize: 14, color: jyStyles.red, marginBottom: 8 }}>SLUTRESULTAT</div>
          {ranked.slice(3).map(score => (
            <div key={score.raceId} style={{ display: "grid", gridTemplateColumns: "28px 1fr auto", alignItems: "center", gap: 7, borderTop: `2px solid ${hexA(jyStyles.black, 0.16)}`, padding: "7px 0" }}>
              <span style={{ ...jyFonts.num, fontSize: 21, letterSpacing: 0 }}>{score.rank}</span>
              <span style={{ ...jyFonts.display, fontSize: 17, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{score.name}</span>
              <span style={{ ...jyFonts.num, fontSize: 15, letterSpacing: 0 }}>{raceMeters(score.distanceMeters)}</span>
            </div>
          ))}
        </div>
      )}

    </div>
  );
}

function JumpyRaceController() {
  const { leaderboard, isLiveMode } = useJY();
  const options = React.useMemo(readJumpyRaceOptions, []);
  const reducedMotion = useReducedMotion();
  const [phase, setPhase] = React.useState(JUMPY_RACE_PHASE.CLOSED);
  const [roster, setRoster] = React.useState([]);
  const [demoOffsets, setDemoOffsets] = React.useState({});
  const [countdown, setCountdown] = React.useState(10);
  const [timeLeftMs, setTimeLeftMs] = React.useState(options.durationSeconds * 1000);
  const [runId, setRunId] = React.useState(0);
  const leaderboardRef = React.useRef(leaderboard);
  const autoStartedRef = React.useRef(false);
  leaderboardRef.current = leaderboard;

  const begin = React.useCallback(() => {
    const nextRoster = JumpyRaceEngine.createRaceRoster(leaderboardRef.current, options.maxRacers);
    setRoster(nextRoster);
    setDemoOffsets({});
    setCountdown(10);
    setTimeLeftMs(options.durationSeconds * 1000);
    setRunId(value => value + 1);
    setPhase(nextRoster.length > 0 ? JUMPY_RACE_PHASE.COUNTDOWN : JUMPY_RACE_PHASE.WAITING);
  }, [options.durationSeconds, options.maxRacers]);

  const close = React.useCallback(() => {
    setPhase(JUMPY_RACE_PHASE.CLOSED);
    setRoster([]);
    setDemoOffsets({});
  }, []);

  React.useEffect(() => {
    if (!options.autoStart || autoStartedRef.current || leaderboard.length === 0) return undefined;
    autoStartedRef.current = true;
    const timer = window.setTimeout(begin, 650);
    return () => window.clearTimeout(timer);
  }, [options.autoStart, leaderboard.length, begin]);

  React.useEffect(() => {
    const onKeyDown = (event) => {
      const target = event.target;
      if (target && ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)) return;
      if (event.key === "Escape" && phase !== JUMPY_RACE_PHASE.CLOSED) close();
      if ((event.key === "r" || event.key === "R") && [JUMPY_RACE_PHASE.CLOSED, JUMPY_RACE_PHASE.WAITING, JUMPY_RACE_PHASE.PODIUM].includes(phase)) begin();
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [phase, begin, close]);

  React.useEffect(() => {
    window.JumpyRace = { start: begin, close };
    return () => {
      if (window.JumpyRace?.start === begin) delete window.JumpyRace;
    };
  }, [begin, close]);

  React.useEffect(() => {
    if (phase !== JUMPY_RACE_PHASE.COUNTDOWN) return undefined;
    const startedAt = performance.now();
    setCountdown(10);
    const tick = () => {
      const elapsed = performance.now() - startedAt;
      const next = Math.max(1, 10 - Math.floor(elapsed / 1000));
      setCountdown(next);
      if (elapsed >= 10000) {
        window.clearInterval(interval);
        setRoster(current => JumpyRaceEngine.captureRaceBaseline(current, leaderboardRef.current));
        setTimeLeftMs(options.durationSeconds * 1000);
        setPhase(JUMPY_RACE_PHASE.RUNNING);
      }
    };
    const interval = window.setInterval(tick, 60);
    tick();
    return () => window.clearInterval(interval);
  }, [phase, options.durationSeconds]);

  React.useEffect(() => {
    if (phase !== JUMPY_RACE_PHASE.RUNNING) return undefined;
    setRoster(current => JumpyRaceEngine.updateRaceTotals(current, leaderboard));
    return undefined;
  }, [phase, leaderboard]);

  React.useEffect(() => {
    if (phase !== JUMPY_RACE_PHASE.RUNNING) return undefined;
    const durationMs = options.durationSeconds * 1000;
    const startedAt = performance.now();
    const tick = () => {
      const remaining = Math.max(0, durationMs - (performance.now() - startedAt));
      setTimeLeftMs(remaining);
      if (remaining <= 0) {
        window.clearInterval(interval);
        setPhase(JUMPY_RACE_PHASE.FINISH);
      }
    };
    const interval = window.setInterval(tick, 50);
    tick();
    return () => window.clearInterval(interval);
  }, [phase, options.durationSeconds]);

  React.useEffect(() => {
    if (phase !== JUMPY_RACE_PHASE.RUNNING || isLiveMode || roster.length === 0) return undefined;
    let turn = 0;
    const interval = window.setInterval(() => {
      const variance = Math.floor(Math.random() * Math.min(3, roster.length));
      const racer = roster[(turn + variance) % roster.length];
      turn += 1;
      const jumpMeters = 0.78 + Math.random() * 0.92;
      setDemoOffsets(current => {
        const previous = current[racer.raceId] || { jumps: 0, meters: 0 };
        return {
          ...current,
          [racer.raceId]: {
            jumps: previous.jumps + 1,
            meters: previous.meters + jumpMeters,
          },
        };
      });
    }, reducedMotion ? 620 : 330);
    return () => window.clearInterval(interval);
  }, [phase, isLiveMode, roster.length, runId, reducedMotion]);

  React.useEffect(() => {
    if (phase !== JUMPY_RACE_PHASE.FINISH) return undefined;
    const timer = window.setTimeout(() => setPhase(JUMPY_RACE_PHASE.PODIUM), reducedMotion ? 800 : 1900);
    return () => window.clearTimeout(timer);
  }, [phase, reducedMotion]);

  const scores = React.useMemo(
    () => JumpyRaceEngine.scoreRaceRoster(roster, demoOffsets),
    [roster, demoOffsets],
  );

  if (phase === JUMPY_RACE_PHASE.CLOSED) {
    return null;
  }

  return (
    <div className="jy-race-overlay" role="dialog" aria-modal="true" aria-label="Jumpy Race" style={{
      position: "absolute",
      inset: 0,
      zIndex: 80,
      overflow: "hidden",
      background: jyStyles.white,
      animation: "jy-race-overlay-in 320ms ease-out both",
    }}>
      <style>{JUMPY_RACE_CSS}</style>
      {phase === JUMPY_RACE_PHASE.WAITING && (
        <JumpyRaceWaiting onClose={close} hasRacers={leaderboard.length > 0}/>
      )}
      {[JUMPY_RACE_PHASE.COUNTDOWN, JUMPY_RACE_PHASE.RUNNING, JUMPY_RACE_PHASE.FINISH].includes(phase) && (
        <JumpyRaceTrack
          scores={scores}
          phase={phase}
          runId={runId}
          countdown={countdown}
          timeLeftMs={timeLeftMs}
          durationSeconds={options.durationSeconds}
          onClose={close}
          reducedMotion={reducedMotion}
        />
      )}
      {phase === JUMPY_RACE_PHASE.PODIUM && (
        <JumpyRacePodium scores={scores} runId={runId} onClose={close}/>
      )}
    </div>
  );
}

Object.assign(window, { JumpyRaceController });
