// hud.jsx — React HUD: boot, top bar, dossiers, scanner, claims, toasts, tweaks
const { useState, useEffect, useRef, useCallback } = React;

const fmt = (n) => n.toLocaleString('en-US');
const DEV_WALLET = 'BsDgoFdjDPvmHn7S5FAk4HyAzbdMXsFu6kA9E8Na9vUy';
const PLATFORM_URL = 'https://nomansol.com';
const LOGO_SRC = 'public/images/NoMansSOL.png';
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const mockAddr = () => Array.from({ length: 44 }, () => B58[(Math.random() * B58.length) | 0]).join('');
const shortAddr = (a) => a ? a.slice(0, 4) + '…' + a.slice(-4) : '';
const solscanAcct = (wallet) => wallet ? 'https://solscan.io/account/' + encodeURIComponent(wallet) : '#';
function fmtMcap(n) {
  if (n == null || !Number.isFinite(n)) return '—';
  const abs = Math.abs(n);
  if (abs >= 1e9) return '$' + (n / 1e9).toFixed(2).replace(/\.00$/, '') + 'B';
  if (abs >= 1e6) return '$' + (n / 1e6).toFixed(2).replace(/\.00$/, '') + 'M';
  if (abs >= 1e4) return '$' + (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
  if (abs >= 1e3) return '$' + (n / 1e3).toFixed(2) + 'K';
  return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
}
const tokenSym = (rewards, gate) => {
  if (rewards?.token?.symbol) return rewards.token.symbol;
  if (gate?.symbol) return gate.symbol;
  return 'NMS';
};
const cacheTotal = () => window.UNIVERSE?.REWARD_CACHES ?? 20;
const cachePct = (rewards) => (rewards && rewards.perCachePct != null) ? rewards.perCachePct : (window.UNIVERSE?.PER_CACHE_PCT ?? 5);
const distributablePct = (rewards) => (rewards && rewards.distributablePct != null) ? rewards.distributablePct : (window.UNIVERSE?.DISTRIBUTABLE_PCT ?? 100);

const THEMES = {
  'Holo Cyan': { acc: '#4de8ff', acc2: '#9af2ff', dim: 'rgba(77,232,255,0.35)', faint: 'rgba(77,232,255,0.10)', line: 'rgba(77,232,255,0.22)' },
  'Magenta': { acc: '#ff5fae', acc2: '#ffaad4', dim: 'rgba(255,95,174,0.35)', faint: 'rgba(255,95,174,0.10)', line: 'rgba(255,95,174,0.22)' },
  'Ember': { acc: '#ffb454', acc2: '#ffd9a0', dim: 'rgba(255,180,84,0.35)', faint: 'rgba(255,180,84,0.10)', line: 'rgba(255,180,84,0.22)' },
  'Ion Green': { acc: '#9cff57', acc2: '#d6ffb0', dim: 'rgba(156,255,87,0.35)', faint: 'rgba(156,255,87,0.10)', line: 'rgba(156,255,87,0.22)' },
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "Holo Cyan",
  "nebula": "Crimson Nebula",
  "orbitSpeed": 1,
  "glow": 1,
  "labels": true,
  "sound": true
}/*EDITMODE-END*/;

const TYPE_GLYPHS = { lava: 'VOLCANIC', rocky: 'BARREN', desert: 'ARID', lush: 'VERDANT', ocean: 'OCEANIC', toxic: 'TOXIC', ice: 'GLACIAL', gas: 'GAS GIANT' };

function Logo({ size = 30, className = 'logo' }) {
  return <img className={className} src={LOGO_SRC} alt="No Man's SOL" width={size} height={size} draggable={false} />;
}

function useNmsEvent(type, fn) {
  useEffect(() => {
    const h = (e) => fn(e.detail);
    window.addEventListener('nms:' + type, h);
    return () => window.removeEventListener('nms:' + type, h);
  }, [fn]);
}

// ---------------- activity feed (transient HUD toasts below brand) ----------------
let toastId = 0;
const FEED_TTL_MS = 5500;
const FEED_MAX = 6;

function scanStateFromPrior(prior, planet) {
  return {
    signal: prior.found ? 1 : 0.72,
    busy: false,
    found: prior.found,
    result: {
      ...prior.planet,
      found: prior.found,
      scanned: prior.scannedInSystem,
      total: prior.totalInSystem,
    },
    msg: prior.found
      ? <span>⚠ REWARD CACHE SIGNATURE ON <b>{planet.name}</b> — claim it from the dossier.</span>
      : <span>Sweep of <b>{planet.name}</b> complete. Survey logged: {prior.scannedInSystem}/{prior.totalInSystem} in this galaxy.</span>,
  };
}
function ActivityFeed({ items }) {
  if (!items.length) return null;
  return (
    <div className="activity-feed hud-compact" aria-live="polite">
      {items.map((t) => (
        <div key={t.id} className={'feed-item ' + (t.kind || 'net')}>{t.text}</div>
      ))}
    </div>
  );
}

// ---------------- mini animated solar system (orbiting dots) ----------------
function MiniSystem({ sys, size = 46 }) {
  const U = window.UNIVERSE;
  const star = U.starOf(sys);
  const col = '#' + star.col.toString(16).padStart(6, '0');
  const n = U.counts[sys];
  const orbits = [];
  for (let k = 0; k < n; k++) {
    const m = U.planetMeta(sys, k);
    const r = 6.5 + (size / 2 - 2.5 - 6.5) * (n > 1 ? k / (n - 1) : 0.5);
    const dur = 5 + k * 3;
    orbits.push({
      k, r, dur,
      delay: -(m.phase / (Math.PI * 2)) * dur,
      color: m.pal[1].getStyle(),
      dot: Math.max(1.8, 3.4 - k * 0.1),
    });
  }
  return (
    <span className="mini-sys" style={{ width: size, height: size }}>
      <span className="ms-star" style={{ background: col, boxShadow: '0 0 7px ' + col }}></span>
      {orbits.map((o) => (
        <span key={o.k} className="ms-orbit"
          style={{ width: o.r * 2, height: o.r * 2, animationDuration: o.dur + 's', animationDelay: o.delay + 's' }}>
          <span className="ms-dot" style={{ width: o.dot, height: o.dot, background: o.color }}></span>
        </span>
      ))}
    </span>
  );
}

// ---------------- scanner (per-planet deep scan) ----------------
function Scanner({ mode, system, planet, scanState, onScan, onSweep, hasSweep, claimed, log, rewards }) {
  const busy = scanState.busy;
  const isPlanet = mode === 'planet' && planet;
  const inSystem = mode === 'system' || mode === 'planet';
  if (!inSystem) return null;
  const entry = system && log ? log[system.index] : null;
  const scannedCount = entry ? entry.scanned.length : 0;
  const result = scanState.result;
  return (
    <div data-guide="scanner">
      <div className="panel">
        <div className="ph"><h3>PLANETARY DEEP SCANNER</h3><span className="ph-r">{busy ? 'SWEEP' : isPlanet ? 'READY' : 'STANDBY'}</span></div>
        <div className={'scan-body' + (busy ? ' scanning' : '')}>
          <div className="sig-track">
            <div className={'sig-fill' + (scanState.found ? ' gold' : '')}
              style={{ width: busy ? undefined : Math.round((scanState.signal || 0) * 100) + '%' }}></div>
          </div>
          <div className="sig-meta">
            <span>TARGET <b>{isPlanet ? planet.name : '—'}</b></span>
            {(mode === 'system' || mode === 'planet') && system
              ? <span>LOGGED <b>{scannedCount}/{system.planets}</b></span>
              : <span>NO TARGET LINK</span>}
          </div>
          {scanState.msg ? <div className="scan-note">{scanState.msg}</div> : (
            <div className="scan-note">{isPlanet
              ? 'Runs a full surface sweep of this world — logs the survey and reveals any reward-cache signature.'
              : 'Select a planet to bring the deep scanner online.'}</div>
          )}
          <button className="btn" disabled={busy || !isPlanet} onClick={onScan}>{busy ? 'SCANNING…' : 'DEEP SCAN PLANET'}</button>
          {hasSweep && inSystem ? (
            <button className="btn gold" disabled={busy} onClick={onSweep}>⟁ SWEEP ENTIRE GALAXY</button>
          ) : null}
        </div>
      </div>
      {result ? (
        <div className="panel" style={{ marginTop: 10 }}>
          <div className="ph"><h3>SCAN RESULT</h3><span className="ph-r">{result.name}</span></div>
          <dl className="kv">
            <dt>BIOME</dt><dd>{result.biome}</dd>
            <dt>WEATHER</dt><dd>{result.weather}</dd>
            <dt>RESOURCES</dt><dd><span className="res-tags">{result.res.map((r) => <span key={r}>{r}</span>)}</span></dd>
            <dt>CACHE</dt><dd style={result.found ? { color: 'var(--gold)' } : { color: 'var(--ink-dim)' }}>
              {result.found ? '⚠ REWARD SIGNATURE DETECTED' : 'NO SIGNATURE'}</dd>
            <dt>SURVEY</dt><dd>{result.scanned}/{result.total} PLANETS SCANNED IN GALAXY</dd>
          </dl>
        </div>
      ) : null}
    </div>
  );
}

// ---------------- creator rewards (Helius + pump.fun vault) ----------------
function fmtSol(n, d = 4) {
  if (n == null || !Number.isFinite(n)) return '—';
  if (n < 0.0001) return n.toExponential(2) + ' SOL';
  return n.toFixed(d) + ' SOL';
}
function fmtUsd(n) {
  if (n == null || !Number.isFinite(n)) return '';
  return ' ≈ $' + n.toLocaleString('en-US', { maximumFractionDigits: 2 });
}
function fmtUsdPlain(n) {
  if (n == null || !Number.isFinite(n)) return null;
  return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function SolUsd({ sol, usd, solDigits = 5, className = '' }) {
  const usdStr = fmtUsdPlain(usd);
  return (
    <span className={'sol-usd' + (className ? ' ' + className : '')}>
      <span className="sol-amt">{fmtSol(sol, solDigits)}</span>
      {usdStr ? <span className="usd-amt">{usdStr}</span> : null}
    </span>
  );
}

function RewardCachesMini({ claimed, rewards, apiOnline, onOpen }) {
  const per = rewards && rewards.perCacheSol;
  const total = cacheTotal();
  const pct = cachePct(rewards);
  const poolReady = rewards && rewards.configured;
  return (
    <div className="panel cache-mini clickable" data-guide="caches" onClick={onOpen} title="View claimed caches">
      <div className="ph"><h3>REWARD CACHES</h3><span className="ph-r">{pct}% EACH · VIEW ▸</span></div>
      <div className="tracker cache-tracker">
        <div className="cache-col cache-cr" data-guide="rewards">
          <span className="cache-lbl">CREATOR REWARDS</span>
          {poolReady ? (
            <React.Fragment>
              <span className="cache-sol">{fmtSol(rewards.totalSol, 3)}</span>
              {fmtUsdPlain(rewards.totalUsd) ? <span className="cache-usd">{fmtUsdPlain(rewards.totalUsd)}</span> : null}
            </React.Fragment>
          ) : (
            <span className="cache-usd">{apiOnline ? 'LOADING…' : 'OFFLINE'}</span>
          )}
        </div>
        <div className="cache-vdiv" aria-hidden="true"></div>
        <div className="cache-count-wrap">
          <div className="cache-count">
            <span className="cache-count-n">{claimed}</span>
            <span className="cache-count-rest">/{total}</span>
          </div>
          <span className="cache-count-lbl">REWARDS CACHE COLLECTED</span>
        </div>
        {per != null ? (
          <div className="cache-col cache-per">
            <span className="cache-lbl">PER CACHE</span>
            <span className="cache-sol">{fmtSol(per, 5)}</span>
            {fmtUsdPlain(rewards.perCacheUsd) ? <span className="cache-usd">{fmtUsdPlain(rewards.perCacheUsd)}</span> : null}
          </div>
        ) : null}
      </div>
    </div>
  );
}

// ---------------- claimed caches modal ----------------
function CacheModal({ rewards, claimed, onClose, onGo }) {
  const U = window.UNIVERSE;
  const systems = U.claimedSystems().sort((a, b) => a - b);
  const per = rewards && rewards.perCacheSol;
  const total = cacheTotal();
  return (
    <div className="modal-bg hud-compact" onClick={onClose}>
      <div className="modal panel" onClick={(e) => e.stopPropagation()}>
        <div className="ph"><h3>REWARD CACHES</h3><button className="btn ghost" style={{ padding: '4px 10px' }} onClick={onClose}>CLOSE</button></div>
        <div className="log-stats">
          <span><b>{systems.length}</b> CLAIMED</span>
          <span><b>{total - systems.length}</b> STILL HIDDEN</span>
          {per != null ? (
            <span style={{ color: 'var(--gold)' }}>
              PER CACHE <SolUsd sol={per} usd={rewards.perCacheUsd} solDigits={5} />
            </span>
          ) : null}
        </div>
        <div className="logbody">
          {systems.length === 0 ? (
            <div className="hint" style={{ padding: 14 }}>No caches claimed yet. {fmt(window.UNIVERSE.TOTAL_PLANETS)} worlds, {total} hidden caches — be the first.</div>
          ) : systems.map((s) => {
            const k = U.claimedPlanetK(s);
            if (k == null) return null;
            const p = U.planetMeta(s, k);
            const by = U.claimedBy(s);
            return (
              <div key={s} className="log-row">
                <div className="log-row-top">
                  <MiniSystem sys={s} />
                  <span className="lr-name">{U.systemName(s)}</span>
                  <span className="lr-meta gold">⚑ {shortAddr(by)}</span>
                  <button className="btn" style={{ padding: '5px 10px', fontSize: 10.125, letterSpacing: '0.18em' }} onClick={() => onGo(s)}>VISIT ▸</button>
                </div>
                <div className="lr-planets">
                  <span className="lr-chip clickable gold" onClick={() => onGo(s, k)}>◎ {p.name} — CACHE WORLD ▸</span>
                </div>
              </div>
            );
          })}
          <div className="hint" style={{ padding: '12px 16px' }}>
            UNCLAIMED CACHE LOCATIONS STAY SEALED UNTIL A TRAVELER CLAIMS THEM ON-CHAIN.
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------------- boosts (each lasts 24h from purchase) ----------------
const BOOSTS = [
  { id: 'fastscan', name: 'HYPER SCANNER', price: 0.1, desc: 'Deep scans complete in 4s instead of 8s. Stacks with other boosts.' },
  { id: 'sysscan', name: 'GALAXY SWEEP', price: 0.5, desc: 'Scan every planet in a galaxy at once. Combines with Hyper Scanner for faster sweeps.' },
  { id: 'autoscan', name: 'AUTO SCANNER', price: 1.5, desc: 'Autonomously tours galaxies for 24h — sweeps entire galaxies if you own Galaxy Sweep, otherwise scans planet-by-planet (50% faster with Hyper Scanner).' },
];

function fmtBoostTime(ms) {
  if (!ms || ms <= 0) return '0m';
  const h = Math.floor(ms / 3600000);
  const m = Math.floor((ms % 3600000) / 60000);
  return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
}

function BoostsPanel({ owned, buying, onBuy, autoOn, onToggleAuto }) {
  const [, tick] = useState(0);
  useEffect(() => {
    if (!owned.length) return;
    const tm = setInterval(() => tick((n) => n + 1), 30000);
    return () => clearInterval(tm);
  }, [owned.length]);
  const active = (id) => owned.find((o) => o.id === id);
  return (
    <div className="panel">
      <div className="ph"><h3>BOOSTS</h3><span className="ph-r">24H · PAY IN SOL</span></div>
      <div className="boosts">
        {BOOSTS.map((b) => {
          const st = active(b.id);
          return (
            <div key={b.id} className={'boost-row' + (st ? ' owned' : '')}>
              <div className="b-info">
                <span className="b-name">{b.name}</span>
                <span className="b-desc">{b.desc}</span>
              </div>
              {st ? (
                b.id === 'autoscan' ? (
                  <div className="b-stack">
                    <span className="b-active">● {fmtBoostTime(st.remainingMs)} LEFT</span>
                    <button className={'btn ' + (autoOn ? 'gold' : 'ghost')} style={{ padding: '5px 12px' }}
                      onClick={onToggleAuto}>
                      {autoOn ? '⏸ PAUSE' : '▶ RESUME'}
                    </button>
                  </div>
                ) : (
                  <span className="b-active">● {fmtBoostTime(st.remainingMs)} LEFT</span>
                )
              ) : (
                <button className="btn gold" disabled={buying === b.id} onClick={() => onBuy(b)}>
                  {buying === b.id ? 'SIGNING…' : '◎ ' + b.price + ' SOL'}
                </button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ---------------- traveler's field guide (interactive tour over the LIVE app) ----------------
// Every "clip" is the real app: the tour spotlights actual HUD panels and drives
// real camera moves / warps via window.NMS. No mock UI anywhere.
const GUIDE_CHAPTERS = [
  {
    id: 'welcome', name: 'WELCOME', blurb: 'What No Man\u2019s SOL is, in 30 seconds.',
    steps: [
      {
        title: 'WELCOME, TRAVELER',
        body: <span>No Man's SOL is a fully explorable universe of <b>{fmt(window.UNIVERSE.TOTAL_PLANETS)} worlds</b> across {fmt(window.UNIVERSE.SYS)} galaxies — and <b>{cacheTotal()} of those worlds hide a reward cache</b> worth {cachePct()}% of the live creator-fee pool. Your Solana wallet is your account: no email, no password. This guide runs inside the live universe — you can fly around at any step.</span>,
      },
    ],
  },
  {
    id: 'nav', name: 'NAVIGATION', blurb: 'Fly the universe map, find galaxies, warp in.',
    steps: [
      {
        title: 'FLY THE UNIVERSE', ensure: 'galaxy',
        demo: () => { try { window.NMS.focusSystem(window.UNIVERSE.spawn); } catch (e) { /* ignore */ } },
        body: <span><b>Drag</b> to orbit the camera · <b>scroll</b> to zoom · <b>click any galaxy</b> to select it. <b>Arrow keys / WASD</b> hop to neighbouring galaxies. We just flew you to a nearby one for real — try dragging right now.</span>,
      },
      {
        title: 'LOCATE ANY GALAXY', ensure: 'galaxy', target: '[data-guide="search"]',
        body: <span>Every galaxy has a code like <b>EUCLID-7F3A1</b>. Type or paste one here and hit LOCATE to jump straight to it — handy when another traveler shares coordinates in chat.</span>,
      },
      {
        title: 'THE GALAXY DOSSIER', ensure: 'galaxy', target: '[data-guide="dossier"]',
        body: <span>Selecting a galaxy opens its dossier: core star class, temperature, planet count and sector. A <b>gold ring</b> on the map means its cache was already claimed.</span>,
      },
      {
        title: 'WARP IN', ensure: 'galaxy', target: '[data-guide="warphint"]',
        body: <span>Press <b>Space</b> — or the WARP button in the dossier — to travel into the selected galaxy. Hit NEXT and we'll warp there for real.</span>,
      },
    ],
  },
  {
    id: 'system', name: 'GALAXIES', blurb: 'Planets, orbits, and moving around inside a galaxy.',
    steps: [
      {
        title: 'INSIDE A GALAXY', ensure: 'system', target: '[data-guide="dossier"]',
        body: <span>That warp was real. Worlds orbit live around the galactic core — <b>click one in this list</b> (or click the planet itself) to approach it. <b>Esc</b> or <b>←</b> returns to the universe map.</span>,
      },
    ],
  },
  {
    id: 'scan', name: 'SCANNING', blurb: 'Deep scans — the heart of the cache hunt.',
    steps: [
      {
        title: 'THE PLANET DOSSIER', ensure: 'planet', target: '[data-guide="dossier"]',
        body: <span>Up close, every world has its own dossier — class, biome, weather, gravity and resources, all unique to this planet.</span>,
      },
      {
        title: 'DEEP SCAN', ensure: 'planet', target: '[data-guide="scanner"]',
        body: <span>This is the heart of the hunt. <b>DEEP SCAN PLANET</b> sweeps the surface (~8 seconds, 4 with Hyper Scanner) and reveals whether this world hides a reward cache. Scans are saved to your traveler log. Go ahead — scan this one for real.</span>,
      },
    ],
  },
  {
    id: 'rewards', name: 'CACHES & CLAIMING', blurb: 'The 20 caches, the pool, and how to claim SOL.',
    steps: [
      {
        title: cacheTotal() + ' HIDDEN CACHES', target: '[data-guide="caches"]',
        body: <span><b>{cacheTotal()} reward caches</b> are hidden among {fmt(window.UNIVERSE.TOTAL_PLANETS)} worlds. Locations were sealed before the season started — they're not in the game files and can't be datamined. The only way to find one: be at a planet and deep-scan it.</span>,
      },
      {
        title: 'THE REWARD POOL', target: '[data-guide="rewards"]',
        body: <span>Every $NMS trade feeds creator fees into the reward pool, tracked here live with USD values. Each cache pays <b>{cachePct()}% of the pool at the moment it's claimed</b> — so the pot can grow while a cache sits unfound.</span>,
      },
      {
        title: 'CLAIMING A CACHE',
        body: <span>When a scan hits, the planet dossier lights up: <b style={{ color: 'var(--gold)' }}>⚠ REWARD CACHE DETECTED</b>. Press <b>CLAIM CACHE</b> and the SOL is sent straight to your connected wallet — automatically, within seconds. <b>First wallet to claim keeps it forever.</b> You can wait for the pool to grow before claiming… but another traveler might scan the same world and beat you to it.</span>,
      },
    ],
  },
  {
    id: 'boosts', name: 'BOOSTS', blurb: 'Scan faster, sweep galaxies, go hands-free.',
    steps: [
      {
        title: 'BOOSTS', target: '[data-guide="boosts"]',
        body: <span>Tap the pulsing <b>⚡</b> under your wallet (top-right) to open boosts. Three 24-hour power-ups, paid in SOL: <b>HYPER SCANNER</b> halves scan time · <b>GALAXY SWEEP</b> scans every planet in a galaxy at once · <b>AUTO SCANNER</b> tours galaxies for you. They stack.</span>,
      },
    ],
  },
  {
    id: 'hud', name: 'HUD & COMMS', blurb: 'Your log, global chat, sound and more.',
    steps: [
      {
        title: 'TRAVELER LOG', target: '[data-guide="log"]',
        body: <span>Every galaxy you visit and planet you scan is recorded to your wallet. Open <b>LOG</b> to browse your journey — one click flies you back to any galaxy.</span>,
      },
      {
        title: 'GLOBAL COMMS', target: '[data-guide="comms"]',
        body: <span>Live chat with every traveler online — the panel opens on the right side of your screen. Coordinates get shared, rumours spread, cache rushes happen — keep an eye on it.</span>,
      },
      {
        title: 'TRAVELERS ONLINE', target: '[data-guide="travelers"]',
        body: <span>See who's exploring right now. Click <b>TRAVELERS</b> for the live roster — each traveler's galaxies viewed, planets scanned, active boosts and cache finds, with a one-click link to their wallet on Solscan.</span>,
      },
      {
        title: 'SOUND & MUSIC', target: '[data-guide="audio"]',
        body: <span>Toggle the ambient score, galaxy-hover pings and the warp sound with these three switches in the top bar, just right of the GUIDE button.</span>,
      },
      {
        title: 'CONTROLS RECAP',
        body: <span><b>Drag</b> orbit · <b>Scroll</b> zoom · <b>Click</b> select · <b>Space</b> warp in · <b>Esc / ←</b> back out · <b>Arrows / WASD</b> hop galaxies · <b>Galaxy code + LOCATE</b> jump anywhere. Good hunting, traveler. ◎</span>,
      },
    ],
  },
];
const GUIDE_STEPS = GUIDE_CHAPTERS.flatMap((c) => c.steps.map((s) => ({ ...s, chapter: c.name, chapterId: c.id })));

function useGuideRect(selector) {
  const [rect, setRect] = useState(null);
  useEffect(() => {
    if (!selector) { setRect(null); return undefined; }
    let raf = 0; let last = '';
    const tick = () => {
      const el = document.querySelector(selector);
      if (el) {
        const r = el.getBoundingClientRect();
        const key = [r.x | 0, r.y | 0, r.width | 0, r.height | 0].join(',');
        if (key !== last) { last = key; setRect({ x: r.x, y: r.y, w: r.width, h: r.height }); }
      } else if (last !== 'none') { last = 'none'; setRect(null); }
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [selector]);
  return rect;
}

/** Move the real app toward the mode a tour step needs (one nudge per call). */
function guideNavTo(want) {
  const cur = window.NMS.getMode();
  if (cur === want || cur === 'warping' || cur === 'boot') return;
  if (want === 'galaxy') window.NMS.back();
  else if (want === 'system') {
    if (cur === 'galaxy') window.NMS.warpTo(window.UNIVERSE.spawn);
    else if (cur === 'planet') window.NMS.back();
  } else if (want === 'planet') {
    if (cur === 'galaxy') window.NMS.warpTo(window.UNIVERSE.spawn);
    else if (cur === 'system') window.NMS.selectPlanet(0);
  }
}

function GuideTour({ mode, startChapter, onExit }) {
  const startIdx = Math.max(0, GUIDE_STEPS.findIndex((s) => s.chapterId === startChapter));
  const [idx, setIdx] = useState(startIdx);
  const step = GUIDE_STEPS[idx];
  const ready = !step.ensure || step.ensure === mode;
  const demoRan = useRef(-1);

  const finishTour = () => {
    try { window.NMS.resetGalaxyView(); } catch (e) { /* ignore */ }
    onExit();
  };

  useEffect(() => { if (mode === 'boot') finishTour(); }, [mode, onExit]);
  useEffect(() => {
    if (ready) return undefined;
    guideNavTo(step.ensure);
    const t = setInterval(() => guideNavTo(step.ensure), 1400);
    return () => clearInterval(t);
  }, [idx, mode, ready, step.ensure]);
  useEffect(() => {
    if (ready && step.demo && demoRan.current !== idx) {
      demoRan.current = idx;
      try { step.demo(); } catch (e) { /* ignore */ }
    }
  }, [ready, idx, step]);
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') finishTour(); };
    addEventListener('keydown', onKey);
    return () => removeEventListener('keydown', onKey);
  }, [onExit]);

  const rect = useGuideRect(ready ? step.target : null);
  let cardStyle = null;
  if (rect) {
    const vw = window.innerWidth; const vh = window.innerHeight;
    const left = Math.min(Math.max(rect.x + rect.w / 2 - 230, 16), Math.max(16, vw - 478));
    const below = rect.y + rect.h / 2 < vh / 2;
    cardStyle = below
      ? { left, top: Math.min(rect.y + rect.h + 16, vh - 280) }
      : { left, bottom: Math.max(vh - rect.y + 16, 16) };
  }
  return (
    <div className="guide-layer hud-compact">
      {rect ? (
        <div className="guide-spot" style={{ left: rect.x - 8, top: rect.y - 8, width: rect.w + 16, height: rect.h + 16 }}></div>
      ) : null}
      <div className={'guide-card' + (cardStyle ? '' : ' anchored')} style={cardStyle || undefined}>
        <div className="guide-chapter">{step.chapter} · STEP {idx + 1}/{GUIDE_STEPS.length}</div>
        <button type="button" className="guide-x" onClick={finishTour} aria-label="Exit guide">✕</button>
        <div className="guide-title">{step.title}</div>
        <div className="guide-body">{ready ? step.body : 'Traveling to the right spot in the universe…'}</div>
        <div className="guide-btns">
          <button className="btn ghost" disabled={idx === 0} onClick={() => setIdx(idx - 1)}>‹ BACK</button>
          {idx < GUIDE_STEPS.length - 1
            ? <button className="btn" onClick={() => setIdx(idx + 1)}>NEXT ›</button>
            : <button className="btn gold" onClick={finishTour}>FINISH ▸</button>}
        </div>
      </div>
    </div>
  );
}

function GuideModal({ onClose, onPlay }) {
  return (
    <div className="modal-bg hud-compact" onClick={onClose}>
      <div className="modal panel guide-modal" onClick={(e) => e.stopPropagation()}>
        <div className="ph"><h3>TRAVELER'S FIELD GUIDE</h3><button className="btn ghost" style={{ padding: '4px 10px' }} onClick={onClose}>CLOSE</button></div>
        <div className="guide-intro">
          An interactive tour that runs inside the <b>live universe</b> — real HUD, real warps, real scans.
          Pick a chapter, or take the full tour. You stay in control the whole time; exit with Esc.
        </div>
        <div className="guide-list">
          {GUIDE_CHAPTERS.map((c) => (
            <div key={c.id} className="guide-row">
              <div className="guide-row-info">
                <span className="g-name">{c.name}</span>
                <span className="g-blurb">{c.blurb}</span>
              </div>
              <button className="btn" onClick={() => onPlay(c.id)}>PLAY ▸</button>
            </div>
          ))}
        </div>
        <div className="guide-foot">
          <button className="btn gold lg" onClick={() => onPlay(GUIDE_CHAPTERS[0].id)}>▶ TAKE THE FULL TOUR</button>
        </div>
      </div>
    </div>
  );
}

// ---------------- traveler log ----------------
function LogModal({ log, onClose, onGo }) {
  const U = window.UNIVERSE;
  const entries = Object.keys(log).map(Number).sort((a, b) => (log[b].last || 0) - (log[a].last || 0));
  const totalScans = entries.reduce((n, s) => n + (log[s].scanned ? log[s].scanned.length : 0), 0);
  return (
    <div className="modal-bg hud-compact" onClick={onClose}>
      <div className="modal panel" onClick={(e) => e.stopPropagation()}>
        <div className="ph"><h3>TRAVELER LOG</h3><button className="btn ghost" style={{ padding: '4px 10px' }} onClick={onClose}>CLOSE</button></div>
        <div className="log-stats">
          <span><b>{entries.length}</b> GALAXIES VISITED</span>
          <span><b>{totalScans}</b> PLANETS SCANNED</span>
        </div>
        <div className="logbody">
          {entries.length === 0 ? (
            <div className="hint" style={{ padding: 14 }}>No journeys logged yet. Warp into a galaxy to begin your record.</div>
          ) : entries.map((s) => {
            const e = log[s];
            const total = U.counts[s];
            const scanned = e.scanned || [];
            return (
              <div key={s} className="log-row">
                <div className="log-row-top">
                  <MiniSystem sys={s} />
                  <span className="lr-name">{U.systemName(s)}</span>
                  <span className="lr-meta">{e.visits}× VISITED · SCANNED {scanned.length}/{total}</span>
                  <button className="btn" style={{ padding: '5px 10px', fontSize: 10.125, letterSpacing: '0.18em' }} onClick={() => onGo(s)}>VISIT ▸</button>
                </div>
                {scanned.length ? (
                  <div className="lr-planets">
                    {scanned.slice().sort((a, b) => a - b).map((k) => (
                      <span key={k} className="lr-chip clickable" onClick={() => onGo(s, k)}>{U.planetMeta(s, k).name} ▸</span>
                    ))}
                  </div>
                ) : null}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ---------------- left context panel ----------------
function NavCrumb({ system, mode, planet, onCrumb }) {
  if (!system) return null;
  return (
    <div className="nav-crumb bl-crumb">
      {system.sector ? (
        <span className="crumb-sector" title={system.sector.name}>{system.sector.quadrant} QUADRANT · {system.sector.code}</span>
      ) : null}
      {system.sector ? <span className="sep">/</span> : null}
      {mode === 'planet' ? (
        <span className="crumb-link clickable" onClick={() => onCrumb('system')}><b>{system.name}</b></span>
      ) : mode === 'system' ? (
        <span className="crumb-link clickable" onClick={() => onCrumb('galaxy')}><b>{system.name}</b></span>
      ) : (
        <b>{system.name}</b>
      )}
      {mode === 'planet' && planet ? (
        <React.Fragment><span className="sep">/</span><b>{planet.name}</b></React.Fragment>
      ) : null}
    </div>
  );
}

function SystemDossier({ sys, onWarp }) {
  if (!sys) return null;
  const coreDist = Math.round(Math.hypot(sys.pos[0], sys.pos[1], sys.pos[2]) * 7.4);
  return (
    <div className="panel">
      <div className="ph"><h3>GALAXY</h3><span className="ph-r">#{fmt(sys.index)}</span></div>
      <dl className="kv">
        <dt>DESIGNATION</dt><dd style={{ fontFamily: 'var(--font-d)', fontWeight: 600, letterSpacing: '0.12em' }}>{sys.name}</dd>
        <dt>SECTOR</dt><dd>{sys.sector ? sys.sector.name : '—'}</dd>
        <dt>STAR</dt><dd><span style={{ color: sys.starColor }}>●</span> CLASS {sys.starClass} · {sys.starTemp}</dd>
        <dt>BODIES</dt><dd>{sys.planets} PLANETS</dd>
        <dt>CORE DIST</dt><dd>{fmt(coreDist)} LY</dd>
        <dt>CATALOG</dt><dd>PLANET IDS {fmt(sys.firstId)}–{fmt(sys.firstId + sys.planets - 1)}</dd>
      </dl>
      <div className="actions">
        <button className="btn" onClick={onWarp}>WARP TO GALAXY ▸</button>
      </div>
      <div className="hint">CLICK A GALAXY TO FOCUS · CLICK IT AGAIN TO WARP IN · DRAG TO ORBIT · RIGHT-DRAG TO PAN · SCROLL TO ZOOM</div>
    </div>
  );
}

function PlanetList({ sys, planets, anomalyK, scanned, onPick }) {
  const scannedSet = new Set(scanned || []);
  return (
    <div className="panel">
      <div className="ph"><h3>{sys ? sys.name : ''}</h3><span className="ph-r">{planets.length} BODIES</span></div>
      <div style={{ padding: '6px 0' }}>
        {planets.map((p) => (
          <div key={p.k} className={'planet-row clickable' + (anomalyK === p.k ? ' anomaly' : '')} onClick={() => onPick(p.k)}>
            <span className="pr-dot" style={{ background: p.color }}></span>
            <span className="pr-name">{p.name}</span>
            <span className="pr-type">{anomalyK === p.k ? '⚠ ANOMALY' : p.typeName.toUpperCase()}{p.rings ? ' · RINGED' : ''}</span>
            <span
              className={'pr-scan' + (scannedSet.has(p.k) ? ' done' : '')}
              title={scannedSet.has(p.k) ? 'Deep scanned' : 'Not scanned'}
              aria-label={scannedSet.has(p.k) ? 'Deep scanned' : 'Not scanned'}
            />
          </div>
        ))}
      </div>
      <div className="hint">SELECT A BODY TO APPROACH · ESC RETURNS TO UNIVERSE MAP</div>
    </div>
  );
}

function PlanetDossier({ planet, anomaly, claimedHere, claimedBy, wallet, onBack, onClaim, claiming, claimedNow, rewards }) {
  if (!planet) return null;
  return (
    <div className="panel">
      <div className="ph"><h3>PLANETARY DOSSIER</h3><span className="ph-r">ID {fmt(planet.id)}</span></div>
      <dl className="kv">
        <dt>DESIGNATION</dt><dd style={{ fontFamily: 'var(--font-d)', fontWeight: 600, letterSpacing: '0.12em' }}>{planet.name}</dd>
        <dt>CLASS</dt><dd>{planet.typeName.toUpperCase()}{planet.rings ? ' · RINGED' : ''}</dd>
        <dt>BIOME</dt><dd>{planet.biome}</dd>
        <dt>WEATHER</dt><dd>{planet.weather}</dd>
        <dt>GRAVITY</dt><dd>{planet.gravity} G</dd>
        <dt>DAY LENGTH</dt><dd>{planet.dayLen} HRS</dd>
        <dt>RESOURCES</dt><dd><span className="res-tags">{planet.res.map((r) => <span key={r}>{r}</span>)}</span></dd>
      </dl>
      {anomaly && !claimedNow ? (
        <div className="claim-box">
          <div className="ct">⚠ REWARD CACHE DETECTED</div>
          <div className="cd">This world holds <b style={{ color: 'var(--gold)' }}>
            {rewards && rewards.perCacheSol != null
              ? fmtSol(rewards.perCacheSol, 5) + ' (' + cachePct(rewards) + '% of live reward pool)' + (fmtUsdPlain(rewards.perCacheUsd) ? ' · ' + fmtUsdPlain(rewards.perCacheUsd) : '')
              : cachePct(rewards) + '% of $NMS creator rewards'}
          </b>. Server sweeps pump.fun fees and sends SOL to your connected wallet. One claim per cache — amount is locked at claim time.</div>
          <button className="btn gold" disabled={claiming} onClick={onClaim}>
            {claiming ? 'COLLECTING & SENDING SOL…' : 'CLAIM CACHE ▸ ' + shortAddr(wallet)}
          </button>
        </div>
      ) : null}
      {claimedNow ? (
        <div className="claim-box" style={{ animation: 'none' }}>
          <div className="ct">✓ CACHE CLAIMED</div>
          <div className="cd">Rewards routed to <b>{shortAddr(wallet)}</b>. Transaction confirmed on Solana mainnet. Glory is yours, traveler.</div>
        </div>
      ) : null}
      {claimedHere && !claimedNow ? (
        <div className="hint" style={{ color: 'var(--gold)' }}>
          ⚑ CACHE CLAIMED BY{' '}
          {claimedBy ? (
            <a className="wallet-link" href={solscanAcct(claimedBy)} target="_blank" rel="noopener noreferrer" title="View on Solscan">
              {shortAddr(claimedBy)}
            </a>
          ) : 'ANOTHER TRAVELER'}
        </div>
      ) : null}
      <div className="actions">
        <button className="btn ghost" onClick={onBack}>◂ BACK TO GALAXY</button>
      </div>
    </div>
  );
}

// ---------------- cinematic stream overlay (dev wallet only) ----------------
function CineScanCard({ scan }) {
  const p = scan.planet, s = scan.system;
  const busy = scan.scanning;
  return (
    <div className="cine-scan panel">
      <div className="ph">
        <h3>{busy ? 'SCANNING…' : 'DEEP SCAN'}</h3>
        <span className="ph-r">ID {fmt(p.id)}</span>
      </div>
      {busy ? (
        <div className="rbody" style={{ padding: '10px 14px 4px' }}>
          <div className="sig-track scanning"><div className="sig-fill"></div></div>
          <div className="scan-note" style={{ marginTop: 8 }}>Surface sweep of <b>{p.name}</b> in progress…</div>
        </div>
      ) : null}
      <dl className="kv">
        <dt>DESIGNATION</dt><dd style={{ fontFamily: 'var(--font-d)', fontWeight: 600, letterSpacing: '0.12em' }}>{p.name}</dd>
        <dt>GALAXY</dt><dd>{s.name}</dd>
        <dt>STAR</dt><dd><span style={{ color: s.starColor }}>●</span> CLASS {s.starClass} · {s.starTemp}</dd>
        <dt>BODIES</dt><dd>{s.planets} PLANETS</dd>
        <dt>CATALOG</dt><dd>#{fmt(p.id)}</dd>
        <dt>CLASS</dt><dd>{p.typeName.toUpperCase()}{p.rings ? ' · RINGED' : ''}</dd>
        <dt>BIOME</dt><dd>{p.biome}</dd>
        <dt>WEATHER</dt><dd>{p.weather}</dd>
        <dt>GRAVITY</dt><dd>{p.gravity} G</dd>
        <dt>DAY LENGTH</dt><dd>{p.dayLen} HRS</dd>
        <dt>RESOURCES</dt><dd><span className="res-tags">{p.res.map((r) => <span key={r}>{r}</span>)}</span></dd>
        <dt>CACHE</dt><dd style={{ color: busy ? 'var(--ink-dim)' : 'var(--ink-dim)' }}>
          {busy ? 'AWAITING RESULT…' : 'NO SIGNATURE'}
        </dd>
        {!busy && scan.scannedInSystem != null ? (
          <React.Fragment>
            <dt>SURVEY</dt><dd>{scan.scannedInSystem}/{scan.totalInSystem} PLANETS SCANNED IN GALAXY</dd>
          </React.Fragment>
        ) : null}
      </dl>
      {!busy ? (
        <div className="rbody muted" style={{ padding: '0 14px 10px', fontSize: 11.25, letterSpacing: '0.1em' }}>
          Sweep complete — no reward cache signature on this world.
        </div>
      ) : null}
      <a className="cine-link clickable" href={PLATFORM_URL + '/planet/' + p.id} target="_blank" rel="noreferrer">
        VISIT {p.name} ON NOMANSOL.COM →
      </a>
    </div>
  );
}

function CinematicOverlay({ online, claimed, ticker, feed, scan, caption, onExit, tokenLabel }) {
  return (
    <div className="cine hud-compact">
      <div className="cine-bar top"></div>
      <div className="cine-bar bottom"></div>
      <div className="cine-brand">
        <Logo size={26} />
        <div>
          <div className="bt">NO MAN'S <em>SOL</em></div>
          <a className="cine-url clickable" href={PLATFORM_URL} target="_blank" rel="noreferrer">NOMANSOL.COM</a>
        </div>
        <span className="cine-live">● LIVE</span>
      </div>
      <div className="cine-stats">
        <div className="cstat"><span className="n">{fmt(online)}</span><span className="l">EXPLORERS ONLINE</span></div>
        <div className="cstat gold"><span className="n">{claimed}<i>/{cacheTotal()}</i></span><span className="l">CACHES CLAIMED</span></div>
        <div className="cstat gold"><span className="n">{cacheTotal() - claimed}</span><span className="l">HIDDEN · {cachePct()}% FEES EACH</span></div>
        <div className={'cstat ' + ((ticker.change24h ?? 0) >= 0 ? 'up' : 'down')}>
          <span className="n">{fmtMcap(ticker.marketCapUsd)}</span>
          <span className="l">${tokenLabel} MCAP{ticker.change24h != null ? ' ' + (ticker.change24h >= 0 ? '▲' : '▼') + Math.abs(ticker.change24h).toFixed(1) + '%' : ''}</span>
        </div>
      </div>
      <div className="cine-feed">
        <div className="cine-feed-h">LIVE TRAVELER ACTIVITY</div>
        {feed.slice(-6).map((f) => <div key={f.id} className={'cf-item ' + (f.kind || '')}>{f.text}</div>)}
        {feed.length === 0 ? <div className="cf-item">Listening to the relay network…</div> : null}
      </div>
      {caption ? <div className="cine-caption">{caption}</div> : null}
      <div className="cine-cta">{fmt(window.UNIVERSE.TOTAL_PLANETS)} WORLDS · {cacheTotal()} REWARD CACHES · JOIN THE HUNT AT <b>NOMANSOL.COM</b></div>
      <button className="cine-exit clickable" onClick={onExit}>■ END CINEMATIC</button>
      {scan && scan.planet ? <CineScanCard scan={scan} /> : null}
    </div>
  );
}

// ---------------- intro trailer overlay ("What is No Man's SOL") ----------------
function TrailerOverlay({ card, fade, onSkip }) {
  useEffect(() => {
    const h = (e) => { if (e.key === 'Escape') onSkip(); };
    addEventListener('keydown', h);
    return () => removeEventListener('keydown', h);
  }, [onSkip]);
  return (
    <div className="trailer hud-compact">
      <div className="cine-bar top"></div>
      <div className="cine-bar bottom"></div>
      <div className="cine-brand">
        <Logo size={26} />
        <div>
          <div className="bt">NO MAN'S <em>SOL</em></div>
        </div>
      </div>
      {card && card.title ? (
        <div className={'trailer-card' + (card.kind ? ' ' + card.kind : '')} key={card.title}>
          <div className="tc-title">{card.title}</div>
          {card.sub ? <div className="tc-sub">{card.sub}</div> : null}
        </div>
      ) : null}
      <button className="trailer-skip clickable" onClick={onSkip}>SKIP INTRO ▸</button>
      <div className={'trailer-fade' + (fade ? ' on' : '')}></div>
    </div>
  );
}

// ---------------- how-it-works modal ----------------
function RewardsModal({ onClose }) {
  return (
    <div className="modal-bg hud-compact" onClick={onClose}>
      <div className="modal panel" onClick={(e) => e.stopPropagation()}>
        <div className="ph"><h3>HOW {cacheTotal()} CACHES STAY UNHACKABLE</h3><button className="btn ghost" style={{ padding: '4px 10px' }} onClick={onClose}>CLOSE</button></div>
        <div className="mbody">
          <h4>THE HUNT</h4>
          <p><b>{cacheTotal()} worlds</b> across {fmt(window.UNIVERSE.TOTAL_PLANETS)} hold a reward cache worth <b>{cachePct()}% of the live reward pool</b> each — pump.fun vault plus creator wallet, split across finders. The <b>Reward Caches</b> bar tracks the full pool live via Helius. Explore, deep-scan a planet, and claim with your wallet if you hit one.</p>
          <h4>LOCATIONS NEVER SHIP TO YOUR BROWSER</h4>
          <p>At season start the server locks exactly {cacheTotal()} coordinates using a secret <code>REWARD_SALT</code>. They live in <b>Postgres only</b> — not in client JavaScript, not in any public list API. The only public artifact is a <b>Merkle root commitment</b> so anyone can verify the set was fixed before play began. The full coordinate list is never downloadable.</p>
          <h4>SCAN-ONLY REVEAL</h4>
          <p>You learn a cache exists only when your wallet scans that exact planet and the server returns a hit. No datamining the client, no coordinate scraping, no guessing from leaked files — you have to be there and scan it. Wallet-signed login is required for every scan and claim.</p>
          <h4>CLAIM NOW — OR WAIT</h4>
          <p>When a deep scan hits a cache, you decide: <b>claim right away</b> or <b>log it and claim later</b>. Logging records the find in your traveler log — it does not lock the cache. Wait and the creator-fee vault may grow, so your payout could be higher when you finally claim; wait too long and <b>another traveler may scan that world and claim it first</b>. The first valid wallet to claim a cache keeps it — once claimed, that coordinate is gone for everyone else. Your reward is <b>{cachePct()}% of the live vault at claim time</b> (not when you first scanned) — as trading fees accumulate, later claims can pay more.</p>
          <h4>DETERMINISTIC UNIVERSE</h4>
          <p>Every world derives from its integer ID — terrain, color, rings, weather. The universe is identical for every traveler, forever. What differs is who gets there first.</p>
        </div>
      </div>
    </div>
  );
}

// ---------------- top bar ----------------
// ---------------- global comms (chat) ----------------
function ChatPanel({ msgs, wallet, onClose, onWarn }) {
  const [text, setText] = useState('');
  const listRef = useRef(null);
  useEffect(() => {
    if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
  }, [msgs]);
  const send = () => {
    const t = text.trim();
    if (!t) return;
    const r = window.NMS_RT ? window.NMS_RT.sendChat(t) : { ok: false, reason: 'offline' };
    if (r.ok) setText('');
    else if (r.reason === 'no-auth') onWarn('Connect your wallet to transmit on global comms.');
    else onWarn('Comms relay offline — reconnecting…');
  };
  return (
    <div className="chat-panel hud-compact">
      <div className="ph"><h3>GLOBAL COMMS</h3><span className="ph-r clickable" onClick={onClose}>✕</span></div>
      <div className="chat-list" ref={listRef}>
        {msgs.length === 0 ? (
          <div className="chat-empty">No transmissions yet. Say hello to the universe.</div>
        ) : msgs.map((m, i) => (
          <div key={i} className={'chat-msg' + (wallet && m.wallet === wallet ? ' mine' : '')}>
            <span className="chat-who">{m.who || shortAddr(m.wallet)}</span>
            <span className="chat-text">{m.text}</span>
          </div>
        ))}
      </div>
      <div className="chat-input">
        <input value={text} maxLength={240} placeholder={wallet ? 'Transmit to all travelers…' : 'Connect wallet to chat'}
          onChange={(e) => setText(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter') send(); }} />
        <button className="btn" onClick={send}>SEND</button>
      </div>
    </div>
  );
}

const BOOST_NAMES = Object.fromEntries(BOOSTS.map((b) => [b.id, b.name]));

function travelerBoosts(t) {
  if (Array.isArray(t.boosts)) return t.boosts;
  return t.boosts ? ['active'] : [];
}

function TravelersModal({ travelers, loading, online, onClose }) {
  const totalScans = travelers.reduce((n, t) => n + (t.scans || 0), 0);
  const totalGalaxies = travelers.reduce((n, t) => n + (t.galaxies || 0), 0);
  const anon = Math.max(0, online - travelers.length);
  return (
    <div className="modal-bg hud-compact" onClick={onClose}>
      <div className="modal panel" onClick={(e) => e.stopPropagation()}>
        <div className="ph"><h3>TRAVELERS ONLINE</h3><button className="btn ghost" style={{ padding: '4px 10px' }} onClick={onClose}>CLOSE</button></div>
        <div className="log-stats">
          <span><b>{fmt(online)}</b> CONNECTED</span>
          <span><b>{fmt(totalGalaxies)}</b> GALAXIES VIEWED</span>
          <span><b>{fmt(totalScans)}</b> PLANETS SCANNED</span>
        </div>
        <div className="logbody">
          {loading ? (
            <div className="hint" style={{ padding: 14 }}>Scanning relay network…</div>
          ) : travelers.length === 0 ? (
            <div className="hint" style={{ padding: 14 }}>No wallet-connected travelers on the relay right now.</div>
          ) : travelers.map((t) => {
            const boosts = travelerBoosts(t);
            return (
              <div key={t.wallet} className="log-row">
                <div className="log-row-top">
                  <a className="lr-name traveler-addr" href={solscanAcct(t.wallet)} target="_blank" rel="noopener noreferrer" title={t.wallet + ' — view on Solscan'}>
                    {t.who || shortAddr(t.wallet)} ↗
                  </a>
                  {t.cacheFound ? <span className="lr-meta gold">⚑ CACHE{t.claims ? ' × ' + t.claims : ' FOUND'}</span> : null}
                  <span className="lr-meta">{fmt(t.galaxies)} GALAXIES · {fmt(t.scans)} PLANETS SCANNED</span>
                </div>
                <div className="lr-planets">
                  {boosts.length ? boosts.map((b) => (
                    <span key={b} className="lr-chip gold" title="Active 24h boost">⚡ {BOOST_NAMES[b] || 'BOOSTS ACTIVE'}</span>
                  )) : <span className="lr-chip dim">NO ACTIVE BOOSTS</span>}
                </div>
              </div>
            );
          })}
          {!loading && anon > 0 ? (
            <div className="log-row traveler-anon">
              <span className="lr-meta">+ {fmt(anon)} ANONYMOUS TRAVELER{anon === 1 ? '' : 'S'} — wallet not connected</span>
            </div>
          ) : null}
        </div>
        <div className="travelers-foot">CLICK A TRAVELER TO VIEW THEIR WALLET ON SOLSCAN</div>
      </div>
    </div>
  );
}

function TopBar({ mode, system, planet, wallet, online, ticker, visited, isDev, tokenLabel, onSearch, onTicker, onLog, onChat, chatOpen, onCinema, onCacheDemo, onDisconnect, onGuide,
  boosts, buying, onBuy, autoOn, onToggleAuto, activityFeed,
  travelersOpen, onTravelers,
  audioMusic, audioHover, audioWarp, onMusic, onHover, onWarp }) {
  const [q, setQ] = useState('');
  const [walletOpen, setWalletOpen] = useState(false);
  const [boostsOpen, setBoostsOpen] = useState(false);
  const walletWrapRef = useRef(null);
  const submit = () => { if (onSearch(q)) setQ(''); };
  const inGame = mode === 'galaxy' || mode === 'system' || mode === 'planet';
  useEffect(() => {
    if (!walletOpen && !boostsOpen) return;
    const close = (e) => {
      if (walletWrapRef.current && !walletWrapRef.current.contains(e.target)) {
        setWalletOpen(false);
        setBoostsOpen(false);
      }
    };
    addEventListener('pointerdown', close);
    return () => removeEventListener('pointerdown', close);
  }, [walletOpen, boostsOpen]);
  return (
    <div className="topbar hud-compact">
      <div className="brand-col">
        <div className="brand">
          <Logo />
          <div>
            <div className="bt">NO MAN'S <em>SOL</em></div>
            <div className="bs">{fmt(window.UNIVERSE.TOTAL_PLANETS)} WORLDS · SOLANA</div>
          </div>
        </div>
        <ActivityFeed items={activityFeed || []} />
      </div>
      <div className="topbar-center">
        {mode === 'galaxy' ? (
          <div className="search" data-guide="search">
            <input value={q} placeholder="GALAXY CODE · e.g. EUCLID-7F3A1 or #123456" onChange={(e) => setQ(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') submit(); }} />
            <button className="btn" onClick={submit}>LOCATE</button>
          </div>
        ) : null}
      </div>
      <div className="chips">
        <div className={'chip clickable ' + ((ticker.change24h ?? 0) >= 0 ? 'up' : 'down')} onClick={onTicker} title="Market cap via Solana Tracker">
          <span className="lbl">MCAP · ${tokenLabel}</span>
          <span>{fmtMcap(ticker.marketCapUsd)}</span>
          {ticker.change24h != null ? (
            <span>{ticker.change24h >= 0 ? '▲' : '▼'}{Math.abs(ticker.change24h).toFixed(1)}%</span>
          ) : null}
        </div>
        <div className={'chip clickable travelers-chip' + (travelersOpen ? ' up' : '')} data-guide="travelers" onClick={() => onTravelers(!travelersOpen)} title="View active travelers">
          <span className="dot"></span><span className="lbl">TRAVELERS</span><span>{fmt(online)}</span>
        </div>
        <div className="chip clickable" data-guide="log" onClick={onLog}><span className="lbl">LOG</span><span>{visited} GAL</span></div>
        <div className={'chip clickable' + (chatOpen ? ' up' : '')} data-guide="comms" onClick={onChat}><span className="lbl">COMMS</span><span>{chatOpen ? 'LIVE ▾' : 'CHAT ▸'}</span></div>
        <div className="chip clickable" data-guide="guide" onClick={onGuide} title="Interactive tour of the app"><span className="lbl">GUIDE</span><span>HOW TO ▸</span></div>
        <div className="audio-bar" data-guide="audio">
          <AudioToggles horizontal music={audioMusic} hover={audioHover} warp={audioWarp}
            onMusic={onMusic} onHover={onHover} onWarp={onWarp} />
        </div>
        {isDev ? (
          <React.Fragment>
            <div className="chip clickable cinema" onClick={onCinema}><span className="lbl">DEV</span><span>▶ CINEMATIC</span></div>
            <div className="chip clickable cinema" onClick={onCacheDemo} title="Full cache-find demo (warp + scan)"><span className="lbl">DEV</span><span>⚠ TEST CACHE</span></div>
          </React.Fragment>
        ) : null}
        <div className="wallet-wrap" ref={walletWrapRef}>
          <div className="chip wallet clickable" onClick={() => { setBoostsOpen(false); setWalletOpen((o) => !o); }}>
            <span className="lbl">WALLET</span><span>{shortAddr(wallet)}</span>
          </div>
          {walletOpen ? (
            <div className="wallet-menu">
              <div className="wallet-menu-addr">{wallet}</div>
              <button className="btn" onClick={() => { setWalletOpen(false); onDisconnect(); }}>DISCONNECT</button>
            </div>
          ) : null}
          {inGame && wallet ? (
            <div className="boosts-wrap">
              <button
                type="button"
                className={'boosts-fab' + (boostsOpen ? ' open' : '') + (boosts.length ? ' has-owned' : '')}
                onClick={() => { setWalletOpen(false); setBoostsOpen((o) => !o); }}
                aria-expanded={boostsOpen}
                aria-label="Boosts"
                title="Boosts — 24h power-ups"
                data-guide="boosts"
              >
                <span className="boosts-fab-ring" aria-hidden="true"></span>
                <span className="boosts-fab-icon" aria-hidden="true">⚡</span>
              </button>
              {boostsOpen ? (
                <div className="boosts-drop">
                  <BoostsPanel owned={boosts} buying={buying} onBuy={onBuy} autoOn={autoOn} onToggleAuto={onToggleAuto} />
                </div>
              ) : null}
            </div>
          ) : null}
        </div>
      </div>
    </div>
  );
}

// ---------------- connect / title screen ----------------
function ChromeLogo({ size = 15 }) {
  return (
    <svg className="boot-chrome-icon" width={size} height={size} viewBox="0 0 48 48" aria-hidden="true">
      <circle cx="24" cy="24" r="22" fill="#fff" />
      <path fill="#EA4335" d="M24 8c7.2 0 13.5 3.9 16.9 9.7H24a9.9 9.9 0 0 0-8.6 5l-7.5-13A22 22 0 0 1 24 8z" />
      <path fill="#FBBC05" d="M8.9 22.7a22 22 0 0 0 0 2.6l13-7.5a9.9 9.9 0 0 0-.1 5.1l-12.9 7.4z" />
      <path fill="#34A853" d="M24 40a22 22 0 0 1-19-11l13-7.5A9.9 9.9 0 0 0 24 34h16.9A22 22 0 0 1 24 40z" />
      <path fill="#4285F4" d="M40.9 18.7H24a9.9 9.9 0 0 0-3.4 5.8l12.9 7.4A22 22 0 0 0 40.9 18.7z" />
      <circle cx="24" cy="24" r="9" fill="#fff" />
      <circle cx="24" cy="24" r="7.2" fill="#4285F4" />
    </svg>
  );
}

function XLogo() {
  return (
    <svg className="boot-x-icon" viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
    </svg>
  );
}

function ConnectChromeHint() {
  return (
    <div className="connect-chrome-hint" role="note">
      <ChromeLogo />
      <div className="boot-chrome-body">
        <div className="boot-chrome-label">MAC USERS · CHROME</div>
        <ol className="boot-chrome-steps">
          <li>Click the three vertical dots in the top-right corner of Chrome.</li>
          <li>Select <b>Settings</b> from the drop-down menu.</li>
          <li>In the left sidebar, click <b>System</b>.</li>
          <li>Toggle on <b>Use graphics acceleration when available</b>.</li>
          <li>Click <b>Relaunch</b> to restart Chrome and apply the changes.</li>
        </ol>
      </div>
    </div>
  );
}

function BootWebGLError() {
  return (
    <div className="boot hud-compact">
      <div className="boot-card">
        <div className="boot-title">WEBGL UNAVAILABLE</div>
        <div className="boot-sub" style={{ marginTop: 12 }}>
          Your browser could not start the 3D universe. Enable <b>graphics acceleration</b> in browser settings,
          disable extensions that block WebGL, then hard-refresh. Safari or Firefox often works if Chrome is locked down.
        </div>
      </div>
      <ConnectChromeHint />
    </div>
  );
}

function Boot({ onEnter, wallet, onConnect, connecting, onTrailer, tokenGate, tokenMint }) {
  const pumpUrl = tokenMint ? `https://pump.fun/coin/${tokenMint}` : null;
  return (
    <div className="boot hud-compact">
      <div className="boot-panel">
        <div className="boot-logo-wrap">
          <img className="boot-logo" src={LOGO_SRC} alt="No Man's SOL" draggable={false} />
        </div>
        <div className="boot-card">
          <div className="boot-title">NO MAN'S <em>SOL</em></div>
          <div className="boot-sub">AN INFINITE-FEELING, FULLY DETERMINISTIC UNIVERSE.<br />{cacheTotal()} WORLDS HIDE CREATOR-REWARD CACHES. FIND THEM FIRST.</div>
          <div className="boot-stats">
            <div><span className="n">{fmt(window.UNIVERSE.TOTAL_PLANETS)}</span><span className="l">WORLDS</span></div>
            <div><span className="n">{fmt(window.UNIVERSE.SYS)}</span><span className="l">GALAXIES</span></div>
            <div><span className="n">{cacheTotal()}</span><span className="l">REWARD CACHES</span></div>
            <div><span className="n">{cachePct()}%</span><span className="l">FEES PER CACHE</span></div>
          </div>
          <div className="boot-actions">
            <div className={`boot-wallet${wallet ? '' : ' boot-wallet--placeholder'}`} aria-hidden={!wallet}>
              {wallet ? `● WALLET LINKED · ${shortAddr(wallet)}` : '● WALLET LINKED'}
            </div>
            {wallet ? (
              <button className="btn lg" onClick={onEnter}>INITIALIZE — ENTER UNIVERSE</button>
            ) : (
              <button className="btn lg" disabled={connecting} onClick={onConnect}>{connecting ? 'CONNECTING…' : 'CONNECT SOLANA WALLET'}</button>
            )}
          </div>
          <button className="btn ghost boot-trailer" onClick={onTrailer}>▶ WHAT IS NO MAN'S SOL?</button>
          <div className="boot-foot">
            <div className="boot-follow">
              <span className="boot-follow-lbl">FOLLOW US ON</span>
              <a
                className="boot-x"
                href="https://x.com/NoMansSOL"
                target="_blank"
                rel="noopener noreferrer"
                aria-label="NoMansSOL on X"
              >
                <XLogo />
              </a>
            </div>
            {pumpUrl ? (
              <a className="boot-ca" href={pumpUrl} target="_blank" rel="noopener noreferrer" title={tokenMint}>
                <span className="boot-ca-lbl">CA</span>
                <span className="boot-ca-addr">{tokenMint}</span>
              </a>
            ) : null}
            {tokenGate && !tokenGate.skipped ? (
              <div className="boot-gate">REQUIRES <b>{fmt(tokenGate.minBalance)} ${tokenGate.symbol}</b> IN YOUR CONNECTED WALLET.</div>
            ) : null}
          </div>
        </div>
        <ConnectChromeHint />
      </div>
    </div>
  );
}

function AudioToggleBtn({ on, icon, label, desc, onToggle }) {
  return (
    <button type="button" className={'audio-btn' + (on ? ' on' : ' off')} onClick={onToggle} aria-label={label} aria-pressed={on}>
      <span className="audio-ico" aria-hidden="true">{icon}</span>
      <span className="audio-tip" role="tooltip">
        <span className="audio-tip-lbl">{label}{on ? '' : ' · OFF'}</span>
        <span className="audio-tip-desc">{desc}</span>
      </span>
    </button>
  );
}

function AudioToggles({ music, hover, warp, onMusic, onHover, onWarp, horizontal }) {
  return (
    <div className={'audio-toggles' + (horizontal ? ' audio-toggles-h' : ' panel')}>
      <AudioToggleBtn on={music} icon="♪" label="BACKGROUND MUSIC" desc="Generative score & ambient bed" onToggle={() => onMusic(!music)} />
      <AudioToggleBtn on={hover} icon="✦" label="GALAXY HOVER BEEPS" desc="Tick when pointing at a galaxy" onToggle={() => onHover(!hover)} />
      <AudioToggleBtn on={warp} icon="⇢" label="WARP WOOSH" desc="Lightspeed transition SFX" onToggle={() => onWarp(!warp)} />
    </div>
  );
}

// ---------------- root ----------------
const GL_READY = window.NMS.init();

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [mode, setMode] = useState('boot');
  const [system, setSystem] = useState(null);
  const [planets, setPlanets] = useState([]);
  const [planet, setPlanet] = useState(null);
  const [anomaly, setAnomaly] = useState(false);
  const [anomalyK, setAnomalyK] = useState(-1);
  const [wallet, setWallet] = useState(null);
  const [connecting, setConnecting] = useState(false);
  const connectBusy = useRef(false);
  const [online, setOnline] = useState(1);
  const [chatOpen, setChatOpen] = useState(false);
  const [chatMsgs, setChatMsgs] = useState([]);
  const [travelersOpen, setTravelersOpen] = useState(false);
  const [travelersList, setTravelersList] = useState([]);
  const [travelersLoading, setTravelersLoading] = useState(false);
  const [claimed, setClaimed] = useState(0);
  const [scanState, setScanState] = useState({ signal: 0 });
  const [claiming, setClaiming] = useState(false);
  const [claimedNow, setClaimedNow] = useState(false);
  const [showInfo, setShowInfo] = useState(false);
  const [showLog, setShowLog] = useState(false);
  const [showCaches, setShowCaches] = useState(false);
  const [showGuide, setShowGuide] = useState(false);
  const [tourChapter, setTourChapter] = useState(null);
  const [boosts, setBoosts] = useState(() => (window.NMS.getBoosts ? window.NMS.getBoosts() : []));
  const [buying, setBuying] = useState(null);
  const [autoOn, setAutoOn] = useState(() => (window.NMS.getAutoScan ? window.NMS.getAutoScan() : true));
  const [cineOn, setCineOn] = useState(false);
  const [cineScan, setCineScan] = useState(null);
  const [cineCaption, setCineCaption] = useState('');
  const [trailerOn, setTrailerOn] = useState(false);
  const [trailerCard, setTrailerCard] = useState(null);
  const [trailerFade, setTrailerFade] = useState(false);
  const [feed, setFeed] = useState([]);
  const feedTimers = useRef(new Map());
  const cineRef = useRef(false);
  cineRef.current = cineOn;
  const [log, setLog] = useState(() => ({ ...(window.NMS.getLog ? window.NMS.getLog() : {}) }));
  const [ticker, setTicker] = useState(() => (window.NMS_API?.getTicker?.() || { symbol: 'NMS', marketCapUsd: null, change24h: null }));
  const [rewards, setRewards] = useState(() => (window.NMS_API ? window.NMS_API.getRewards() : null));
  const [tokenGate, setTokenGate] = useState(null);
  const [tokenMint, setTokenMint] = useState(null);
  const [apiOnline, setApiOnline] = useState(false);
  const [audioMusic, setAudioMusic] = useState(() => (window.NMS_AUDIO?.getMusic ? window.NMS_AUDIO.getMusic() : true));
  const [audioHover, setAudioHover] = useState(() => (window.NMS_AUDIO?.getHover ? window.NMS_AUDIO.getHover() : true));
  const [audioWarp, setAudioWarp] = useState(() => (window.NMS_AUDIO?.getWarp ? window.NMS_AUDIO.getWarp() : true));

  // theme + tweaks side effects
  useEffect(() => {
    const th = THEMES[t.theme] || THEMES['Holo Cyan'];
    const r = document.documentElement.style;
    r.setProperty('--acc', th.acc); r.setProperty('--acc2', th.acc2);
    r.setProperty('--acc-dim', th.dim); r.setProperty('--acc-faint', th.faint);
    r.setProperty('--panel-line', th.line);
  }, [t.theme]);
  useEffect(() => {
    window.NMS.setTweaks({ orbitSpeed: t.orbitSpeed, labels: t.labels, glow: t.glow, sound: t.sound, nebula: t.nebula });
  }, [t.orbitSpeed, t.labels, t.glow, t.sound, t.nebula]);
  useEffect(() => { window.NMS_AUDIO.setMusic(audioMusic); }, [audioMusic]);
  useEffect(() => { window.NMS_AUDIO.setHover(audioHover); }, [audioHover]);
  useEffect(() => { window.NMS_AUDIO.setWarp(audioWarp); }, [audioWarp]);

  useEffect(() => {
    if (!travelersOpen || !window.NMS_API) return undefined;
    let cancelled = false;
    setTravelersLoading(true);
    const load = () => window.NMS_API.fetchTravelers().then((r) => {
      if (!cancelled && r && r.ok) setTravelersList(r.travelers || []);
    });
    const t = setInterval(load, 10000);
    window.NMS_API.fetchTravelers().then((r) => {
      if (!cancelled && r?.travelers) setTravelersList(r.travelers);
    }).finally(() => {
      if (!cancelled) setTravelersLoading(false);
    });
    return () => { cancelled = true; clearInterval(t); };
  }, [travelersOpen]);

  const dismissToast = useCallback((id) => {
    const tm = feedTimers.current.get(id);
    if (tm) {
      clearTimeout(tm);
      feedTimers.current.delete(id);
    }
    setFeed((f) => f.filter((x) => x.id !== id));
  }, []);

  const pushToast = useCallback((kind, text) => {
    const id = ++toastId;
    setFeed((f) => [...f.slice(-(FEED_MAX - 1)), { id, kind, text }]);
    const tm = setTimeout(() => dismissToast(id), FEED_TTL_MS);
    feedTimers.current.set(id, tm);
  }, [dismissToast]);

  useEffect(() => () => {
    feedTimers.current.forEach((tm) => clearTimeout(tm));
    feedTimers.current.clear();
  }, []);

  useNmsEvent('state', useCallback((d) => {
    setMode(d.mode);
    if (d.system !== undefined) setSystem(d.system);
    if (d.planets) setPlanets(d.planets);
    if (d.mode === 'planet') {
      setPlanet(d.planet);
      setAnomaly(!!d.anomaly);
      setClaimedNow(false);
      setScanState(d.priorScan ? scanStateFromPrior(d.priorScan, d.planet) : { signal: 0 });
    } else { setPlanet(null); }
    if (d.mode === 'system') {
      setScanState({ signal: 0 });
      if (d.anomalyK !== undefined) setAnomalyK(d.anomalyK);
    }
    if (d.mode === 'galaxy') { setAnomalyK(-1); setScanState({ signal: 0 }); }
  }, []));
  useNmsEvent('log', useCallback((d) => setLog({ ...d.log }), []));
  useNmsEvent('wallet', useCallback((d) => {
    if (!d.wallet) setLog({});
    else setLog({ ...(window.NMS.getLog ? window.NMS.getLog() : {}) });
  }, []));
  useNmsEvent('boosts', useCallback((d) => setBoosts(d.owned), []));
  useNmsEvent('autoscan', useCallback((d) => setAutoOn(d.on), []));
  useNmsEvent('cine', useCallback((d) => {
    setCineOn(d.active);
    setTrailerOn(!!(d.active && d.trailer));
    if (!d.active) { setCineScan(null); setCineCaption(''); setTrailerCard(null); setTrailerFade(false); }
  }, []));
  useNmsEvent('trailercard', useCallback((d) => setTrailerCard(d && d.title ? d : null), []));
  useNmsEvent('trailerfade', useCallback((d) => setTrailerFade(!!(d && d.on)), []));
  useNmsEvent('cinescan', useCallback((d) => setCineScan(d && d.planet ? d : null), []));
  useNmsEvent('cinecaption', useCallback((d) => setCineCaption(d.text || ''), []));
  // auto-dismiss the cinematic scan card so it never lingers between scenes
  useEffect(() => {
    if (!cineScan) return;
    const tm = setTimeout(() => setCineScan(null), 7000);
    return () => clearTimeout(tm);
  }, [cineScan]);
  useNmsEvent('select', useCallback((d) => { setSystem(d.sys); }, []));
  useNmsEvent('online', useCallback((d) => setOnline(d.n), []));
  const mergeChat = useCallback((prev, incoming) => {
    const items = incoming || [];
    if (!items.length) return prev;
    if (!prev.length) return items;
    const seen = new Set();
    const merged = [];
    for (const m of [...prev, ...items]) {
      const key = (m.ts || 0) + '|' + (m.wallet || '') + '|' + (m.text || '');
      if (seen.has(key)) continue;
      seen.add(key);
      merged.push(m);
    }
    merged.sort((a, b) => (a.ts || 0) - (b.ts || 0));
    return merged.slice(-100);
  }, []);
  useNmsEvent('chat', useCallback((d) => setChatMsgs((m) => mergeChat(m, [d])), [mergeChat]));
  useNmsEvent('chathistory', useCallback((d) => setChatMsgs((m) => mergeChat(m, d.items)), [mergeChat]));
  useNmsEvent('chaterror', useCallback((d) => pushToast('warn', d.reason === 'rate_limited'
    ? 'Comms: transmitting too fast — wait a moment.'
    : 'Comms: connect your wallet to transmit.'), [pushToast]));
  useNmsEvent('toast', useCallback((d) => pushToast(d.kind, d.text), [pushToast]));
  useNmsEvent('activity', useCallback((d) => {
    if (!d.text) return;
    pushToast(d.kind || 'net', d.text);
  }, [pushToast]));
  // Past relay events are not replayed as toasts — only live activity via pushToast.
  useNmsEvent('claims', useCallback((d) => setClaimed(d.claimed), []));
  useNmsEvent('rewards', useCallback((d) => setRewards(d), []));
  useNmsEvent('api', useCallback((d) => setApiOnline(!!d.online), []));
  useEffect(() => {
    if (!window.NMS_API) return;
    window.NMS_API.ping().then((h) => {
      if (h?.tokenGate) setTokenGate(h.tokenGate);
      if (h?.tokenMint) setTokenMint(h.tokenMint);
    });
  }, []);
  useNmsEvent('scan', useCallback((d) => {
    if (!d.ok) {
      setScanState({ signal: 0, busy: false, msg: 'NO TARGET — approach a planet before scanning.' });
      return;
    }
    if (d.phase === 'start') {
      setScanState({
        signal: 0, busy: true, result: null,
        msg: d.sweep
          ? <span>Full-galaxy sweep of <b>{d.name}</b> in progress — scanning all bodies…</span>
          : <span>Surface sweep of <b>{d.name}</b> in progress…</span>,
      });
      return;
    }
    if (d.sweep) {
      setScanState({
        signal: d.found ? 1 : 0.72, busy: false, found: d.found, result: null,
        msg: d.found
          ? <span>⚠ GALAXY SWEEP COMPLETE — cache signature on <b>{d.name}</b>.</span>
          : <span>Galaxy sweep complete — all {d.totalInSystem} bodies surveyed. No cache signature.</span>,
      });
      if (d.found) { setAnomalyK(d.k); pushToast('gold', '⚠ ANOMALY DETECTED: ' + d.name); }
      else pushToast('sys', 'GALAXY SWEEP COMPLETE — ' + d.totalInSystem + ' BODIES SURVEYED');
      return;
    }
    setScanState({
      signal: d.found ? 1 : 0.72, busy: false, found: d.found,
      result: { ...d.planet, found: d.found, scanned: d.scannedInSystem, total: d.totalInSystem },
      msg: d.found
        ? <span>⚠ REWARD CACHE SIGNATURE ON <b>{d.name}</b> — claim it from the dossier.</span>
        : <span>Sweep of <b>{d.name}</b> complete. Survey logged: {d.scannedInSystem}/{d.totalInSystem} in this galaxy.</span>,
    });
    if (d.found) { setAnomalyK(d.k); pushToast('gold', '⚠ ANOMALY DETECTED: ' + d.name); }
    else pushToast('sys', 'SCAN LOGGED: ' + d.name);
  }, [pushToast]));

  // keyboard
  useEffect(() => {
    const h = (e) => {
      if (e.key !== 'Escape') return;
      if (cineRef.current) { window.NMS.stopCinematic(); return; }
      if (window.NMS.getMode() === 'planet' || window.NMS.getMode() === 'system') window.NMS.back();
    };
    addEventListener('keydown', h);
    return () => removeEventListener('keydown', h);
  }, []);

  useNmsEvent('ticker', useCallback((d) => setTicker(d), []));

  const disconnect = async () => {
    if (window.solana && window.solana.isPhantom && window.solana.isConnected) {
      try { await window.solana.disconnect(); } catch (e) { /* ignore */ }
    }
    if (window.NMS_API) window.NMS_API.disconnect();
    setWallet(null);
    setLog({});
    if (window.NMS.getMode() !== 'boot') window.NMS.returnToBoot();
    pushToast('sys', 'Wallet disconnected');
  };

  const connect = async () => {
    if (connectBusy.current) return;
    connectBusy.current = true;
    setConnecting(true);
    window.NMS_AUDIO.boot();
    try {
      const provider = window.solana;
      if (provider && provider.isPhantom) {
        try {
          await provider.connect();
        } catch (e) {
          const rejected = e && (e.code === 4001 || /reject|denied|cancel/i.test(e.message || ''));
          pushToast('warn', rejected
            ? 'Wallet connection cancelled.'
            : ('Wallet connection failed — ' + (e.message || 'try again.')));
          return;
        }
        const pk = provider.publicKey;
        if (!pk) {
          pushToast('warn', 'Wallet connected but no public key returned — try again.');
          return;
        }
        const w = pk.toString();
        if (window.NMS_API) {
          const gate = await window.NMS_API.checkGate(w);
          if (!gate.ok && !gate.skipped) {
            if (gate.reason === 'gate_failed' || gate.required == null) {
              pushToast('warn', 'Could not verify $NMS balance — server may be updating. Try again shortly.');
            } else {
              const held = gate.balance != null ? Math.floor(gate.balance).toLocaleString('en-US') : '0';
              pushToast('warn', `You need at least ${fmt(gate.required)} $${gate.symbol || 'NMS'} to play (${held} held).`);
            }
            try { await provider.disconnect(); } catch (e) { /* ignore */ }
            return;
          }
        }
        // Prove wallet ownership with a signature before doing anything else.
        const auth = window.NMS_API ? await window.NMS_API.authenticate(w) : { ok: false, reason: 'no-api' };
        if (!auth.ok) {
          const msgs = {
            'user_rejected': 'Sign-in cancelled — approve the signature request to play.',
            'no-signmessage': 'This wallet cannot sign messages. Try Phantom or Solflare.',
            'insufficient_token': `You need at least ${fmt(auth.required || 1000)} $${auth.symbol || 'NMS'} to play (${auth.balance != null ? Math.floor(auth.balance).toLocaleString('en-US') : '0'} held).`,
            'no-wallet': 'No Solana wallet found.',
            'no-api': 'Game server unreachable — try again shortly.',
          };
          const reason = auth.reason || 'unknown';
          const fetchFail = reason.includes('Failed to fetch');
          pushToast('warn', msgs[reason] || (fetchFail
            ? 'Cannot reach game server — check your connection and hard-refresh.'
            : ('Sign-in failed: ' + reason + ' — hard-refresh and retry.')));
          return;
        }
        setWallet(w);
        await window.NMS_API.sync(w);
        pushToast('gold', '● WALLET AUTHENTICATED · ' + shortAddr(w));
      } else {
        // No Solana wallet: local demo only. No server auth, boosts disabled.
        await new Promise((r) => setTimeout(r, 700));
        const w = mockAddr();
        window.UNIVERSE.bindWallet(w);
        setWallet(w);
        pushToast('sys', 'No Solana wallet detected — local demo mode (boosts & claims disabled).');
      }
    } catch (e) {
      console.warn('[connect]', e);
      const msg = (e && e.message) ? String(e.message) : 'unknown error';
      pushToast('warn', 'Wallet connection failed — ' + msg);
    } finally {
      connectBusy.current = false;
      setConnecting(false);
    }
  };

  const startIntroTrailer = () => {
    if (!window.NMS.isReady()) {
      pushToast('warn', '3D engine not ready — enable WebGL / graphics acceleration and refresh.');
      return;
    }
    window.NMS_AUDIO.boot();
    window.NMS.startTrailer();
  };

  const enter = async () => {
    if (!window.NMS.isReady()) {
      pushToast('warn', '3D engine not ready — enable WebGL / graphics acceleration and refresh.');
      return;
    }
    if (wallet && window.NMS_API) {
      const gate = await window.NMS_API.checkGate(wallet);
      if (!gate.ok && !gate.skipped) {
        if (gate.reason === 'gate_failed' || gate.required == null) {
          pushToast('warn', 'Could not verify $NMS balance — server may be updating. Try again shortly.');
        } else {
          const held = gate.balance != null ? Math.floor(gate.balance).toLocaleString('en-US') : '0';
          pushToast('warn', `You need at least ${fmt(gate.required)} $${gate.symbol || 'NMS'} to enter (${held} held).`);
        }
        return;
      }
    }
    window.NMS_AUDIO.boot(); window.NMS.enter();
    // First visit: offer the field guide once the galaxy is on screen.
    try {
      if (!localStorage.getItem('nmsol_guide_seen')) {
        localStorage.setItem('nmsol_guide_seen', '1');
        setTimeout(() => setShowGuide(true), 1600);
      }
    } catch (e) { /* storage blocked — skip */ }
  };

  const doScan = () => { window.NMS.scan(); };
  const doSweep = () => { window.NMS.scanSystem(); };

  const doBuy = async (b) => {
    if (buying) return;
    setBuying(b.id);
    let r;
    try { r = await window.NMS.buyBoost(b.id); } catch (e) { r = { ok: false, reason: e.message }; }
    setBuying(null);
    if (r && r.ok) {
      pushToast('gold', '✓ BOOST ACTIVATED: ' + b.name + ' — 24H · ◎ ' + b.price + ' SOL');
      return;
    }
    const reasons = {
      'no-auth': 'Connect and sign in with a Solana wallet first.',
      'no-wallet': 'A Solana wallet (Phantom) is required to pay.',
      'no-web3': 'Payment library failed to load — refresh and try again.',
      'user_rejected': 'Payment cancelled.',
      'sign_failed': 'Wallet could not sign the payment — approve the Phantom transaction prompt.',
      'wallet_mismatch': 'Connected wallet does not match your signed-in account. Reconnect the same wallet.',
      'payment_unverified': 'Payment could not be verified on-chain. If SOL left your wallet, contact support.',
      'payment_already_used': 'That payment was already redeemed.',
      'info_failed': 'Could not reach the payment service. Try again.',
      'active': 'That boost is already active.',
    };
    const msg = (r && r.detail && (r.reason === 'sign_failed' || r.reason === 'payment_unverified'))
      ? ('Boost purchase failed: ' + r.detail)
      : reasons[r && r.reason]
      || (r && r.detail ? 'Boost purchase failed: ' + r.detail : null)
      || ('Boost purchase failed: ' + ((r && r.reason) || 'unknown'));
    pushToast('sys', msg);
  };

  const goFromLog = (s, k) => {
    setShowLog(false);
    window.NMS.travelTo(s, k == null ? -1 : k);
  };

  const goFromCaches = (s, k) => {
    setShowCaches(false);
    window.NMS.travelTo(s, k == null ? -1 : k);
  };

  const doClaim = async () => {
    setClaiming(true);
    const r = await window.NMS.claim(wallet);
    setClaiming(false);
    if (r.ok) {
      setClaimedNow(true);
      const amt = r.rewardSol != null ? fmtSol(r.rewardSol, 5) : cachePct() + '% CREATOR REWARDS';
      pushToast('gold', '✓ CACHE CLAIMED — ' + amt + ' sent to ' + shortAddr(wallet));
      if (window.NMS_API) window.NMS_API.fetchRewards();
    } else {
      const claimReasons = {
        already_claimed: 'Cache already claimed by ' + shortAddr(r.claimedBy),
        pool_empty: 'Reward pool is empty — wait for more trading fees, then try again.',
        vault_empty: 'Reward pool is empty — wait for more trading fees, then try again.',
        payout_not_configured: 'On-chain payouts are not configured on the server yet.',
        payout_failed: 'SOL payout failed — try again in a moment.',
      };
      const msg = claimReasons[r.reason]
        || (r.detail ? 'Claim failed: ' + r.detail : null)
        || ('Claim failed: ' + (r.reason || 'unknown'));
      pushToast('sys', msg);
    }
  };

  const crumb = (target) => {
    const m = window.NMS.getMode();
    if (target === 'galaxy' && (m === 'system')) window.NMS.back();
    else if (target === 'galaxy' && m === 'planet') { window.NMS.back(); setTimeout(() => window.NMS.back(), 100); }
    else if (target === 'system' && m === 'planet') window.NMS.back();
  };

  const isDev = wallet === DEV_WALLET;
  const tokenLabel = tokenSym(rewards, tokenGate);

  if (!GL_READY) return <BootWebGLError />;

  return (
    <React.Fragment>
      {mode === 'boot' && !showInfo && !trailerOn ? (
        <div className="boot-notices hud-compact" aria-live="polite">
          <div className="boot-notices-brand">NO MAN'S <em>SOL</em></div>
          <ActivityFeed items={feed} />
        </div>
      ) : null}
      {mode !== 'boot' && !cineOn ? (
        <TopBar mode={mode} system={system} planet={planet} wallet={wallet} online={online} ticker={ticker}
          visited={Object.keys(log).length} isDev={isDev} tokenLabel={tokenLabel}
          onSearch={(q) => window.NMS.search(q)} onTicker={() => setShowInfo(true)}
          onLog={() => setShowLog(true)} onChat={() => setChatOpen((o) => !o)} chatOpen={chatOpen}
          onCinema={() => window.NMS.startCinematic()}
          onCacheDemo={() => window.NMS.debugCacheDemo()} onDisconnect={disconnect}
          onGuide={() => setShowGuide(true)}
          boosts={boosts} buying={buying} onBuy={doBuy} autoOn={autoOn} onToggleAuto={() => window.NMS.setAutoScan(!autoOn)}
          activityFeed={feed}
          travelersOpen={travelersOpen} onTravelers={setTravelersOpen}
          audioMusic={audioMusic} audioHover={audioHover} audioWarp={audioWarp}
          onMusic={setAudioMusic} onHover={setAudioHover} onWarp={setAudioWarp} />
      ) : null}
      {mode === 'galaxy' && system && !cineOn ? (
        <div className="bl hud-compact" data-guide="dossier">
          <NavCrumb system={system} mode={mode} planet={planet} onCrumb={crumb} />
          <SystemDossier sys={system} onWarp={() => window.NMS.warpToSelected()} />
        </div>
      ) : null}
      {mode === 'system' && !cineOn ? (
        <div className="bl hud-compact" data-guide="dossier">
          <NavCrumb system={system} mode={mode} planet={planet} onCrumb={crumb} />
          <PlanetList sys={system} planets={planets} anomalyK={anomalyK}
            scanned={system && log[system.index] ? log[system.index].scanned : []} onPick={(k) => window.NMS.selectPlanet(k)} />
        </div>
      ) : null}
      {mode === 'planet' && !cineOn ? (
        <div className="bl hud-compact" data-guide="dossier">
          <NavCrumb system={system} mode={mode} planet={planet} onCrumb={crumb} />
          <PlanetDossier planet={planet} anomaly={anomaly || (anomalyK >= 0 && planet && planet.k === anomalyK)}
          claimedHere={system && system.claimed} claimedBy={system && system.claimedBy}
          wallet={wallet} claiming={claiming} claimedNow={claimedNow} rewards={rewards}
          onBack={() => window.NMS.back()} onClaim={doClaim} /></div>
      ) : null}
      {(mode === 'galaxy' || mode === 'system' || mode === 'planet') && !cineOn ? (
        <div className="br hud-compact">
          <Scanner mode={mode} system={system} planet={planet} scanState={scanState} onScan={doScan}
            onSweep={doSweep} hasSweep={boosts.some((b) => b.id === 'sysscan')} claimed={claimed} log={log} rewards={rewards} />
        </div>
      ) : null}
      {(mode === 'galaxy' || mode === 'system' || mode === 'planet') && !cineOn ? (
        <div className="bm hud-compact">
          {mode === 'galaxy' && system ? (
            <div className="nav-hint" data-guide="warphint">
              <kbd className="key-cap key-cap--wide">SPACE</kbd> to warp into galaxy
            </div>
          ) : null}
          {mode === 'system' ? (
            <button type="button" className="nav-hint clickable" onClick={() => window.NMS.back()}>
              <kbd className="key-cap">←</kbd> Back to universe map
            </button>
          ) : null}
          {mode === 'planet' && planet && !scanState.busy ? (
            <div className="nav-hint nav-hint--key-right" data-guide="scanhint">
              DEEP SCAN PLANET <kbd className="key-cap key-cap--wide">SPACE</kbd>
            </div>
          ) : null}
          <RewardCachesMini claimed={claimed} rewards={rewards} apiOnline={apiOnline} onOpen={() => setShowCaches(true)} />
        </div>
      ) : null}
      {chatOpen && mode !== 'boot' && !cineOn ? (
        <ChatPanel msgs={chatMsgs} wallet={wallet} onClose={() => setChatOpen(false)}
          onWarn={(t) => pushToast('warn', t)} />
      ) : null}
      {cineOn && !trailerOn ? (
        <CinematicOverlay online={online} claimed={claimed} ticker={ticker} feed={feed} scan={cineScan}
          caption={cineCaption} tokenLabel={tokenLabel} onExit={() => window.NMS.stopCinematic()} />
      ) : null}
      {trailerOn ? (
        <TrailerOverlay card={trailerCard} fade={trailerFade} onSkip={() => window.NMS.skipTrailer()} />
      ) : null}
      {mode === 'boot' && !showInfo ? <Boot onEnter={enter} wallet={wallet} onConnect={connect} connecting={connecting} onTrailer={startIntroTrailer} tokenGate={tokenGate} tokenMint={tokenMint} /> : null}
      {mode === 'warping' ? null : null}
      {showInfo && !cineOn ? <RewardsModal onClose={() => setShowInfo(false)} /> : null}
      {showLog && !cineOn ? <LogModal log={log} onClose={() => setShowLog(false)} onGo={goFromLog} /> : null}
      {travelersOpen && !cineOn ? <TravelersModal travelers={travelersList} loading={travelersLoading} online={online} onClose={() => setTravelersOpen(false)} /> : null}
      {showCaches && !cineOn ? <CacheModal rewards={rewards} claimed={claimed} onClose={() => setShowCaches(false)} onGo={goFromCaches} /> : null}
      {showGuide && !cineOn && mode !== 'boot' ? (
        <GuideModal onClose={() => setShowGuide(false)}
          onPlay={(id) => { setShowGuide(false); setTourChapter(id); }} />
      ) : null}
      {tourChapter && !cineOn && mode !== 'boot' ? (
        <GuideTour mode={mode} startChapter={tourChapter} onExit={() => setTourChapter(null)} />
      ) : null}
      <TweaksPanel>
        <TweakSection label="HUD" />
        <TweakSelect label="Theme" value={t.theme} options={Object.keys(THEMES)} onChange={(v) => setTweak('theme', v)} />
        <TweakToggle label="Body labels" value={t.labels} onChange={(v) => setTweak('labels', v)} />
        <TweakSection label="Universe" />
        <TweakSelect label="Nebula" value={t.nebula} options={Object.keys(window.GALAXY.PALETTES)} onChange={(v) => setTweak('nebula', v)} />
        <TweakSlider label="Orbit speed" value={t.orbitSpeed} min={0} max={4} step={0.1} onChange={(v) => setTweak('orbitSpeed', v)} />
        <TweakSlider label="Star glow" value={t.glow} min={0.4} max={2} step={0.05} onChange={(v) => setTweak('glow', v)} />
        <TweakSection label="Audio" />
        <TweakToggle label="Sound" value={t.sound} onChange={(v) => setTweak('sound', v)} />
      </TweaksPanel>
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('hud')).render(<App />);
