// ============================================================
// HUNGRY HOLE 3D — game screens: HUD, LevelComplete, OutOfTime, BonusIntro
// ============================================================
const { useState: useStateG, useEffect: useEffectG, useRef: useRefG } = React;

// Full circular AI-generated power-up badges (glossy glass disc, icon baked in).
// Optimized WebP (~9KB each). Rendered as the whole tray button, no CSS tile.
const PU_V = '?v=129';
const PU_BADGE = {
  magnet:   'assets/ui-ai/pu-magnet.webp'  + PU_V,
  time:     'assets/ui-ai/pu-time.webp'    + PU_V,
  slow:     'assets/ui-ai/pu-slow.webp'    + PU_V,
  frenzy:   'assets/ui-ai/pu-frenzy.webp'  + PU_V,
  bomb:     'assets/ui-ai/pu-blast.webp'   + PU_V,
  grow:     'assets/ui-ai/pu-grow.webp'    + PU_V,
  shield:   'assets/ui-ai/pu-shield.webp'  + PU_V,
  aegis:    'assets/ui-ai/pu-shield.webp'  + PU_V,  // Aegis is a shield variant → same badge
  tornado:  'assets/ui-ai/pu-tornado.webp' + PU_V,
  titan:    'assets/ui-ai/pu-titan.webp'   + PU_V,
  colossus: 'assets/ui-ai/pu-titan.webp'   + PU_V,  // Colossus is a titan variant → same badge
};

// ---------- 5 · Gameplay HUD ----------
function Gameplay({ go, world, level, planet = 0, skin, finish, timeout, equipped }) {
  const HH = window.HH;
  // PLANET re-tint: the engine reskins the whole scene from this world object.
  const W = (HH.worldView ? HH.worldView(world, planet) : HH.WORLDS[world]);
  const PL = HH.planetForIndex ? HH.planetForIndex(planet) : null;
  const lv = HH.LEVELS.flat().find(l => l[0] === level) || HH.LEVELS[0][0];
  const goal = HH.GOALS[lv[2]];
  const D = HH.DIFFICULTY(level, planet);       // ×planet difficulty ramp
  // per-level challenge modifier (anti-repeat); timeMul + frenzyStart are applied here, the rest in-engine
  const CH = (HH.challengeFor ? HH.challengeFor(level) : { name: 'Classic', color: 'var(--sky)', desc: '', mods: {} });
  const cm = CH.mods || {};
  const eq = (equipped && equipped.length ? equipped : (HH.equipped || ['magnet'])).slice(0, 3);
  const puById = Object.fromEntries(HH.POWERUPS.map(p => [p.id, p]));

  const [live, setLive] = useStateG({ score: 0, combo: 0, eaten: 0, total: D.count, holeTier: 1, remaining: D.count, touched: false });
  // One-shot continue bonus (earned via rewarded ad on a previous run's Out-of-Time screen, fallback path)
  const [time, setTime] = useStateG(() => { const b = HH._bonusTime || 0; HH._bonusTime = 0; return Math.round(D.time * (cm.timeMul || 1)) + b; });
  const [paused, setPaused] = useStateG(false);
  // in-place "Out of Time" overlay: when the clock hits 0 we PAUSE the run (don't route away)
  // and offer Continue-after-ad. true = overlay up; the run is frozen until a choice is made.
  const [outOfTime, setOutOfTime] = useStateG(false);
  const [continueAd, setContinueAd] = useStateG(false); // rewarded-ad modal open for the continue flow
  const [callout, setCallout] = useStateG(null);
  const [charges, setCharges] = useStateG(() => Object.fromEntries(eq.map(id => [id, puById[id] ? puById[id].charges : 1])));
  const [active, setActive] = useStateG({});            // { frenzy:endTs, slow:endTs, magnet:endTs }
  const [scoreMult, setScoreMult] = useStateG(1);
  const [reachBoost, setReachBoost] = useStateG(0);
  const [slow, setSlow] = useStateG(false);
  const [bombSignal, setBombSignal] = useStateG(0);
  const [growSignal, setGrowSignal] = useStateG(0);
  const [titanSignal, setTitanSignal] = useStateG(0);
  const [shield, setShield] = useStateG(eq.includes('shield'));
  const [adFor, setAdFor] = useStateG(null);             // power id needing refill
  const [flash, setFlash] = useStateG(null);
  const [milestone, setMilestone] = useStateG(null);     // "Halfway there!" / "Almost there!" banner
  const [rush, setRush] = useStateG(false);              // final-10s "×2 RUSH" comeback mode
  const [tierBanner, setTierBanner] = useStateG(null);   // named size-tier banner ("BOULDER")
  const lastCombo = useRefG(0);
  const lastTier = useRefG('');
  const m50 = useRefG(false), m90 = useRefG(false), lastKills = useRefG(0), wasDanger = useRefG(false);
  const liveRef = useRefG(live); liveRef.current = live;
  const wonRef = useRefG(false);
  const playSecRef = useRefG(0);      // cumulative seconds of ACTIVE play (pauses don't count)
  const lastTimedRef = useRefG(0);    // playSec at which the last timed interstitial fired
  const target = D.target;

  // CHALLENGE "Frenzy Start": open the level already in a ×2 score frenzy for a few seconds (feel-good kick).
  useEffectG(() => {
    if (!cm.frenzyStart) return;
    setScoreMult(2); setActive(a => ({ ...a, frenzy: Date.now() + cm.frenzyStart * 1000 }));
    const t = setTimeout(() => { setScoreMult(1); setActive(a => { const n = { ...a }; delete n.frenzy; return n; }); }, cm.frenzyStart * 1000);
    return () => clearTimeout(t);
  }, []);

  // timer (slow-mo aware) — does NOT tick until the player's first touch, and freezes
  // while paused / out-of-time overlay is up. Re-runs (restarts the interval) when those clear.
  useEffectG(() => {
    if (paused || outOfTime || !live.touched) return;
    const iv = setInterval(() => setTime(t => {
      const dec = 0.1 * (slow ? 0.45 : 1);
      if (t - dec <= 0) {
        if (shield) { setShield(false); setFlash('Shield saved you! +10s'); setTimeout(() => setFlash(null), 1400); return t + 10; }
        // out of time: clear THIS interval and freeze the run in place (don't route away).
        // The in-place OutOfTime overlay below offers Continue-after-ad / Retry / Map.
        clearInterval(iv);
        if (!wonRef.current) { setPaused(true); setOutOfTime(true); }
        return 0;
      }
      return t - dec;
    }), 100);
    return () => clearInterval(iv);
  }, [paused, outOfTime, slow, shield, live.touched]);

  // Paced full-screen interstitial on long runs: a CLOSEABLE ad ~every 5 min of active
  // play (native only; no-op in browser). Accumulates only unpaused seconds so pausing
  // never resets progress; HHAds also enforces a gap so it won't stack on level-change ads.
  // The native ad backgrounds the webview, so the engine's rAF auto-pauses while it's up.
  useEffectG(() => {
    if (paused || outOfTime || !live.touched) return;
    const iv = setInterval(() => {
      playSecRef.current += 1;
      if (playSecRef.current - lastTimedRef.current >= 180) {   // 3 minutes of active play
        lastTimedRef.current = playSecRef.current;
        if (window.HHAds && window.HHAds.showTimedInterstitial) window.HHAds.showTimedInterstitial();
      }
    }, 1000);
    return () => clearInterval(iv);
  }, [paused, outOfTime, live.touched]);

  // --- Continue-after-ad: grant 30s, hide the overlay, and RESUME the same run ---
  // We do NOT route away: the engine + score + progress are untouched. Clearing `outOfTime`
  // and `paused` re-runs the timer effect above, restarting the interval from the bonus time.
  const doContinue = () => {
    const bonus = (HH.grantContinue ? HH.grantContinue(level) : (HH.CONTINUE_BONUS_SEC || 30));
    setTime(t => Math.max(t, 0) + bonus);   // add the bonus seconds to the (now-zero) clock
    setContinueAd(false);
    setOutOfTime(false);
    setPaused(false);                        // unpause → engine resumes, timer interval restarts
    setFlash('+' + bonus + 's — keep going!'); setTimeout(() => setFlash(null), 1400);
  };
  // Player declined / chose Retry or Map → fall through to the existing route behaviour.
  const bailTimeout = () => { const L = liveRef.current; timeout({ score: L.score, eaten: L.eaten, total: L.total, target, level }); };

  // power-up effect timers → drop boosts when expired
  useEffectG(() => {
    const iv = setInterval(() => {
      const now = Date.now();
      setActive(a => { const n = { ...a }; let ch = false;
        Object.keys(n).forEach(k => { if (n[k] && n[k] < now) { delete n[k]; ch = true;
          if (k === 'frenzy') setScoreMult(1); if (k === 'magnet' || k === 'tornado') setReachBoost(0); if (k === 'slow') setSlow(false); } });
        return ch ? n : a; });
    }, 200);
    return () => clearInterval(iv);
  }, []);

  // win on reaching target score
  useEffectG(() => {
    if (!wonRef.current && live.score >= target) { wonRef.current = true;
      setTimeout(() => finish({ score: liveRef.current.score, eaten: liveRef.current.eaten, total: liveRef.current.total, target, level, hitTarget: true }), 400); }
  }, [live.score]);

  // COMBO TIER callouts — feel-good escalating chain feedback (centered announcer, can't overflow).
  // Fires once each time the live chain crosses a tier upward; the score multiplier scales in-engine.
  useEffectG(() => {
    const c = live.combo, p = lastCombo.current;
    const tiers = [[24, '24 COMBO'], [16, '16 COMBO'], [10, '10 COMBO'], [5, 'COMBO']];
    for (const [thr, word] of tiers) { if (c >= thr && p < thr) { setCallout(word); if (window.HHHaptics) try { window.HHHaptics.tap(); } catch (e) {} setTimeout(() => setCallout(null), 1100); break; } }
    lastCombo.current = c;
  }, [live.combo]);

  // 50% / 90% milestone banners — hole.io "Halfway there!" (mint) / "Almost there!" (gold)
  useEffectG(() => {
    if (!m50.current && live.score >= target * 0.5 && live.score < target) { m50.current = true; setMilestone('50%'); setTimeout(() => setMilestone(null), 1800); }
    if (!m90.current && live.score >= target * 0.9 && live.score < target) { m90.current = true; setMilestone('90%'); setTimeout(() => setMilestone(null), 1900); }
  }, [live.score]);

  // FINAL RUSH — when the clock dips under 10s the run flips into an auto ×2 "comeback" mode:
  // a pulsing red vignette + banner turn dead time into the most exciting stretch of the level.
  useEffectG(() => {
    if (!rush && live.touched && !outOfTime && !wonRef.current && time > 0 && time <= 10) {
      setRush(true); setScoreMult(m => Math.max(m, 2));
      setCallout('RUSH x2'); if (window.HHHaptics) try { window.HHHaptics.tap(); } catch (e) {}
      setTimeout(() => setCallout(null), 1500);
    }
    if (rush && time <= 0) setRush(false);
  }, [time, live.touched, outOfTime]);

  // Named size-tier banner — celebrates each milestone (Pebble→Megacity) the engine reports.
  useEffectG(() => {
    const t = live.tierName;
    if (t && lastTier.current && t !== lastTier.current) { setTierBanner(t); setTimeout(() => setTierBanner(null), 1600); }
    if (t) lastTier.current = t;
  }, [live.tierName]);

  // (danger-zone text flash removed per request — no on-screen "Danger Zone" banner)

  // kill-streak announcer — fires when the player swallows a rival hole
  useEffectG(() => {
    if (live.kills > lastKills.current) {
      const words = ['KO', 'DOUBLE KO', 'TRIPLE KO', 'MEGA KO', 'STREAK', 'DOMINATE'];
      setCallout(words[Math.min(live.kills - 1, words.length - 1)]);
      lastKills.current = live.kills;
      setTimeout(() => setCallout(null), 1300);
    }
  }, [live.kills]);

  const onEmpty = (s) => { if (!wonRef.current) { wonRef.current = true; finish({ score: s, eaten: live.total, total: live.total, perfect: true, target, level }); } };

  const fire = (id) => {
    const now = Date.now();
    if (id === 'magnet') { setReachBoost(72); setActive(a => ({ ...a, magnet: now + 8000 })); }
    else if (id === 'time') setTime(t => t + 8);
    else if (id === 'slow') { setSlow(true); setActive(a => ({ ...a, slow: now + 6000 })); }
    else if (id === 'frenzy') { setScoreMult(2); setActive(a => ({ ...a, frenzy: now + 8000 })); }
    else if (id === 'bomb') setBombSignal(s => s + 1);
    else if (id === 'grow') setGrowSignal(s => s + 1);
    else if (id === 'shield') setShield(true);
    else if (id === 'tornado') { setReachBoost(200); setActive(a => ({ ...a, tornado: now + 6000 })); }
    else if (id === 'titan') setTitanSignal(s => s + 1);
    // ---- new open-world rivals power-ups ----
    else if (id === 'vortex') { setReachBoost(160); setTimeout(() => setReachBoost(0), 4000); }
    else if (id === 'blackhole') { setReachBoost(400); setTimeout(() => setReachBoost(0), 3000); }
    else if (id === 'dash') { setReachBoost(120); setTimeout(() => setReachBoost(0), 2500); }
    else if (id === 'goldrush') { setScoreMult(3); setTimeout(() => setScoreMult(1), 8000); }
    else if (id === 'phase' || id === 'aegis') { setShield(true); }      // protection (HUD-level + timer save)
    else if (id === 'scout') { /* reveal nudge — surfaced via the flash below */ }
    // ---- city / weather power-ups ----
    else if (id === 'quake') { try { window.__HHENG && window.__HHENG.bomb && window.__HHENG.bomb(); } catch (e) {} }
    else if (id === 'freeze') { try { window.__HHENG && window.__HHENG.freeze && window.__HHENG.freeze(5000); } catch (e) {} }
    else if (id === 'storm') { setReachBoost(300); setTimeout(() => setReachBoost(0), 5000); }
    else if (id === 'magnetx') { setReachBoost(140); setTimeout(() => setReachBoost(0), 12000); }
    else if (id === 'rush') { setScoreMult(5); setTimeout(() => setScoreMult(1), 6000); }
    else if (id === 'colossus') { setTitanSignal(s => s + 1); }
    setFlash(puById[id].name + '!'); setTimeout(() => setFlash(null), 900);
  };
  const tap = (id) => {
    if ((charges[id] || 0) > 0) { setCharges(c => ({ ...c, [id]: c[id] - 1 })); fire(id); }
    else setAdFor(id);   // out of charges → watch an ad to refill (ad economy)
  };

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <Board3D world={W} level={level} skin={skin} interactive={!paused} onUpdate={setLive} onEmpty={onEmpty}
        scoreMult={scoreMult} reachBoost={reachBoost} bombSignal={bombSignal} growSignal={growSignal} titanSignal={titanSignal}
        onPickup={(id) => { if (id === 'time') setTime(t => t + 8);
          const nm = (puById[id] || {}).name || id.toUpperCase();
          setFlash(nm + '!'); setTimeout(() => setFlash(null), 900); }} />

      {/* planet badge — only on Mars and beyond (Earth stays clean) */}
      {PL && PL.isNew && (
        <div style={{ position: 'absolute', top: 62, left: 0, right: 0, zIndex: 50, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '5px 13px 5px 10px', borderRadius: 100,
            background: 'linear-gradient(180deg,rgba(12,20,38,0.82),rgba(7,12,24,0.72))', border: '1px solid rgba(255,255,255,0.16)',
            boxShadow: '0 4px 12px rgba(0,0,0,0.35)' }}>
            <span style={{ fontSize: 15 }}>{PL.icon}</span>
            <span className="disp" style={{ fontSize: 13, letterSpacing: 0.8, color: '#fff' }}>{PL.name}</span>
          </div>
        </div>
      )}

      {/* (removed the floating TARGET pop-up banner — it overlapped the score/progress bar;
          the progress bar already shows value / target, so this was redundant) */}

      {/* milestone banners */}
      {milestone && (
        <div key={milestone} style={{ position: 'absolute', top: 158, left: 0, right: 0, zIndex: 49, display: 'flex', justifyContent: 'center', pointerEvents: 'none', animation: 'hh-pop .34s cubic-bezier(.34,1.56,.64,1) both' }}>
          <div className={'hh-milestone ' + (milestone === '90%' ? 'gold' : 'mint')}
            style={{ fontSize: 28, WebkitTextStroke: '2px rgba(16,26,46,0.8)', paintOrder: 'stroke fill', textShadow: '0 3px 0 rgba(16,26,46,0.45), 0 8px 18px rgba(0,0,0,0.32)' }}>{milestone}</div>
        </div>
      )}

      {/* named size-tier banner — "BOULDER", "DISTRICT"… on every milestone growth */}
      {tierBanner && (
        <div key={tierBanner} style={{ position: 'absolute', top: '34%', left: 0, right: 0, zIndex: 57, display: 'flex', justifyContent: 'center', pointerEvents: 'none', animation: 'hh-pop .42s cubic-bezier(.34,1.56,.64,1) both' }}>
          <div className="disp" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, fontSize: 30, letterSpacing: 0.5,
            background: 'linear-gradient(180deg,#FFE6A0,#FFB627)', WebkitBackgroundClip: 'text', backgroundClip: 'text', color: 'transparent',
            filter: 'drop-shadow(0 3px 5px rgba(120,70,0,0.45))' }}>
            {tierBanner.toUpperCase()}
          </div>
        </div>
      )}

      {/* ready state: the level waits for the player — no auto-play, timer paused */}
      {!live.touched && !paused && (
        <div style={{ position: 'absolute', inset: 0, zIndex: 48, pointerEvents: 'none', display: 'flex', alignItems: 'flex-end', justifyContent: 'center', paddingBottom: 150 }}>
          {/* simple hint: icon + text only (no button plate) */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, animation: 'hh-bob 2.2s ease-in-out infinite' }}>
            <I.drag width={26} height={26} style={{ color: '#fff', filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.55))' }} />
            <span className="disp" style={{ fontSize: 22, color: '#fff', letterSpacing: 1.4, textShadow: '0 2px 5px rgba(0,0,0,0.6), 0 1px 0 rgba(0,0,0,0.4)' }}>DRAG</span>
          </div>
        </div>
      )}

      {/* frenzy / slow tints */}
      {active.frenzy && <div style={{ position: 'absolute', inset: 0, zIndex: 45, pointerEvents: 'none', boxShadow: 'inset 0 0 90px rgba(255,182,39,0.5)' }} />}
      {active.slow && <div style={{ position: 'absolute', inset: 0, zIndex: 45, pointerEvents: 'none', background: 'rgba(124,92,252,0.12)' }} />}
      {/* FINAL RUSH — pulsing coral vignette + floating ×2 badge while the clock runs out */}
      {rush && time > 0 && <div style={{ position: 'absolute', inset: 0, zIndex: 46, pointerEvents: 'none', boxShadow: 'inset 0 0 110px rgba(255,75,75,0.55)', animation: 'hh-glowpulse 0.7s ease-in-out infinite' }} />}
      {rush && time > 0 && <div style={{ position: 'absolute', top: 'calc(env(safe-area-inset-top, 0px) + 116px)', left: 0, right: 0, zIndex: 51, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
        <div className="disp" style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 15, letterSpacing: 0.6, color: '#fff', padding: '6px 16px', borderRadius: 100, background: 'linear-gradient(180deg,#FF6B6B,#E23B3B)', boxShadow: '0 4px 14px rgba(226,59,59,0.5), inset 0 1px 0 rgba(255,255,255,0.35)', animation: 'hh-bob 0.9s ease-in-out infinite' }}><I.bolt width={15} height={15} style={{ color: '#FFE08A' }} /> RUSH x2</div>
      </div>}

      {/* ===== top HUD — pinned to the very top edge (safe-area aware), mobile-tidy ===== */}
      {/* row 1: timer ring hugs the TOP-LEFT edge · pause+map hug the TOP-RIGHT edge (tight gap) */}
      <div style={{ position: 'absolute', top: 'calc(env(safe-area-inset-top, 0px) + 8px)', left: 12, right: 12, zIndex: 50, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8, pointerEvents: 'none' }}>
        {/* top-LEFT edge: timer ring */}
        <div style={{ display: 'flex', alignItems: 'center', pointerEvents: 'auto' }}>
          <TimerRing frac={time / D.time} label={(() => { const s = Math.max(0, Math.ceil(time)); const m = Math.floor(s / 60); return m > 0 ? (m + ':' + String(s % 60).padStart(2, '0')) : String(s); })()} color={time < 15 ? 'var(--coral)' : slow ? 'var(--grape)' : 'var(--mint)'} />
        </div>

        {/* top-RIGHT edge: pause + map */}
        <div style={{ display: 'flex', gap: 7, pointerEvents: 'auto' }}>
          <button aria-label="Pause" className="hh-gamehud-btn" onClick={() => setPaused(true)} style={{ width: 52, height: 52, borderRadius: 16, border: '1px solid rgba(255,255,255,0.18)', display: 'grid', placeItems: 'center', cursor: 'pointer',
            color: '#fff', background: 'linear-gradient(180deg,rgba(19,30,54,0.88),rgba(10,16,32,0.92))',
            boxShadow: '0 8px 18px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.18)' }}><I.pause width={24} height={24} /></button>
          <button aria-label="Map" className="hh-gamehud-btn" onClick={() => go('map')} style={{ width: 52, height: 52, borderRadius: 16, border: '1px solid rgba(255,255,255,0.18)', display: 'grid', placeItems: 'center', cursor: 'pointer',
            color: '#fff', background: 'linear-gradient(180deg,rgba(19,30,54,0.88),rgba(10,16,32,0.92))',
            boxShadow: '0 8px 18px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.18)' }}><I.map width={24} height={24} /></button>
        </div>
      </div>

      {/* row 2: "SCORE" eyebrow + big white outlined score, then the full-width progress bar.
          ProgressBar carries the green→yellow fill, leading sparkle and the value/target text. */}
      <div style={{ position: 'absolute', top: 'calc(env(safe-area-inset-top, 0px) + 64px)', left: 24, right: 24, zIndex: 49, pointerEvents: 'none' }}>
        <span className="disp" style={{ display: 'block', fontSize: 12, letterSpacing: 1.4, color: '#fff', textShadow: '0 1px 3px rgba(0,0,0,0.5)', marginBottom: 1 }}>SCORE</span>
        <div className="num" style={{ fontSize: 30, fontWeight: 700, color: '#fff', lineHeight: 1, letterSpacing: 0.3,
          WebkitTextStroke: '2px rgba(16,26,46,0.85)', paintOrder: 'stroke fill', marginBottom: 5 }}>{live.score.toLocaleString()}</div>
        <ProgressBar value={live.score} target={target} h={14} />
      </div>

      {/* power-up flash */}
      {flash && <div key={flash} style={{ position: 'absolute', top: '30%', left: '50%', transform: 'translateX(-50%)', zIndex: 56, pointerEvents: 'none', animation: 'hh-pop .34s cubic-bezier(.34,1.56,.64,1)' }}>
        <div className="disp" style={{ fontSize: 25, color: '#fff', WebkitTextStroke: '2px rgba(16,26,46,0.85)', paintOrder: 'stroke fill',
          textShadow: '0 3px 0 rgba(16,26,46,0.45), 0 8px 18px rgba(0,0,0,0.34)' }}>{String(flash).replace('!', '')}</div></div>}

      {/* kill-streak announcer — fires on rival swallows (centered, can't overflow) */}
      {callout && (
        <div style={{ position: 'absolute', top: '40%', left: '50%', zIndex: 58, pointerEvents: 'none', animation: 'hh-announce 1.3s ease-out forwards' }}>
          <div className="disp" style={{ fontSize: 40, whiteSpace: 'nowrap', color: '#fff',
            WebkitTextStroke: '2.5px rgba(16,26,46,0.9)', paintOrder: 'stroke fill',
            textShadow: '0 4px 0 rgba(16,26,46,0.48), 0 10px 24px rgba(0,0,0,0.38)' }}>{callout}</div>
        </div>
      )}

      {/* bottom: combo meter + power-up tray */}
      <div style={{ position: 'absolute', bottom: 20, left: 14, right: 14, zIndex: 50, maxWidth: 440, margin: '0 auto' }}>
        {live.combo >= 2 && <div style={{ marginBottom: 10 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 800, fontSize: 11, color: '#fff', marginBottom: 4, textShadow: '0 1px 3px rgba(0,0,0,0.4)' }}><span>COMBO</span><span className="num">×{live.combo}</span></div>
          <Bar v={Math.min(1, live.combo / 12)} color="var(--coral)" track="rgba(255,255,255,0.4)" h={6} />
          {/* decay bar — drains over ~1.2s and RESETS on every eat (keyed to eaten count); empties = combo breaks.
              The shrinking bar is the "keep eating NOW" urgency cue that powers the flow loop. */}
          <div style={{ marginTop: 3, height: 3, borderRadius: 100, background: 'rgba(255,255,255,0.22)', overflow: 'hidden' }}>
            <div key={live.eaten} style={{ height: '100%', borderRadius: 100, background: 'linear-gradient(90deg,#FFD24A,#FF6B6B)', transformOrigin: 'left', animation: 'hh-combodecay 1.25s linear forwards' }} />
          </div>
        </div>}
        <div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'flex-end' }}>
          {/* power-up tray — premium glossy tiles matching the 3D asset icons.
              (The old bottom-right size pill was removed per design.) */}
          <div style={{ display: 'flex', gap: 10 }}>
            {eq.map(id => { const pu = puById[id]; if (!pu) return null; const ch = charges[id] || 0; const on = !!active[id];
              const tint = pu.color || '#22A6F2';
              // Full circular AI-generated badge for power-ups that have one (glossy glass
              // disc with the icon baked in) — rendered as the whole button, no CSS tile.
              const badge = PU_BADGE[id];
              return <button key={id} onClick={() => tap(id)} className={badge ? 'hh-pubadge' : 'hh-putray'} style={{ position: 'relative', width: 60, height: 60, borderRadius: '50%', cursor: 'pointer', padding: 0,
                background: badge ? 'transparent' : (on
                  ? `radial-gradient(120% 120% at 50% 18%, ${tint} 0%, ${tint} 45%, ${shade(tint,-0.34)} 100%)`
                  : `radial-gradient(120% 120% at 50% 16%, rgba(255,255,255,0.20) 0%, ${tint}2e 30%, rgba(11,22,44,0.94) 78%)`),
                border: badge ? 'none' : `2px solid ${on ? '#fff' : shade(tint,0.18) + 'cc'}`, display: 'grid', placeItems: 'center',
                boxShadow: badge
                  ? (on ? `0 0 0 2px ${tint}, 0 8px 18px ${tint}aa` : '0 6px 14px rgba(0,0,0,0.4)')
                  : (on
                  ? `0 0 0 2px ${tint}, 0 7px 0 ${shade(tint,-0.42)}, 0 14px 24px ${tint}99, inset 0 3px 0 rgba(255,255,255,0.55), inset 0 -6px 10px rgba(0,0,0,0.32)`
                  : `0 6px 0 rgba(3,10,24,0.55), 0 12px 22px rgba(0,0,0,0.34), inset 0 3px 0 rgba(255,255,255,0.30), inset 0 -6px 10px rgba(0,0,0,0.4)`),
                animation: on ? 'hh-glowpulse 1s ease-in-out infinite' : 'none', transition: 'transform .12s cubic-bezier(.2,.8,.3,1)' }}>
                {badge
                  ? <img src={badge} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', filter: on ? 'brightness(1.12) saturate(1.1)' : 'none' }} />
                  : <PowerIcon id={pu.icon} size={34} color={on ? '#fff' : pu.color} />}
                <span style={{ position: 'absolute', bottom: -6, right: -6, minWidth: 21, height: 21, padding: '0 4px', borderRadius: 11,
                  background: ch > 0 ? 'linear-gradient(180deg,#2A3F62,#16223C)' : 'var(--coral)', color: '#fff', fontSize: 12, fontWeight: 800, display: 'grid', placeItems: 'center', border: '2px solid #fff', boxShadow: '0 2px 6px rgba(22,34,60,0.45)' }}>
                  {ch > 0 ? ch : <PowerIcon id="ad" size={12} color="#fff" />}</span>
              </button>; })}
          </div>
        </div>
      </div>

      {adFor && <AdModal title="Refill power-up" rewardLabel={(puById[adFor] || {}).name + ' ×1'} accent={(puById[adFor] || {}).color}
        onClose={() => setAdFor(null)} onClaim={() => {
          // reward = the power-up fires IMMEDIATELY (no second tap needed)
          const id = adFor; setAdFor(null); fire(id);
          if (window.HHToast) window.HHToast(((puById[id] || {}).name || id) + ' activated!', 'linear-gradient(180deg,#34b6ff,#22A6F2)');
        }} />}
      {/* Pause overlay — only when paused for a normal break (not the out-of-time freeze) */}
      {paused && !outOfTime && <PausePop world={W} onResume={() => setPaused(false)} onRetry={() => go('intro')} onMap={() => go('map')} />}

      {/* ===== IN-PLACE Out of Time — the run is frozen; Continue-after-ad resumes it ===== */}
      {outOfTime && (
        <Dialog title="Out of Time!">
          <div style={{ textAlign: 'center' }}>
            <p style={{ fontWeight: 700, color: 'var(--slate)', margin: '0 0 4px', fontSize: 14 }}>
              Score <b className="num" style={{ color: 'var(--ink)' }}>{live.score.toLocaleString()}</b> / {target.toLocaleString()}
            </p>
            <div style={{ margin: '10px 0 18px' }}><ProgressBarLite value={live.score} target={target} /></div>

            {/* primary CTA: watch a rewarded ad to keep playing the SAME run with +30s.
                Two tidy stacked lines (no mid-phrase wrap) + a clear +30s chip. */}
            <button className="btn sun shimmer block" onClick={() => setContinueAd(true)}
              style={{ marginBottom: 14, padding: '12px 16px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 11 }}>
              <I.ad width={23} />
              <span style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', lineHeight: 1.12, whiteSpace: 'nowrap' }}>
                <span style={{ fontSize: 18 }}>Continue</span>
                <span style={{ fontSize: 11, fontWeight: 700, opacity: 0.82, letterSpacing: 0.2 }}>Rewarded continue</span>
              </span>
              <span className="num" style={{ marginLeft: 'auto', fontSize: 15, fontWeight: 800, background: 'rgba(90,59,0,0.16)', borderRadius: 100, padding: '4px 10px' }}>+{HH.CONTINUE_BONUS_SEC || 30}s</span>
            </button>

            <div style={{ display: 'flex', gap: 12 }}>
              <button className="btn ghost" onClick={() => { setOutOfTime(false); go('map'); }} style={{ flex: 1, fontSize: 15, padding: '13px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
                <I.map width={18} />Map
              </button>
              <button className="btn ghost" onClick={() => { setOutOfTime(false); bailTimeout(); }} style={{ flex: 1, fontSize: 15, padding: '13px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
                <I.retry width={18} />Retry
              </button>
            </div>
          </div>
        </Dialog>
      )}

      {/* rewarded-ad modal for the continue flow — AdModal funnels through HHAds.showRewarded
          on device, and a simulated countdown in plain browser. Reward → resume the run. */}
      {continueAd && <AdModal title="Keep playing" rewardLabel={`+${HH.CONTINUE_BONUS_SEC || 30} seconds`} accent="var(--mint)"
        onClose={() => setContinueAd(false)}
        onClaim={() => doContinue()} />}
    </div>
  );
}

// compact progress bar for the in-place Out-of-Time dialog (dark-on-light context — no outlined text)
function ProgressBarLite({ value, target }) {
  const tgt = Math.max(1, Number(target) || 1);
  const pct = Math.max(0, Math.min(1, (Number(value) || 0) / tgt)) * 100;
  return <div style={{ position: 'relative', height: 12, borderRadius: 100, background: 'rgba(22,34,60,0.12)', overflow: 'hidden', boxShadow: 'inset 0 1px 2px rgba(22,34,60,0.18)' }}>
    <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${pct}%`, borderRadius: 100,
      background: 'linear-gradient(90deg,#7CFC4A,#A7F23C 55%,#FFD24A)', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.5)',
      transition: 'width .4s cubic-bezier(.22,1,.36,1)' }} />
  </div>;
}

function PausePop({ world, onResume, onRetry, onMap }) {
  // Three full-width cartoon candy buttons, generously spaced (no cramped row).
  return <Dialog title="Paused" onClose={onResume}>
    <p style={{ fontWeight: 700, color: 'var(--slate)', margin: '0 0 22px', textAlign: 'center', fontSize: 14 }}>Run paused.</p>
    <button className="btn mint block shimmer" onClick={onResume} style={{ marginBottom: 14, fontSize: 20, padding: '17px' }}>
      <I.play width={22} /> Resume</button>
    <button className="btn sun block" onClick={onRetry} style={{ marginBottom: 14, fontSize: 18, padding: '15px' }}>
      <I.retry width={20} /> Retry</button>
    <button className="btn ghost block" onClick={onMap} style={{ fontSize: 18, padding: '15px' }}>
      <I.map width={20} /> Map</button>
  </Dialog>;
}

// ---------- 6 · Level Complete (the payoff) ----------
function LevelComplete({ go, nextLevel, replay, world, level, planet = 0, result, wallet }) {
  const HH = window.HH;
  const W = (HH.worldView ? HH.worldView(world, planet) : HH.WORLDS[world]);
  const PL = HH.planetForIndex ? HH.planetForIndex(planet) : null;
  const gLevel = HH.globalLevel ? HH.globalLevel(planet, level) : level;   // display level across all planets
  const lv = HH.LEVELS.flat().find(l => l[0] === level);
  const isBoss = lv && lv[2] === 'boss';
  const tgt = result.target || HH.DIFFICULTY(level, planet).target;
  // stars from score vs target — harder, but doable
  const ratio = result.score / tgt;
  const earned = result.hitTarget || ratio >= 1 ? 3 : ratio >= 0.65 ? 2 : ratio >= 0.35 || result.perfect ? Math.max(2, 1) : 1;
  const baseCoins = 40 + earned * 50 + level * 8 + (isBoss ? 150 : 0);
  const [coins, setCoins] = useStateG(baseCoins);
  const [coinDisp, setCoinDisp] = useStateG(0);
  const [showCoins, setShowCoins] = useStateG(false);
  const [adOpen, setAdOpen] = useStateG(false);
  const [doubled, setDoubled] = useStateG(false);
  // a power-up that unlocks at the NEXT level (drives the unlock-reward nudge)
  const nextUnlock = HH.POWERUPS.find(p => p.unlock === level + 1) || HH.SKINS.find(s => s.unlock === level + 1);

  const rollTo = (target, from = 0) => { let v = from; const iv = setInterval(() => { v = Math.min(target, v + Math.ceil((target - from) / 30 + 1)); setCoinDisp(v); if (v >= target) clearInterval(iv); }, 34); };
  useEffectG(() => {
    const t1 = setTimeout(() => setShowCoins(true), 700);
    const t2 = setTimeout(() => rollTo(baseCoins), 900);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, []);

  return (
    <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(180deg, ${W.sky[0]}, ${W.sky[1]})`, overflow: 'hidden' }}>
      <StatusBar dark={!W.night} />
      {Array.from({ length: 20 }).map((_, i) => <div key={i} style={{ position: 'absolute', left: `${(i * 37) % 100}%`, top: -10, width: 9, height: 14, borderRadius: 2,
        background: [HH.C.sun, HH.C.coral, HH.C.mint, HH.C.sky, HH.C.grape][i % 5], animation: `hh-confetti ${1.6 + (i % 5) * 0.3}s ${(i % 7) * 0.15}s ease-in forwards`, opacity: 0 }} />)}

      <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
        {/* AI trophy + animated sunburst — the win payoff hero (seen every clear) */}
        <div style={{ position: 'relative', width: 156, height: 156, display: 'grid', placeItems: 'center', marginBottom: 2 }}>
          <div style={{ position: 'absolute', inset: -14, borderRadius: '50%',
            background: 'repeating-conic-gradient(rgba(255,210,74,0.34) 0deg 9deg, rgba(255,210,74,0) 9deg 18deg)',
            WebkitMaskImage: 'radial-gradient(circle, #000 26%, rgba(0,0,0,0.6) 46%, transparent 72%)',
            maskImage: 'radial-gradient(circle, #000 26%, rgba(0,0,0,0.6) 46%, transparent 72%)',
            animation: 'hh-spin 11s linear infinite' }} />
          <div style={{ position: 'absolute', inset: 8, borderRadius: '50%', background: 'radial-gradient(circle, rgba(255,224,140,0.6), rgba(255,224,140,0) 68%)' }} />
          <img src={'assets/ui-ai/trophy.webp' + PU_V} alt="" style={{ position: 'relative', width: 132, height: 132, objectFit: 'contain',
            filter: 'drop-shadow(0 12px 18px rgba(0,0,0,0.22))', animation: 'hh-pop .6s cubic-bezier(.22,1.35,.36,1) both, hh-bob 3.4s ease-in-out .6s infinite' }} />
        </div>
        <div className="eyebrow fadeup" style={{ color: 'var(--ink)' }}>{PL && PL.isNew ? PL.icon + ' ' + PL.name + ' · ' : ''}{isBoss ? (level >= 160 ? 'Planet cleared!' : 'Boss clear') : 'Level ' + gLevel + ' clear'}</div>
        <h2 className="disp fadeup" style={{ animationDelay: '.05s', fontSize: 34, margin: '4px 0 16px', color: 'var(--ink)' }}>{lv ? lv[1] : 'Nice!'}</h2>
        <div style={{ marginBottom: 20, transform: 'scale(1.5)' }}><Stars n={earned} size={30} gap={10} stamp /></div>

        <div className="panel pop" style={{ width: '100%', maxWidth: 320, padding: 20, marginBottom: 16 }}>
          <div style={{ display: 'flex', justifyContent: 'space-around', textAlign: 'center', marginBottom: 12 }}>
            <div><div className="num disp" style={{ fontSize: 28, color: 'var(--ink)' }}>{result.score.toLocaleString()}</div><div className="eyebrow">Score</div></div>
            <div style={{ width: 1, background: 'var(--line)' }} />
            <div><div className="num disp" style={{ fontSize: 28, color: earned >= 3 ? 'var(--mint)' : 'var(--slate)' }}>{tgt.toLocaleString()}</div><div className="eyebrow">Target</div></div>
          </div>
          <div className="card" style={{ padding: '13px 16px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, background: '#fff', position: 'relative', overflow: 'hidden' }}>
            <Coin size={26} /><span className="num disp" style={{ fontSize: 26, color: 'var(--sun)' }}>+{coinDisp}</span>
            {doubled && <span style={{ marginLeft: 6, fontWeight: 800, fontSize: 12, color: 'var(--mint)' }}>×2!</span>}
            {showCoins && Array.from({ length: 8 }).map((_, i) => <span key={i} style={{ position: 'absolute', left: '50%', top: '50%', '--cx': `${(i - 4) * 22}px`, '--cy': `${-40 - (i % 3) * 20}px`, animation: `hh-coin .9s ${i * 0.05}s ease-out forwards` }}><Coin size={16} /></span>)}
          </div>
        </div>

        {/* next power-up unlock nudge */}
        {nextUnlock && nextUnlock.name && <button onClick={() => go('powerups')} className="card shimmer" style={{ width: '100%', maxWidth: 320, marginBottom: 12, padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10, background: '#fff', border: '1px dashed var(--grape)', cursor: 'pointer' }}>
          <span style={{ width: 34, height: 34, borderRadius: 10, background: nextUnlock.color || 'var(--grape)', display: 'grid', placeItems: 'center', color: '#fff' }}>{nextUnlock.icon ? <PowerIcon id={nextUnlock.icon} size={20} color="#fff" /> : <I.bolt width={18} />}</span>
          <div style={{ flex: 1, textAlign: 'left' }}><div style={{ fontWeight: 800, fontSize: 13 }}>{nextUnlock.name} unlocks next level</div><div style={{ fontSize: 11, fontWeight: 700, color: 'var(--slate)' }}>{nextUnlock.ads} unlock steps</div></div>
        </button>}

        {!doubled
          ? <button className="btn sun shimmer" onClick={() => setAdOpen(true)} style={{ width: '100%', maxWidth: 320, marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}><I.ad /> Double coins</button>
          : <div style={{ width: '100%', maxWidth: 320, marginBottom: 16, textAlign: 'center', fontWeight: 800, color: 'var(--mint)', fontSize: 14 }}>✓ Coins doubled!</div>}
        {/* primary action full-width; Map + Replay below — fits any phone width (no off-screen button) */}
        <button className="btn block" onClick={nextLevel} style={{ width: '100%', maxWidth: 320, marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}><I.play width={20} /> Next Level</button>
        <div style={{ display: 'flex', gap: 14, width: '100%', maxWidth: 320 }}>
          <button className="btn ghost" onClick={() => go('map')} style={{ flex: 1, minWidth: 0, fontSize: 15, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }}><I.map width={18} />Map</button>
          <button className="btn ghost" onClick={replay} style={{ flex: 1, minWidth: 0, fontSize: 15, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }}><I.retry width={18} />Replay</button>
        </div>
      </div>
      {adOpen && <AdModal rewardLabel={`+${baseCoins} coins`} onClose={() => setAdOpen(false)} onClaim={() => { setAdOpen(false); setDoubled(true); setCoins(baseCoins * 2); rollTo(baseCoins * 2, coinDisp); }} />}
    </div>
  );
}

// ---------- 7 · Out of Time (soft fail) ----------
function OutOfTime({ go, retry, world, level, result }) {
  const HH = window.HH;
  const W = HH.WORLDS[world];
  const pct = Math.round(result.eaten / result.total * 100);
  const [adOpen, setAdOpen] = useStateG(false);
  return (
    <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(180deg, ${W.sky[0]}, ${W.sky[1]})` }}>
      <StatusBar dark={!W.night} />
      {/* instant return button */}
      <IconBtn light onClick={() => go('map')} style={{ position: 'absolute', top: 48, left: 18, zIndex: 20 }}><I.back /></IconBtn>
      <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 26 }}>
        <div style={{ marginBottom: 18, animation: 'hh-bob 3s ease-in-out infinite' }}><div style={{ transform: 'perspective(400px) rotateX(50deg)' }}><HoleVisual r={44} skin="classic" /></div></div>
        <div className="eyebrow">Out of time</div>
        <h2 className="disp" style={{ fontSize: 34, margin: '4px 0 4px', color: 'var(--ink)' }}>Retry?</h2>
        <p style={{ fontWeight: 700, color: 'var(--slate)', margin: '0 0 22px', textAlign: 'center' }}>Board cleared <b style={{ color: 'var(--ink)' }}>{pct}%</b></p>

        <div className="panel" style={{ width: '100%', maxWidth: 320, padding: 20, marginBottom: 20 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
            <span style={{ fontWeight: 800, fontSize: 13 }}>Progress</span><span className="num" style={{ fontWeight: 800, color: 'var(--mint)' }}>{pct}%</span>
          </div>
          <Bar v={pct / 100} color="var(--mint)" h={10} />
          <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 14, fontWeight: 800, fontSize: 13 }}>
            <span style={{ color: 'var(--slate)' }}>Score</span><span className="num">{result.score.toLocaleString()}</span>
          </div>
        </div>

        <button className="btn sun shimmer" style={{ width: '100%', maxWidth: 320, marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }} onClick={() => setAdOpen(true)}><I.ad /> Continue <span className="num" style={{ opacity: 0.9 }}>+{HH.CONTINUE_BONUS_SEC || 30}s</span></button>
        <div style={{ display: 'flex', gap: 10, width: '100%', maxWidth: 320 }}>
          <button className="btn ghost" onClick={() => go('home')} style={{ flex: 1, fontSize: 15 }}>Home</button>
          <button className="btn ghost" onClick={() => go('map')} style={{ flex: 1, fontSize: 15 }}>Map</button>
          <button className="btn" onClick={retry} style={{ flex: 1.4 }}>Retry</button>
        </div>
      </div>
      {/* fallback continue: grant 30s via the HH helper, then retry (Gameplay reads HH._bonusTime on mount) */}
      {adOpen && <AdModal rewardLabel={`+${HH.CONTINUE_BONUS_SEC || 30} seconds`} onClose={() => setAdOpen(false)}
        onClaim={() => { setAdOpen(false); HH._bonusTime = (HH.grantContinue ? HH.grantContinue(level) : (HH.CONTINUE_BONUS_SEC || 30)); retry(); }} />}
    </div>
  );
}

// ---------- 8 · Bonus Intro ----------
function BonusIntro({ go, startBonus, world }) {
  const HH = window.HH;
  const W = HH.WORLDS[world];
  // a heart pixel-art shape
  const grid = [
    '01100110','11111111','11111111','11111111','01111110','00111100','00011000','00000000',
  ];
  return (
    <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(180deg, ${W.sky[0]}, ${W.sky[1]})` }}>
      <StatusBar dark={!W.night} />
      <IconBtn light onClick={() => go('map')} style={{ position: 'absolute', top: 48, left: 18, zIndex: 20 }}><I.close /></IconBtn>
      <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 26 }}>
        <div className="eyebrow" style={{ color: 'var(--grape)' }}>Bonus</div>
        <h2 className="disp" style={{ fontSize: 34, margin: '4px 0 6px', color: 'var(--ink)' }}>Heart Yard</h2>
        <p style={{ fontWeight: 700, color: 'var(--slate)', margin: '0 0 24px', textAlign: 'center', maxWidth: 280 }}>Clear the shape for bonus coins.</p>

        {/* pixel shape reveal */}
        <div className="panel pop" style={{ padding: 24, marginBottom: 26 }}>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, 22px)', gridAutoRows: '22px', gap: 3 }}>
            {grid.join('').split('').map((c, i) => <div key={i} className={c === '1' ? 'hh-anim' : ''} style={{ width: 22, height: 22, borderRadius: 5,
              background: c === '1' ? 'var(--coral)' : 'transparent', boxShadow: c === '1' ? 'inset 0 -3px 0 rgba(0,0,0,0.12)' : 'none',
              animation: c === '1' ? `hh-pop .4s ${(i % 8) * 0.04 + Math.floor(i / 8) * 0.05}s both` : 'none' }} />)}
          </div>
        </div>

        <button className="btn coral" onClick={() => startBonus()} style={{ minWidth: 220, fontSize: 19 }}>Start</button>
      </div>
    </div>
  );
}

// ---------- 9 · Planet Portal (infinite meta-loop) ----------
// Shown after clearing a planet's final boss (level 160). A spinning wormhole reveals
// the NEXT planet; entering re-tints the whole 16-world run and ramps difficulty.
function Portal({ go, planet = 0, onEnter }) {
  const HH = window.HH;
  const cur = HH.planetForIndex(planet);
  const next = HH.planetForIndex(planet + 1);
  return (
    <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 26,
      background: `radial-gradient(circle at 50% 42%, ${HH.mixHex('#1a1140', next.tint, 0.28)} 0%, #0a0620 56%, #05030f 100%)` }}>
      <StatusBar dark={false} />
      {/* starfield */}
      {Array.from({ length: 44 }).map((_, i) => <div key={i} style={{ position: 'absolute', left: `${(i * 53) % 100}%`, top: `${(i * 29) % 100}%`,
        width: 1 + (i % 3), height: 1 + (i % 3), borderRadius: '50%', background: '#fff', opacity: 0.12 + (i % 5) * 0.13 }} />)}

      <div className="eyebrow fadeup" style={{ color: 'rgba(255,255,255,0.7)' }}>{cur.name} conquered</div>
      <h2 className="disp fadeup" style={{ animationDelay: '.05s', fontSize: 38, margin: '4px 0 20px', color: '#fff', textAlign: 'center' }}>Portal open</h2>

      {/* wormhole */}
      <div style={{ position: 'relative', width: 224, height: 224, display: 'grid', placeItems: 'center', marginBottom: 24 }}>
        <div style={{ position: 'absolute', inset: 0, borderRadius: '50%', filter: 'blur(2px)', animation: 'hh-spin 3.6s linear infinite',
          background: `conic-gradient(from 0deg, ${next.tint}00, ${next.tint}dd, ${next.tint}22, ${next.tint}dd, ${next.tint}00)` }} />
        <div style={{ position: 'absolute', inset: 26, borderRadius: '50%', animation: 'hh-spin 2.4s linear infinite reverse',
          background: `conic-gradient(from 180deg, #ffffff00, ${next.tint}aa, #ffffff00, ${next.tint}aa, #ffffff00)` }} />
        <div style={{ position: 'absolute', inset: 62, borderRadius: '50%', background: 'radial-gradient(circle, rgba(0,0,0,0.92), rgba(0,0,0,0.5) 68%, transparent)' }} />
        <div style={{ position: 'relative', fontSize: 78, animation: 'hh-bob 3s ease-in-out infinite, hh-pop .6s cubic-bezier(.22,1.4,.36,1) both', filter: `drop-shadow(0 0 20px ${next.tint})` }}>{next.icon}</div>
      </div>

      <div className="eyebrow" style={{ color: next.tint }}>Next planet</div>
      <div className="disp" style={{ fontSize: 30, color: '#fff', margin: '2px 0 4px' }}>{next.name}</div>
      <div style={{ fontWeight: 700, fontSize: 13, color: 'rgba(255,255,255,0.62)', textAlign: 'center', margin: '0 0 22px', maxWidth: 300 }}>{next.tag} · tougher, richer rewards</div>

      <button className="btn block" onClick={() => { if (window.HHSound) window.HHSound.tap && window.HHSound.tap(); onEnter && onEnter(); }} style={{ maxWidth: 320, fontSize: 19 }}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>Enter {next.name} {next.icon}</span>
      </button>
      <button className="btn ghost block" onClick={() => go('home')} style={{ maxWidth: 320, marginTop: 12, color: '#fff', borderColor: 'rgba(255,255,255,0.25)' }}>Not yet</button>
    </div>
  );
}

Object.assign(window, { Gameplay, PausePop, LevelComplete, OutOfTime, BonusIntro, Portal });
