/* ============================================================
   app.jsx — router, run state, stage scaler
   ============================================================ */
const { useState, useEffect, useLayoutEffect } = React;
const G = window.GAME;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "control": "free",
  "cardDesign": "token",
  "swingSpeed": 1.7,
  "sizeScale": 1,
  "theme": "abismo",
  "scanlines": true,
  "muted": false,
  "music": true
}/*EDITMODE-END*/;

function useStageScale() {
  const [scale, setScale] = useState(1);
  useLayoutEffect(() => {
    const fit = () => setScale(Math.min(window.innerWidth / 1280, window.innerHeight / 800));
    fit(); window.addEventListener('resize', fit);
    return () => window.removeEventListener('resize', fit);
  }, []);
  return scale;
}

function freshRun(char, tier, season) {
  tier = tier | 0;
  season = (season === 2) ? 2 : 1;                 // TEMPORADA 2 — campaña separada (actos 7+)
  const startAct = G.seasonStartAct(season);
  const am = G.amenazaMods(tier);
  const meta = G.getMeta();
  const has = (m) => meta.includes(m);
  const deck = char.deck.slice();
  if (am.startCurse) deck.push('barnacle');   // Amenaza 5+: empiezas con carta maldita
  if (has('attune')) { const WC = ['sondeoclaro','empujesalino','cortemarea','filtrosimple','burbujatibia']; deck.push(WC[Math.floor(Math.random()*WC.length)]); }   // Sintonía: carta de agua inicial
  const ch = { ...char };
  if (has('reinforced')) ch.maxHp += 8;        // meta: +8 PV máx
  const startRelics = [];
  if (has('vault')) { const r = G.relicChoices(1, [])[0]; if (r) startRelics.push(r.id); }
  const dbgRelics = (() => { try { const s = localStorage.getItem('tuntun_debug_relics'); return s ? s.split(',').map(x => x.trim()).filter(Boolean) : []; } catch (e) { return []; } })();
  const startMods = [];
  if (has('forge')) { const m = G.harpoonModChoices ? (G.harpoonModChoices(1, [])[0]) : null; if (m) startMods.push(m.id || m); }   // Forja: mod de arpón inicial
  if (has('abyssforge')) startMods.push('abyssbarb');   // SORPRESA: Púa Abisal (logro 'destilado')
  if (char.mods) char.mods.forEach(m => { if (!startMods.includes(m)) startMods.push(m); });   // mods de arpón propios del cazador
  const startCons = [];
  if (has('baited')) { const c = G.consumableChoices(1)[0]; if (c) startCons.push(c.id); }
  const dbgCons = (() => { try { const s = localStorage.getItem('tuntun_debug_consumables'); return s ? s.split(',').map(x => x.trim()).filter(Boolean) : []; } catch (e) { return []; } })();
  return {
    char: ch,
    hp: ch.maxHp,
    gold: 40 + (has('coffer') ? 25 : 0),
    metaKeen: has('keen'),
    deck,
    map: G.generateMap(),
    pos: null,
    visited: {},
    floor: 0,                                      // floor relativo a la temporada (acto 7 ≈ acto 1, sin disparar hpScale)
    season,
    // meta boons (vault/baited) + playtest seams (localStorage.tuntun_debug_*)
    relics: [...startRelics, ...dbgRelics],
    consumables: [...startCons, ...dbgCons],
    harpoonMods: [...startMods, ...((() => { try { const s = localStorage.getItem('tuntun_debug_harpoonmods'); return s ? s.split(',').map(x => x.trim()).filter(Boolean) : []; } catch (e) { return []; } })())],
    gadgets: (() => { try { const s = localStorage.getItem('tuntun_debug_gadgets'); return s ? s.split(',').map(x => x.trim()).filter(Boolean) : []; } catch (e) { return []; } })(),
    act: startAct,
    amenaza: tier,
    diffMul: am.mul * (season === 2 ? 1.25 : 1),    // T2 más exigente que la T1 base

    rewardMul: am.reward,
    ascMods: am,
    startedAt: Date.now(),
  };
}

function App() {
  const scale = useStageScale();
  const auth = useAuth();
  useEffect(() => { if (window.AUTH) window.AUTH.init(); }, []);
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [screen, setScreen] = useState('title');
  const [run, setRun] = useState(null);
  const [encounter, setEncounter] = useState(null);
  const [reward, setReward] = useState(null);   // {choices, gold}
  const [wikiOpen, setWikiOpen] = useState(false);   // guía/wiki rápida
  const [reveal, setReveal] = useState(null);   // revelado de botín de evento (cofre/reliquia/etc.)
  const [shop, setShop] = useState(null);
  const [deckOpen, setDeckOpen] = useState(false);
  const [pendingBoss, setPendingBoss] = useState(false);
  const [pendingElite, setPendingElite] = useState(false);
  const [justUnlocked, setJustUnlocked] = useState([]);
  const [metaFrom, setMetaFrom] = useState('title');   // where TESORO ABISAL was opened from (return there)
  const [toasts, setToasts] = useState([]);             // global achievement/unlock toasts (any screen)
  function pushToast(text, color) {
    const id = Math.random().toString(36).slice(2);
    setToasts(ts => [...ts, { id, text, color: color || '#ffd23f' }]);
    setTimeout(() => setToasts(ts => ts.filter(x => x.id !== id)), 3000);
  }
  useEffect(() => { window.pushToast = pushToast; return () => { if (window.pushToast === pushToast) window.pushToast = null; }; }, []);
  // toasts de DESBLOQUEO (abajo-izquierda, con icono): logros, cazadores, bestiario
  const [unlocks, setUnlocks] = useState([]);
  function pushUnlock(kind, title, color, icon) {
    const id = Math.random().toString(36).slice(2);
    setUnlocks(us => [...us, { id, kind, title, color: color || '#ffd23f', icon: icon || 'ic_star' }]);
    setTimeout(() => setUnlocks(us => us.filter(x => x.id !== id)), 4200);
  }
  useEffect(() => { window.pushUnlock = pushUnlock; return () => { if (window.pushUnlock === pushUnlock) window.pushUnlock = null; }; }, []);

  // tweaks
  const control = t.control;
  const cardDesign = t.cardDesign;
  useEffect(() => { if (window.SFX) window.SFX.setMuted(t.muted); }, [t.muted]);
  useEffect(() => { if (window.hideTip) window.hideTip(); }, [screen]);   // kill any lingering hover tooltip on screen change
  useEffect(() => { if (window.MUSIC) window.MUSIC.setEnabled(t.music); }, [t.music]);
  useEffect(() => {
    if (!window.MUSIC) return;
    let track = 'menu';
    if (screen === 'map' || screen === 'reward' || screen === 'rest' || screen === 'event' || screen === 'shop' || screen === 'act2') track = 'map';
    else if (screen === 'combat') track = pendingBoss ? 'boss' : 'combat';
    window.MUSIC.play(track);
  }, [screen, pendingBoss]);

  const update = (patch) => setRun(r => ({ ...r, ...patch }));

  /* ---- mid-run save (local, per user) ---- */
  const runKey = () => { const u = auth.user; const who = (u && (u.id || u.email)) || 'guest'; return 'tuntun_run_' + who; };
  const SAVE_SCREENS = { map:1, reward:1, rest:1, event:1, shop:1, act2:1 };   // board screens = safe to persist
  function clearRunSave() { try { localStorage.removeItem(runKey()); } catch (e) {} }
  // auto-save the run whenever it changes on a board screen (never mid-combat → resume re-fights the node)
  useEffect(() => {
    if (run && SAVE_SCREENS[screen]) { try { localStorage.setItem(runKey(), JSON.stringify({ v: 1, run, screen })); } catch (e) {} }
  }, [run, screen]);
  function continueRun() {
    try { const raw = localStorage.getItem(runKey()); if (!raw) return; const s = JSON.parse(raw);
      if (s && s.run) { setRun(s.run); setScreen('map'); } } catch (e) {}
  }
  const hasSave = (() => { try { return !!localStorage.getItem(runKey()); } catch (e) { return false; } })();

  /* ---- flow ---- */
  function startRun(char, tier, season) { clearRunSave(); setRun(freshRun(char, tier, season)); setScreen('map'); }
  function shopPrice(s) {
    if (s.relic) return 70;
    if (s.consumable) return 25;
    if (s.harpoonMod) return s.harpoonMod.price || 75;
    if (s.gadget) return 35;
    const base = s.it.rarity === 'r' ? 60 : s.it.rarity === 'u' ? 38 : 20;
    return base + (s.it.up || 0) * 18;   // pre-mejora cards cost a premium per +level
  }

  function pickNode(node) {
    const visited = { ...run.visited, [node.id]: true };
    // floor relativo a la temporada → el hpScale no se dispara al empezar una campaña en un acto alto (T2 = acto 7)
    const eff = node.r + (run.act - G.seasonStartAct(run.season || 1)) * 8 + ((run.season || 1) === 2 ? 18 : 0);   // T2 arranca a nivel ~acto 3, no desde 0
    const newRun = { ...run, pos: { r: node.r, c: node.c }, visited, floor: eff };
    setRun(newRun);
    if (node.type === 'battle' || node.type === 'elite' || node.type === 'boss') {
      setEncounter(G.buildEncounter(node.type, eff, newRun.diffMul, run.act, newRun.ascMods));
      setPendingBoss(node.type === 'boss');
      setPendingElite(node.type === 'elite');
      setScreen('combat');
    } else if (node.type === 'rest') setScreen('rest');
    else if (node.type === 'event') setScreen('event');
    else if (node.type === 'shop') {
      const items = G.rewardChoices(2, eff, newRun.char.id).map(it => ({ it }));
      const rel = G.relicChoices(1, newRun.relics)[0];
      if (rel) items.push({ relic: rel });
      const con = G.consumableChoices(1)[0];
      if (con) items.push({ consumable: con });
      const hm = G.harpoonModChoices(1, newRun.harpoonMods)[0];
      if (hm) items.push({ harpoonMod: hm });
      const gd = G.gadgetChoices(1)[0];
      if (gd) items.push({ gadget: gd });
      items.forEach(s => { s.price = shopPrice(s); });
      setShop(items);
      setScreen('shop');
    }
  }

  function combatWin(hp) {
    const hasGill = run.relics.includes('gill');
    const season = run.season || 1;
    const seasonFinal = G.finalActFor(season);
    const meta = window.GAME.getMeta();
    const ach = (id) => { if (window.GAME.unlockAch(id)) { const a = window.GAME.ACH[id]; if (a) pushUnlock('LOGRO DESBLOQUEADO', a.name, '#b06bff', 'ic_star'); } };
    if ((run.deck || []).length >= 14) ach('coleccionista');
    if (Object.keys(window.GAME.META).every(k => meta.includes(k))) ach('arsenal');
    // FINAL boss (last act of the SEASON) → victory
    if (pendingBoss && run.act >= seasonFinal) {
      const before = window.GAME.getUnlocked();
      const tier = run.amenaza || 0;
      let perlas = window.GAME.perlasFor(tier, run.floor);
      if (meta.includes('tithe')) perlas = Math.round(perlas * 1.25);   // Diezmo del Abismo
      window.GAME.recordWin(run.char.id, tier, perlas, season);
      ach('firstwin');
      if (tier >= 3) ach('amenaza3');
      if (tier >= 6) ach('amenaza6');
      if (encounter && encounter.some(e => e.key === 'pearlorgan')) ach('melomano');   // beat the Act VI boss
      if (encounter && encounter.some(e => e.key === 'abyssalembic')) ach('destilado');   // T2 completada → Púa Abisal
      // logros por cazador
      ({ brinco:'corsario', nacar:'tejedora', salmuera:'alquimista' }[run.char.id]) && ach({ brinco:'corsario', nacar:'tejedora', salmuera:'alquimista' }[run.char.id]);
      const wins = window.GAME.loadProfile().wins || {};
      if (['tuntun', 'oxido', 'perla'].every(k => wins[k] > 0)) ach('tridente');
      if (['tuntun','oxido','perla','voltio','coloso','brinco','nacar','salmuera'].filter(k => wins[k] > 0).length >= 6) ach('flota');
      const after = window.GAME.getUnlocked();
      const newChars = after.filter(id => !before.includes(id));
      newChars.forEach(id => { const c = (window.GAME.CHARS.find(x => x.id === id) || {}); pushUnlock('NUEVO CAZADOR', c.name || id, '#2ce8d8', 'ic_anchor'); });
      setJustUnlocked(newChars);
      clearRunSave();
      setRun(r => ({ ...r, hp, perlasEarned: perlas })); setScreen('end-win'); return;
    }
    // act boss → descend to the next act
    if (pendingBoss && run.act < seasonFinal) {
      if (encounter) { if (encounter.some(e => e.key === 'tideleviathan')) ach('mareaviva');
        if (encounter.some(e => e.key === 'crusher')) ach('aplastado');
        if (encounter.some(e => e.key === 'dynamo')) ach('despolarizado'); }
      setRun(r => {
        const char = hasGill ? { ...r.char, maxHp: r.char.maxHp + 5 } : r.char;
        const na = r.act + 1;
        // act-transition heal scales with Amenaza: básica(0)=full · intermedia=+20 · máxima=nada
        const tier = r.amenaza || 0;
        const healAct = tier === 0 ? char.maxHp : (tier >= G.AMENAZA_MAX ? 0 : 20);
        // floor relativo a la temporada → el hpScale no se dispara al entrar en actos altos de una T nueva
        const seasonFloor = (na - G.seasonStartAct(r.season || 1)) * 8 + ((r.season || 1) === 2 ? 18 : 0);
        return { ...r, char, act: na, map: G.generateMap(), pos: null, visited: {}, floor: seasonFloor,
          hp: Math.min(char.maxHp, hp + healAct) };
      });
      setScreen('act2');
      return;
    }
    // normal / elite win
    setRun(r => {
      const char = hasGill ? { ...r.char, maxHp: r.char.maxHp + 5 } : r.char;
      const newHp = hasGill ? Math.min(char.maxHp, hp + 5) : hp;
      return { ...r, char, hp: newHp };
    });
    let gold = Math.round((18 + Math.floor(Math.random()*17)) * (run.rewardMul || 1));
    if (meta.includes('prospector')) gold += 6;   // Prospector Abisal
    if (pendingElite) setReward({ type: 'relic', choices: G.relicChoices(3, run.relics), gold });
    else setReward({ type: 'item', choices: G.rewardChoices(3, run.floor, run.char.id), gold });
    setScreen('reward');
  }
  function combatLose(lossSummary) {
    clearRunSave();
    setRun(r => r ? ({ ...r, lossSummary }) : r);
    setScreen('end-lose');
  }

  function takeReward(it) {
    if (window.SFX) { window.SFX.relic ? (reward.type === 'relic' ? window.SFX.relic() : window.SFX.gold()) : null; }
    setRun(r => {
      if (reward.type === 'relic') return { ...r, relics: [...r.relics, it.id], gold: r.gold + reward.gold };
      const card = it.up ? G.makeCard(it.id, it.up) : it.id;   // preserve pre-mejora level if the offered card was templada
      return { ...r, deck: [...r.deck, card], gold: r.gold + reward.gold };
    });
    setTimeout(() => setScreen('map'), 350);
  }
  function skipReward() {
    setRun(r => ({ ...r, gold: r.gold + reward.gold }));
    setScreen('map');
  }

  function applyEvent(eff) {
    // global side effects (profile / achievements)
    if (eff.unlockAch && G.unlockAch(eff.unlockAch)) { const a = G.ACH[eff.unlockAch]; if (a && window.pushUnlock) window.pushUnlock('LOGRO DESBLOQUEADO', a.name, '#b06bff', 'ic_star'); }
    if (eff.perlas) { const pr = G.loadProfile(); G.saveProfile({ ...pr, perlas: (pr.perlas || 0) + eff.perlas }); if (window.pushToast) window.pushToast('+' + eff.perlas + ' PERLAS', '#b06bff'); }
    // resolver los IDs concretos AHORA (para poder revelarlos) usando el estado actual de la run
    const g = {};
    if (eff.relic) { const rel = G.relicChoices(1, run.relics)[0]; if (rel) g.relic = rel.id; }
    if (eff.harpoonMod) { const m = G.harpoonModChoices(1, run.harpoonMods)[0]; if (m) g.harpoonMod = m.id; }
    if (eff.consumable) { const c = G.consumableChoices(1)[0]; if (c) g.consumable = c.id; }
    if (eff.gadget) { const gd = G.gadgetChoices(1)[0]; if (gd) g.gadget = gd.id; }
    if (eff.card) {
      if (eff.card === 'rare') { const rares = Object.keys(G.ITEMS).filter(k => G.ITEMS[k].rarity === 'r' && !G.ITEMS[k].sig && !G.ITEMS[k].curse && !G.ITEMS[k].req); g.card = rares[Math.floor(Math.random()*rares.length)]; }
      else { const c = G.rewardChoices(1, 5, run.char.id)[0]; g.card = c && c.id; }
    }
    setRun(r => {
      let nr = { ...r };
      if (eff.gold) nr.gold = Math.max(0, nr.gold + eff.gold);
      if (g.harpoonMod) nr.harpoonMods = [...(nr.harpoonMods || []), g.harpoonMod];
      if (g.consumable) nr.consumables = [...(nr.consumables || []), g.consumable];
      if (g.gadget) nr.gadgets = [...(nr.gadgets || []), g.gadget];
      if (eff.hp) nr.hp = Math.max(1, Math.min(nr.char.maxHp, nr.hp + eff.hp));
      if (eff.maxHp) { const mh = Math.max(20, nr.char.maxHp + eff.maxHp); nr.char = { ...nr.char, maxHp: mh }; nr.hp = Math.min(mh, nr.hp); }
      if (g.relic) nr.relics = [...nr.relics, g.relic];
      if (eff.harpoon) nr.char = { ...nr.char, harpoon: eff.harpoon };
      if (eff.curse) nr.deck = [...nr.deck, 'barnacle'];
      if (g.card) nr.deck = [...nr.deck, g.card];
      return nr;
    });
    // revelar el botín tangible; si solo hubo perlas/nada, ir directo al mapa
    const rev = { gold: eff.gold, hp: eff.hp, maxHp: eff.maxHp, curse: eff.curse, chest: !!g.relic, ...g };
    const tangible = g.relic || g.card || g.harpoonMod || g.consumable || g.gadget || eff.curse || eff.gold || eff.hp || eff.maxHp;
    if (tangible) { setReveal(rev); setScreen('reveal'); } else setScreen('map');
  }

  const restart = () => { clearRunSave(); setRun(null); setEncounter(null); setScreen('title'); };

  /* ---- render screen ---- */
  let view = null;
  if (screen === 'title') view = <TitleScreen onStart={() => setScreen('select')} onContinue={hasSave ? continueRun : null} onMeta={() => { setMetaFrom('title'); setScreen('meta'); }} onCodex={() => setScreen('codex')} onLogros={() => setScreen('logros')} />;
  else if (screen === 'meta') view = <MetaScreen onBack={() => setScreen(metaFrom)} />;
  else if (screen === 'codex') view = <CodexScreen onBack={() => setScreen('title')} />;
  else if (screen === 'logros') view = <LogrosScreen onBack={() => setScreen('title')} />;
  else if (screen === 'select') view = <CharacterSelect onChoose={startRun} onMeta={() => { setMetaFrom('select'); setScreen('meta'); }} onBack={() => setScreen('title')} />;
  else if (screen === 'map') view = <MapView run={run} onPick={pickNode} onOpenDeck={() => setDeckOpen(true)} />;
  else if (screen === 'combat') view = (
    <Combat key={run.pos.r + '_' + run.pos.c} run={run} encounter={encounter}
      control={control} cardDesign={cardDesign} swingSpeed={t.swingSpeed} sizeScale={t.sizeScale}
      onWin={combatWin} onLose={combatLose}
      onUseConsumable={(idx) => setRun(r => { const c = r.consumables.slice(); c.splice(idx, 1); return { ...r, consumables: c }; })}
      onUseGadget={(idx) => setRun(r => { const g = (r.gadgets || []).slice(); g.splice(idx, 1); return { ...r, gadgets: g }; })} />
  );
  else if (screen === 'reward') view = (
    <RewardScreen run={run} type={reward.type} choices={reward.choices} gold={reward.gold} cardDesign={cardDesign}
      onTake={takeReward} onSkip={skipReward} />
  );
  else if (screen === 'rest') view = (
    <RestScreen run={run}
      onHeal={(a) => setRun(r => ({ ...r, hp: Math.min(r.char.maxHp, r.hp + a) }))}
      onRemove={(idx) => setRun(r => { const d = r.deck.slice(); d.splice(idx,1); return { ...r, deck: d }; })}
      onUpgrade={(idx) => setRun(r => { const d = r.deck.slice(); d[idx] = G.upgradeCard(d[idx]); return { ...r, deck: d }; })}
      onDone={() => setScreen('map')} />
  );
  else if (screen === 'event') view = <EventScreen run={run} onResolve={applyEvent} />;
  else if (screen === 'reveal') view = <RewardReveal reveal={reveal || {}} onDone={() => setScreen('map')} />;
  else if (screen === 'shop') view = (
    <ShopScreen run={run} stock={shop} cardDesign={cardDesign}
      onBuy={(s) => setRun(r => s.relic
        ? ({ ...r, gold: r.gold - s.price, relics: [...r.relics, s.relic.id] })
        : s.consumable
        ? ({ ...r, gold: r.gold - s.price, consumables: [...(r.consumables || []), s.consumable.id] })
        : s.harpoonMod
        ? ({ ...r, gold: r.gold - s.price, harpoonMods: [...(r.harpoonMods || []), s.harpoonMod.id] })
        : s.gadget
        ? ({ ...r, gold: r.gold - s.price, gadgets: [...(r.gadgets || []), s.gadget.id] })
        : ({ ...r, gold: r.gold - s.price, deck: [...r.deck, s.it.up ? G.makeCard(s.it.id, s.it.up) : s.it.id] }))}
      onRemove={(idx) => setRun(r => { const d = r.deck.slice(); d.splice(idx, 1); return { ...r, deck: d, gold: r.gold - window.SHOP_REMOVE_COST }; })}
      onDuplicate={(idx) => setRun(r => {
        const c = r.deck[idx], copy = (c && typeof c === 'object') ? { ...c } : c;
        return { ...r, deck: [...r.deck, copy], gold: r.gold - window.SHOP_DUP_COST };
      })}
      onLeave={() => setScreen('map')} />
  );
  else if (screen === 'end-win') view = <EndScreen win={true} run={run} unlocked={justUnlocked} onRestart={restart} />;
  else if (screen === 'end-lose') view = <EndScreen win={false} run={run} onRestart={restart} />;
  else if (screen === 'act2') view = <ActIntro act={run.act} onContinue={() => setScreen('map')} />;

  // Auth gate: must sign in (or play as guest) before reaching the game.
  if (window.AUTH) {
    if (!auth.ready) {
      view = <div className="fill col center" style={{ background: '#07131f' }}>
        <div className="pix blink" style={{ fontSize: 12, color: '#2ce8d8' }}>CARGANDO...</div></div>;
    } else if (!auth.authed) {
      view = <LoginScreen onAuthed={() => setScreen('title')} />;
    }
  }

  const themeClass = t.theme && t.theme !== 'abismo' ? ' theme-' + t.theme : '';
  const scanClass = t.scanlines ? '' : ' no-scan';

  return (
    <div className="stage-wrap">
      <div className={'stage scanlines vignette' + themeClass + scanClass} style={{ transform: `scale(${scale})` }}>
        {view}
        {deckOpen && run && (
          <div className="fill" style={{ zIndex: 200 }}>
            <DeckPicker run={run} title={`TU BARAJA · ${run.deck.length} CARTAS`} cardDesign={cardDesign}
              onCancel={() => setDeckOpen(false)} />
          </div>
        )}
      </div>
      {toasts.length > 0 && (
        <div style={{ position:'fixed', top:14, left:'50%', transform:'translateX(-50%)', zIndex:9999,
          display:'flex', flexDirection:'column', gap:8, alignItems:'center', pointerEvents:'none' }}>
          {toasts.map(t2 => (
            <div key={t2.id} className="pop" style={{ background:'rgba(7,19,31,.96)', border:'3px solid '+t2.color,
              boxShadow:'0 4px 0 rgba(0,0,0,.5), 0 0 16px '+t2.color+'66', padding:'8px 16px' }}>
              <span className="pix" style={{ fontSize:11, color:t2.color, textShadow:'2px 2px 0 #000' }}>{t2.text}</span>
            </div>
          ))}
        </div>
      )}
      {/* DESBLOQUEOS — toasts abajo-izquierda con icono (logro / cazador / bestiario) */}
      {unlocks.length > 0 && (
        <div style={{ position:'fixed', bottom:16, left:16, zIndex:9999,
          display:'flex', flexDirection:'column-reverse', gap:10, alignItems:'flex-start', pointerEvents:'none' }}>
          {unlocks.map(u => (
            <div key={u.id} className="unlock-toast" style={{ display:'flex', alignItems:'center', gap:10,
              background:'rgba(7,19,31,.97)', border:'3px solid '+u.color, borderLeft:'8px solid '+u.color,
              boxShadow:'0 4px 0 rgba(0,0,0,.5), 0 0 22px '+u.color+'55', padding:'9px 16px 9px 12px', minWidth:200 }}>
              <Glyph name={u.icon} scale={3.5} color={u.color} />
              <div className="col" style={{ gap:2 }}>
                <span className="pix" style={{ fontSize:7, color:'#9db8c9', letterSpacing:1 }}>{u.kind}</span>
                <span className="pix" style={{ fontSize:11, color:u.color, textShadow:'2px 2px 0 #000' }}>{u.title}</span>
              </div>
            </div>
          ))}
        </div>
      )}
      {/* WIKI — botón fijo arriba-izquierda + modal de guía */}
      {(!window.AUTH || auth.authed) && (
        <div onClick={() => setWikiOpen(true)} title="Guía rápida"
          style={{ position:'fixed', top:8, left:8, zIndex:9998, cursor:'pointer',
            width:26, height:26, borderRadius:6, border:'2px solid #2ce8d8', background:'rgba(7,19,31,.9)',
            display:'flex', alignItems:'center', justifyContent:'center', fontWeight:'bold', color:'#2ce8d8', fontSize:13 }}>?</div>
      )}
      {wikiOpen && <WikiModal onClose={() => setWikiOpen(false)} />}
      <AccountBadge />
      <TooltipHost />
      <TweaksPanel title="Tweaks">
        <TweakSection label="Arpón" />
        <TweakRadio label="Control" value={t.control} options={['pendulum','free','power']}
          onChange={(v) => setTweak('control', v)} />
        <TweakSlider label="Vel. péndulo" value={t.swingSpeed} min={0.8} max={3} step={0.1}
          onChange={(v) => setTweak('swingSpeed', v)} />
        <TweakSlider label="Tamaño objetos" value={t.sizeScale} min={0.7} max={1.5} step={0.05}
          onChange={(v) => setTweak('sizeScale', v)} />
        <TweakSection label="Estilo" />
        <TweakRadio label="Cartas" value={t.cardDesign} options={['token','flat']}
          onChange={(v) => setTweak('cardDesign', v)} />
        <TweakRadio label="Tema" value={t.theme} options={['abismo','neon','magma']}
          onChange={(v) => setTweak('theme', v)} />
        <TweakToggle label="Scanlines CRT" value={t.scanlines}
          onChange={(v) => setTweak('scanlines', v)} />
        <TweakToggle label="Silenciar" value={t.muted}
          onChange={(v) => setTweak('muted', v)} />
        <TweakToggle label="Música" value={t.music}
          onChange={(v) => setTweak('music', v)} />
      </TweaksPanel>
    </div>
  );
}

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