// Maze level — astronaut flies through a glowing maze collecting mini-planets.
// Canvas-based game loop, with a thin React HUD overlay.

const {
  useState: _mzUseState,
  useEffect: _mzUseEffect,
  useRef: _mzUseRef,
  useMemo: _mzUseMemo,
} = React;
// Aliases so the rest of this file reads naturally; avoid clobbering app.jsx's
// own destructured names since each Babel-standalone script has its own scope.
const useState = _mzUseState;
const useEffect = _mzUseEffect;
const useRef = _mzUseRef;
const useMemo = _mzUseMemo;
const MAZE_WIN_GOAL = 5;

function generateMaze(w, h) {
  const cells = Array.from({ length: h }, () =>
    Array.from({ length: w }, () => ({
      N: true,
      E: true,
      S: true,
      W: true,
      visited: false,
    })),
  );
  const stack = [{ x: 0, y: 0 }];
  cells[0][0].visited = true;
  while (stack.length) {
    const { x, y } = stack[stack.length - 1];
    const opts = [];
    if (y > 0 && !cells[y - 1][x].visited) opts.push({ x, y: y - 1, dir: "N" });
    if (x < w - 1 && !cells[y][x + 1].visited)
      opts.push({ x: x + 1, y, dir: "E" });
    if (y < h - 1 && !cells[y + 1][x].visited)
      opts.push({ x, y: y + 1, dir: "S" });
    if (x > 0 && !cells[y][x - 1].visited) opts.push({ x: x - 1, y, dir: "W" });
    if (!opts.length) {
      stack.pop();
      continue;
    }
    const n = opts[Math.floor(Math.random() * opts.length)];
    if (n.dir === "N") {
      cells[y][x].N = false;
      cells[n.y][n.x].S = false;
    }
    if (n.dir === "E") {
      cells[y][x].E = false;
      cells[n.y][n.x].W = false;
    }
    if (n.dir === "S") {
      cells[y][x].S = false;
      cells[n.y][n.x].N = false;
    }
    if (n.dir === "W") {
      cells[y][x].W = false;
      cells[n.y][n.x].E = false;
    }
    cells[n.y][n.x].visited = true;
    stack.push({ x: n.x, y: n.y });
  }
  return cells;
}

function mazeViewport() {
  return {
    width: window.visualViewport?.width || window.innerWidth,
    height: window.visualViewport?.height || window.innerHeight,
  };
}

function placePlanetsInMaze(cells, planetIds) {
  const H = cells.length;
  const W = cells[0].length;
  // Score every cell by distance from start (BFS); prefer dead-ends far from start.
  const dist = Array.from({ length: H }, () => Array(W).fill(-1));
  dist[0][0] = 0;
  const q = [{ x: 0, y: 0 }];
  while (q.length) {
    const { x, y } = q.shift();
    const c = cells[y][x];
    const tries = [
      { dx: 0, dy: -1, wall: c.N },
      { dx: 1, dy: 0, wall: c.E },
      { dx: 0, dy: 1, wall: c.S },
      { dx: -1, dy: 0, wall: c.W },
    ];
    for (const t of tries) {
      if (t.wall) continue;
      const nx = x + t.dx,
        ny = y + t.dy;
      if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
      if (dist[ny][nx] !== -1) continue;
      dist[ny][nx] = dist[y][x] + 1;
      q.push({ x: nx, y: ny });
    }
  }
  const candidates = [];
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      if (x === 0 && y === 0) continue;
      const c = cells[y][x];
      const walls =
        (c.N ? 1 : 0) + (c.E ? 1 : 0) + (c.S ? 1 : 0) + (c.W ? 1 : 0);
      candidates.push({ x, y, walls, d: dist[y][x] });
    }
  }
  // Prefer dead-ends, then far-from-start, with a bit of randomness.
  // Bake a stable random key per candidate so the sort comparator stays
  // transitive — calling Math.random() inside the comparator can return
  // different values for the same pair and produce undefined orderings.
  candidates.forEach((c) => {
    c._rand = Math.random();
  });
  candidates.sort(
    (a, b) => b.walls - a.walls || b.d - a.d || a._rand - b._rand,
  );
  // Spread placements across the maze: greedy max-min distance pick.
  const picked = [];
  for (const cand of candidates) {
    if (picked.length === 0) {
      picked.push(cand);
      continue;
    }
    const minDist = Math.min(
      ...picked.map((p) => Math.abs(p.x - cand.x) + Math.abs(p.y - cand.y)),
    );
    if (minDist >= 3) picked.push(cand);
    if (picked.length >= planetIds.length) break;
  }
  // If we didn't get enough spread cells, fall back to remaining candidates.
  while (picked.length < planetIds.length) {
    const rest = candidates.find((c) => !picked.includes(c));
    if (!rest) break;
    picked.push(rest);
  }
  return planetIds.map((id, i) => ({
    id,
    x: picked[i].x,
    y: picked[i].y,
    collected: false,
    bobPhase: Math.random() * Math.PI * 2,
  }));
}

function MazeLevel({ onExit }) {
  const canvasRef = useRef(null);
  const stateRef = useRef(null);
  const joyRef = useRef(null);
  const planetDivRefs = useRef({});
  const [collected, setCollected] = useState(0);
  const [total, setTotal] = useState(0);
  const [done, setDone] = useState(false);
  const [toast, setToast] = useState(null);
  const [foundIds, setFoundIds] = useState([]);
  const [mazePlanets, setMazePlanets] = useState([]);
  const [planetSize, setPlanetSize] = useState(56);

  const planetList = React.useMemo(() => PLANETS.filter((p) => !p.isStar), []);
  const visiblePlanetIcons = planetList.slice(
    0,
    Math.max(total, MAZE_WIN_GOAL),
  );
  const nextPlanet =
    planetList.find((planet) => !foundIds.includes(planet.id)) || null;
  const remaining = Math.max(0, total - collected);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");

    const fit = () => {
      const dpr = window.devicePixelRatio || 1;
      const vp = mazeViewport();
      canvas.width = vp.width * dpr;
      canvas.height = vp.height * dpr;
      canvas.style.width = vp.width + "px";
      canvas.style.height = vp.height + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    fit();

    const initialViewport = mazeViewport();
    const isNarrow = initialViewport.width <= 900;
    const targetCellPx = isNarrow ? 70 : 82;
    // Small maze size for preschool play: easy to scan, still scrolls if needed.
    const W = isNarrow
      ? 7
      : Math.min(10, Math.floor(initialViewport.width / targetCellPx) - 1);
    const H = isNarrow
      ? 9
      : Math.min(7, Math.floor(initialViewport.height / targetCellPx) - 1);
    const cells = generateMaze(Math.max(7, W), Math.max(6, H));
    const mazePlanetIds = planetList
      .slice(0, MAZE_WIN_GOAL)
      .map((planet) => planet.id);
    const planets = placePlanetsInMaze(cells, mazePlanetIds);

    const astroImg = new Image();
    astroImg.src = "data/sprites/astronaut-flying.png";

    const winGoal = Math.min(MAZE_WIN_GOAL, planets.length);
    setTotal(winGoal);
    setMazePlanets(planets.map((p) => ({ id: p.id, x: p.x, y: p.y })));
    setPlanetSize(Math.round(targetCellPx * 0.72));

    const state = {
      cells,
      W: cells[0].length,
      H: cells.length,
      cellPx: targetCellPx,
      astro: {
        x: targetCellPx * 0.5,
        y: targetCellPx * 0.5,
        vx: 0,
        vy: 0,
        angle: 0,
      },
      planets,
      keys: {},
      touchVx: 0,
      touchVy: 0,
      lastTime: performance.now(),
      collectedCount: 0,
      winGoal,
      done: false,
      sparks: [], // {x, y, vx, vy, life, color}
      doneTimer: null,
      disposed: false,
    };
    stateRef.current = state;

    // Round-3: Escape routed through the shared escape-stack so App's
    // base handler stays out of the way while the maze is on top.
    const popEscape =
      window.SpaceExplorerFoundation?.pushEscapeHandler?.(onExit) || (() => {});
    const onKeyDown = (e) => {
      if (
        ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(e.key)
      )
        e.preventDefault();
      state.keys[e.key.toLowerCase()] = true;
    };
    const onKeyUp = (e) => {
      state.keys[e.key.toLowerCase()] = false;
    };
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);

    // Virtual joystick — anchor wherever the drag first lands.
    let touchAnchor = null;
    let pointerAnchor = null;
    const joyEl = joyRef.current;
    const updateJoy = (cx, cy, ax, ay) => {
      if (!joyEl) return;
      joyEl.style.display = "block";
      joyEl.style.left = ax - 60 + "px";
      joyEl.style.top = ay - 60 + "px";
      const knob = joyEl.querySelector(".maze-joy-knob");
      if (knob) {
        const dx = cx - ax,
          dy = cy - ay;
        const len = Math.hypot(dx, dy);
        const max = 50;
        const k = len > max ? max / len : 1;
        knob.style.transform = `translate(${dx * k}px, ${dy * k}px)`;
      }
    };
    const hideJoy = () => {
      if (joyEl) joyEl.style.display = "none";
    };
    const applyJoyVector = (clientX, clientY, anchor) => {
      const dx = clientX - anchor.x;
      const dy = clientY - anchor.y;
      const len = Math.hypot(dx, dy);
      const SPEED = 205;
      if (len > 8) {
        const norm = Math.min(1, len / 60);
        state.touchVx = (dx / len) * SPEED * norm;
        state.touchVy = (dy / len) * SPEED * norm;
      } else {
        state.touchVx = 0;
        state.touchVy = 0;
      }
      updateJoy(clientX, clientY, anchor.x, anchor.y);
    };
    const onTouchStart = (e) => {
      const t = e.touches[0];
      touchAnchor = { x: t.clientX, y: t.clientY };
      updateJoy(t.clientX, t.clientY, t.clientX, t.clientY);
    };
    const onTouchMove = (e) => {
      if (!touchAnchor) return;
      e.preventDefault();
      const t = e.touches[0];
      applyJoyVector(t.clientX, t.clientY, touchAnchor);
    };
    const onTouchEnd = () => {
      touchAnchor = null;
      state.touchVx = 0;
      state.touchVy = 0;
      hideJoy();
    };
    canvas.addEventListener("touchstart", onTouchStart, { passive: false });
    canvas.addEventListener("touchmove", onTouchMove, { passive: false });
    canvas.addEventListener("touchend", onTouchEnd);
    canvas.addEventListener("touchcancel", onTouchEnd);
    const onPointerDown = (e) => {
      if (e.pointerType === "touch" || e.button !== 0) return;
      e.preventDefault();
      pointerAnchor = { x: e.clientX, y: e.clientY, id: e.pointerId };
      try {
        canvas.setPointerCapture(e.pointerId);
      } catch {}
      updateJoy(e.clientX, e.clientY, e.clientX, e.clientY);
    };
    const onPointerMove = (e) => {
      if (!pointerAnchor || pointerAnchor.id !== e.pointerId) return;
      e.preventDefault();
      applyJoyVector(e.clientX, e.clientY, pointerAnchor);
    };
    const onPointerEnd = (e) => {
      if (!pointerAnchor || pointerAnchor.id !== e.pointerId) return;
      pointerAnchor = null;
      state.touchVx = 0;
      state.touchVy = 0;
      hideJoy();
    };
    canvas.addEventListener("pointerdown", onPointerDown);
    canvas.addEventListener("pointermove", onPointerMove);
    canvas.addEventListener("pointerup", onPointerEnd);
    canvas.addEventListener("pointercancel", onPointerEnd);

    // Resize the canvas backing store on viewport changes (orientation flip
    // on tablet, browser zoom). Maze geometry stays put — kid keeps progress.
    const onResize = () => fit();
    window.addEventListener("resize", onResize);
    if (window.visualViewport) {
      window.visualViewport.addEventListener("resize", onResize);
    }

    let raf;
    function loop(now) {
      const dt = Math.min(0.05, (now - state.lastTime) / 1000);
      state.lastTime = now;
      tick(state, dt, now);
      render(ctx, state, astroImg, now);
      raf = requestAnimationFrame(loop);
    }

    function tick(s, dt, now) {
      let dx = 0,
        dy = 0;
      if (s.keys["arrowleft"] || s.keys["a"]) dx -= 1;
      if (s.keys["arrowright"] || s.keys["d"]) dx += 1;
      if (s.keys["arrowup"] || s.keys["w"]) dy -= 1;
      if (s.keys["arrowdown"] || s.keys["s"]) dy += 1;
      const len = Math.hypot(dx, dy);
      if (len > 0) {
        dx /= len;
        dy /= len;
      }
      const SPEED = 230;
      let targetVx = dx * SPEED;
      let targetVy = dy * SPEED;
      if (s.touchVx || s.touchVy) {
        targetVx = s.touchVx;
        targetVy = s.touchVy;
      }
      const accel = Math.min(1, dt * 14);
      s.astro.vx += (targetVx - s.astro.vx) * accel;
      s.astro.vy += (targetVy - s.astro.vy) * accel;

      const r = s.cellPx * 0.2;
      const newX = s.astro.x + s.astro.vx * dt;
      if (!collidesAt(s, newX, s.astro.y, r)) s.astro.x = newX;
      else s.astro.vx = 0;
      const newY = s.astro.y + s.astro.vy * dt;
      if (!collidesAt(s, s.astro.x, newY, r)) s.astro.y = newY;
      else s.astro.vy = 0;

      const speed = Math.hypot(s.astro.vx, s.astro.vy);
      if (speed > 25) {
        const target = Math.atan2(s.astro.vy, s.astro.vx);
        let diff = target - s.astro.angle;
        while (diff > Math.PI) diff -= 2 * Math.PI;
        while (diff < -Math.PI) diff += 2 * Math.PI;
        s.astro.angle += diff * Math.min(1, dt * 9);
      }

      // Pickup detection
      s.planets.forEach((p) => {
        if (p.collected) return;
        const cx = (p.x + 0.5) * s.cellPx;
        const cy = (p.y + 0.5) * s.cellPx;
        const dist = Math.hypot(s.astro.x - cx, s.astro.y - cy);
        if (dist < s.cellPx * 0.58) {
          p.collected = true;
          s.collectedCount += 1;
          const planet = PLANETS.find((pp) => pp.id === p.id);
          // Sparks burst — fall back to white if the planet metadata is gone
          // (defensive; PLANETS is the source of truth so this should never
          // hit, but it prevents a crash if planet IDs ever drift).
          const sparkColor = (planet && planet.color) || "#ffffff";
          for (let i = 0; i < 22; i++) {
            const a = Math.random() * Math.PI * 2;
            const sp = 60 + Math.random() * 160;
            s.sparks.push({
              x: cx,
              y: cy,
              vx: Math.cos(a) * sp,
              vy: Math.sin(a) * sp,
              life: 0.7 + Math.random() * 0.5,
              age: 0,
              color: sparkColor,
              size: 2 + Math.random() * 2,
            });
          }
          // Toast + short Grok TTS confirmation ("You found Mars!" etc.)
          setCollected(s.collectedCount);
          setFoundIds((arr) => [...arr, p.id]);
          setToast({ planet, key: now });
          if (window.__narration)
            window.__narration.play("found_" + p.id + ".mp3");
          if (s.collectedCount >= s.winGoal) {
            s.done = true;
            // Track the timer so unmount cancels it; otherwise the callback
            // can fire after cleanup, calling setDone on an unmounted
            // component and re-starting narration the cleanup just stopped.
            s.doneTimer = setTimeout(() => {
              s.doneTimer = null;
              if (s.disposed) return;
              setDone(true);
              if (window.__narration)
                window.__narration.play("game_maze_done.mp3");
            }, 850);
          }
        }
      });

      // Update sparks
      for (let i = s.sparks.length - 1; i >= 0; i--) {
        const sp = s.sparks[i];
        sp.age += dt;
        sp.x += sp.vx * dt;
        sp.y += sp.vy * dt;
        sp.vx *= 0.96;
        sp.vy *= 0.96;
        if (sp.age >= sp.life) s.sparks.splice(i, 1);
      }
    }

    function collidesAt(s, x, y, r) {
      if (x < r || y < r) return true;
      if (x > s.W * s.cellPx - r || y > s.H * s.cellPx - r) return true;
      const cx = Math.floor(x / s.cellPx);
      const cy = Math.floor(y / s.cellPx);
      if (cx < 0 || cy < 0 || cx >= s.W || cy >= s.H) return true;
      const localX = x - cx * s.cellPx;
      const localY = y - cy * s.cellPx;
      const c = s.cells[cy][cx];
      if (c.N && localY < r) return true;
      if (c.S && localY > s.cellPx - r) return true;
      if (c.W && localX < r) return true;
      if (c.E && localX > s.cellPx - r) return true;
      return false;
    }

    function render(ctx, s, img, now) {
      const vp = mazeViewport();
      const cw = vp.width;
      const ch = vp.height;
      const bottomSafe = cw <= 900 ? 118 : 84;
      const usableH = Math.max(260, ch - bottomSafe);
      ctx.fillStyle = "rgba(5, 4, 20, 0.32)";
      ctx.fillRect(0, 0, cw, ch);

      // Camera centered on astronaut, clamped to maze bounds.
      const mazeW = s.W * s.cellPx;
      const mazeH = s.H * s.cellPx;
      const camX =
        mazeW <= cw
          ? -(cw - mazeW) / 2
          : Math.max(0, Math.min(mazeW - cw, s.astro.x - cw / 2));
      const camY =
        mazeH <= usableH
          ? -(usableH - mazeH) / 2
          : Math.max(0, Math.min(mazeH - usableH, s.astro.y - usableH / 2));

      ctx.save();
      ctx.translate(-camX, -camY);

      const cp = s.cellPx;

      // Draw walls — glowing accent lines
      ctx.lineCap = "round";
      ctx.lineJoin = "round";
      ctx.strokeStyle = "rgba(255, 214, 107, 0.95)";
      ctx.lineWidth = 3;
      ctx.shadowColor = "rgba(255, 214, 107, 0.8)";
      ctx.shadowBlur = 14;
      ctx.beginPath();
      for (let y = 0; y < s.H; y++) {
        for (let x = 0; x < s.W; x++) {
          const c = s.cells[y][x];
          const x0 = x * cp,
            y0 = y * cp;
          if (c.N) {
            ctx.moveTo(x0, y0);
            ctx.lineTo(x0 + cp, y0);
          }
          if (c.W) {
            ctx.moveTo(x0, y0);
            ctx.lineTo(x0, y0 + cp);
          }
          if (c.S && y === s.H - 1) {
            ctx.moveTo(x0, y0 + cp);
            ctx.lineTo(x0 + cp, y0 + cp);
          }
          if (c.E && x === s.W - 1) {
            ctx.moveTo(x0 + cp, y0);
            ctx.lineTo(x0 + cp, y0 + cp);
          }
        }
      }
      ctx.stroke();
      ctx.shadowBlur = 0;

      // Goal cell marker (top-left corner = start; opposite corner = end / glow)
      const exitX = (s.W - 0.5) * cp;
      const exitY = (s.H - 0.5) * cp;
      if (s.done) {
        const pulse = 0.5 + 0.5 * Math.sin(now / 200);
        const grad = ctx.createRadialGradient(
          exitX,
          exitY,
          0,
          exitX,
          exitY,
          cp,
        );
        grad.addColorStop(0, `rgba(120, 220, 140, ${0.4 + pulse * 0.4})`);
        grad.addColorStop(1, "rgba(120, 220, 140, 0)");
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(exitX, exitY, cp, 0, Math.PI * 2);
        ctx.fill();
      }

      // Mini-planets are real Three.js spheres in DOM divs (see JSX).
      // Each frame, position each div in screen space and pulse slightly.
      const halfPlanet = Math.round(cp * 0.72) / 2;
      s.planets.forEach((p) => {
        const div = planetDivRefs.current[p.id];
        if (!div) return;
        const baseX = (p.x + 0.5) * cp;
        const baseY = (p.y + 0.5) * cp;
        const bob = Math.sin(now / 600 + p.bobPhase) * 3;
        const screenX = baseX - camX;
        const screenY = baseY - camY + bob;
        if (p.collected) {
          if (div.style.opacity !== "0") {
            div.style.opacity = "0";
            div.style.transform = `translate(${screenX - halfPlanet}px, ${screenY - halfPlanet}px) scale(0.4)`;
          }
        } else {
          const pulse = 1 + 0.04 * Math.sin(now / 380 + p.x * 1.7);
          div.style.transform = `translate(${screenX - halfPlanet}px, ${screenY - halfPlanet}px) scale(${pulse})`;
        }
      });

      // Sparks
      s.sparks.forEach((sp) => {
        const t = 1 - sp.age / sp.life;
        ctx.globalAlpha = Math.max(0, t);
        ctx.fillStyle = sp.color;
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, sp.size * (0.4 + t * 0.6), 0, Math.PI * 2);
        ctx.fill();
      });
      ctx.globalAlpha = 1;

      // Astronaut — source PNG is 1260×672 with 4×3 frames (315×224 each).
      const FW = 315,
        FH = 224;
      if (img.complete && img.naturalWidth > 0) {
        const speed = Math.hypot(s.astro.vx, s.astro.vy);
        const cycle = speed > 20 ? Math.floor(now / 180) % 4 : 0;
        // 65% of cell width — keeps the rotated bounding box clear of walls.
        const displayW = cp * 0.65;
        const displayH = displayW * (FH / FW);
        ctx.save();
        ctx.translate(s.astro.x, s.astro.y);
        ctx.rotate(s.astro.angle);
        ctx.shadowColor = "rgba(255, 180, 80, 0.5)";
        ctx.shadowBlur = 8;
        ctx.drawImage(
          img,
          cycle * FW,
          0,
          FW,
          FH,
          -displayW / 2,
          -displayH / 2,
          displayW,
          displayH,
        );
        ctx.restore();
      } else {
        ctx.fillStyle = "#fff";
        ctx.beginPath();
        ctx.arc(s.astro.x, s.astro.y, 6, 0, Math.PI * 2);
        ctx.fill();
      }

      ctx.restore();
    }

    raf = requestAnimationFrame(loop);

    return () => {
      state.disposed = true;
      if (state.doneTimer) {
        clearTimeout(state.doneTimer);
        state.doneTimer = null;
      }
      cancelAnimationFrame(raf);
      window.removeEventListener("resize", onResize);
      if (window.visualViewport) {
        window.visualViewport.removeEventListener("resize", onResize);
      }
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
      popEscape();
      canvas.removeEventListener("touchstart", onTouchStart);
      canvas.removeEventListener("touchmove", onTouchMove);
      canvas.removeEventListener("touchend", onTouchEnd);
      canvas.removeEventListener("touchcancel", onTouchEnd);
      canvas.removeEventListener("pointerdown", onPointerDown);
      canvas.removeEventListener("pointermove", onPointerMove);
      canvas.removeEventListener("pointerup", onPointerEnd);
      canvas.removeEventListener("pointercancel", onPointerEnd);
      if (window.__narration) window.__narration.stop();
    };
  }, []);

  // Auto-dismiss the toast — held long enough for the ~3s Grok clip.
  // Use a stable string key so React's dep comparison doesn't flip between
  // `false` (no toast) and a number (toast.key timestamp).
  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 2600);
    return () => clearTimeout(t);
  }, [toast ? toast.key : 0]);

  const replay = () => {
    setCollected(0);
    setFoundIds([]);
    setDone(false);
    setToast(null);
    // Force fresh maze by triggering an unmount/mount via key change in parent.
    if (typeof onExit === "function") onExit({ replay: true });
  };

  return (
    <div className="maze-level">
      <canvas ref={canvasRef} className="maze-canvas" />
      <div className="maze-planets-layer">
        {mazePlanets.map((p) => {
          const planet = PLANETS.find((pp) => pp.id === p.id);
          if (!planet) return null;
          const isNext = nextPlanet && nextPlanet.id === p.id;
          return (
            <div
              key={p.id}
              className={`maze-planet ${isNext ? "next" : ""}`}
              ref={(el) => {
                planetDivRefs.current[p.id] = el;
              }}
              style={{
                width: planetSize,
                height: planetSize,
                ["--glow"]: planet.glowColor || planet.color,
                ["--planet-color"]: planet.color,
              }}
            >
              <Planet3D
                planet={planet}
                detail={false}
                allowOrbit={false}
                className="maze-planet-canvas"
              />
            </div>
          );
        })}
      </div>
      <div className="maze-hud-top">
        <button className="maze-btn-back" onClick={() => onExit()}>
          ← Back
        </button>
        <div className="maze-counter">
          <span className="maze-counter-num">{collected}</span>
          <span className="maze-counter-sep">/</span>
          <span className="maze-counter-total">{total}</span>
          <span className="maze-counter-label">planets</span>
        </div>
        <div className="maze-icons">
          {visiblePlanetIcons.map((p) => {
            const found = foundIds.includes(p.id);
            return (
              <div
                key={p.id}
                className={`maze-icon ${found ? "found" : ""}`}
                style={{
                  background: found
                    ? `radial-gradient(circle at 35% 35%, #fff, ${p.color})`
                    : "rgba(255,255,255,0.08)",
                  borderColor: found ? p.color : "rgba(255,255,255,0.15)",
                }}
                title={p.name}
              />
            );
          })}
        </div>
      </div>

      <div ref={joyRef} className="maze-joy" style={{ display: "none" }}>
        <div className="maze-joy-knob" />
      </div>

      {!done && nextPlanet ? (
        <div className="maze-next-card" role="status" aria-live="polite">
          <span>Find</span>
          <div
            className="maze-next-dot"
            style={{
              background: `radial-gradient(circle at 35% 35%, #fff, ${nextPlanet.color})`,
              boxShadow: `0 0 18px ${nextPlanet.glowColor || nextPlanet.color}`,
            }}
          />
          <strong>{nextPlanet.name}</strong>
          <em>{remaining} left</em>
        </div>
      ) : null}

      {toast && (
        <div className="maze-toast" key={toast.key}>
          <span className="maze-toast-text">Found</span>
          <span
            className="maze-toast-name"
            style={{ color: toast.planet.color }}
          >
            {toast.planet.name}
          </span>
          <span className="maze-toast-emoji">✨</span>
        </div>
      )}

      {done && (
        <div className="maze-victory">
          <div className="maze-victory-card">
            <div className="maze-victory-emoji">🚀</div>
            <h1>Mission complete!</h1>
            <p>You found five planets. Great exploring!</p>
            <div className="maze-victory-row">
              {visiblePlanetIcons.map((p) => (
                <div
                  key={p.id}
                  className="maze-victory-planet"
                  style={{
                    background: `radial-gradient(circle at 35% 35%, #fff, ${p.color})`,
                  }}
                  title={p.name}
                />
              ))}
            </div>
            <div className="maze-victory-actions">
              <button className="maze-btn-primary" onClick={replay}>
                Play again
              </button>
              <button className="maze-btn-secondary" onClick={() => onExit()}>
                Back to system
              </button>
            </div>
          </div>
        </div>
      )}

      <div className="maze-hint">Drag or use arrows</div>
    </div>
  );
}
