/* ============================================================
   PLAYER SCREEN — mobile-first, immersive hero handset
   ============================================================ */
function HealthBar({ hp, maxHp }){
  const pct = maxHp>0 ? Math.max(0,(hp/maxHp)*100) : 0;
  const low = pct < 30;
  const crit = pct < 15;
  return (
    <div className={`hp-frame liquid-glass ${low?'is-low':''} ${crit?'is-crit':''}`}>
      <div className="hp-track">
        <div className="hp-ticks" aria-hidden="true"></div>
        <div className="hp-fill" style={{ width: pct+'%' }}>
          <span className="hp-fill-sheen" aria-hidden="true"></span>
          <span className="hp-fill-gloss" aria-hidden="true"></span>
          <span className="hp-fill-edge" aria-hidden="true"></span>
        </div>
        <div className="hp-cap"><span className="hp-heart" aria-hidden="true">♥</span>Health</div>
        <div className="hp-num">{hp}<span className="hp-of">/{maxHp}</span></div>
      </div>
    </div>
  );
}

function StatTile({ k, v, delay }){
  const sign = v>0 ? '+'+v : (v===0 ? '0' : v);
  return (
    <div className={`stat-tile liquid-glass animate-fade-rise ${v>0?'has':''}`} style={{animationDelay: delay+'ms'}}>
      <div className="lbl">{k}</div>
      <div className={`val ${v>0?'pos':''}`}>{sign}</div>
    </div>
  );
}

/* ---- inventory helpers: entries are objects {id,name,type,desc,reqs} (legacy strings tolerated) ---- */
function invName(x){ return typeof x==='string' ? x : (x && x.name) || ''; }

function InvTile({ item, onClick }){
  const name = invName(item);
  const reqs = (item && item.reqs) || [];
  const weapon = item && item.type==='weapon';
  const broken = !!(item && item.broken);
  return (
    <button className={`inv-tile liquid-glass glass-btn ${weapon?'weapon':''} ${broken?'broken':''}`} onClick={onClick} title={broken?name+' (broken)':name}>
      <Icon name={iconFor(name)} size={26} style={{color: weapon?'hsl(var(--primary))':'inherit'}}/>
      <span className="inv-nm">{name}</span>
      {broken && <span className="inv-broken">BROKEN</span>}
      {reqs.length>0 && (
        <span className="inv-reqs">
          {reqs.map((r,i)=> <span key={i} className="req-chip sm">{r.n} {r.stat}</span>)}
        </span>
      )}
    </button>
  );
}

/* ---- item detail — description + requirements ---- */
function ItemDetailModal({ item, player, allPlayers, onClose }){
  const [mode, setMode] = useState('detail');   // detail → pick-give → pick-use
  if(!item) return null;
  const reqs = item.reqs || [];
  const meets = (r)=> ((player && player.stats && player.stats[r.stat])||0) >= r.n;
  const typeLabel = (window.ITEM_TYPES && ITEM_TYPES[item.type]) ? ITEM_TYPES[item.type].label : 'Item';
  // an inventory entry has an id and lives in this player's packs; shop "i" previews don't
  const owned = !!item.id && ([...(player.weapons||[]), ...(player.items||[])].some(x=>x.id===item.id));
  const friends = (allPlayers||[]).filter(p=>p.id!==player.id && !p.dead);

  const doUseSelf = ()=>{ Game.requestItemUse(player.id, item, player.id); onClose(); };
  const doUseOn = (tid)=>{ Game.requestItemUse(player.id, item, tid); onClose(); };
  const doGive = (tid)=>{ Game.requestGive(player.id, tid, item.id); onClose(); };

  return (
    <div className="intel-modal-bg" onClick={onClose}>
      <div className="item-modal liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <span className="codex-x glass-btn" style={{position:'absolute',top:14,right:14}} onClick={onClose}>✕</span>
        <div className="im-head">
          <div className={`im-ico ${item.type==='weapon'?'weapon':''}`}><Icon name={iconFor(item.name)} size={34}/></div>
          <div>
            <div className="im-name display">{item.name}</div>
            <div className="im-type">{typeLabel}</div>
          </div>
        </div>

        {mode==='detail' && <>
          {item.desc && <p className="im-desc">{item.desc}</p>}
          <div className="im-reqs-label">Required to use</div>
          {reqs.length>0
            ? <div className="im-reqs">
                {reqs.map((r,i)=>(
                  <span key={i} className={`req-chip lg ${meets(r)?'met':'unmet'}`}>{r.n} {r.stat}{meets(r)?' ✓':''}</span>
                ))}
              </div>
            : <div className="im-noreq muted">No requirements — anyone may use it.</div>}

          {owned && (
            <div className="im-actions">
              <button className="im-act use" onClick={doUseSelf}><Icon name="use" size={16}/> Use</button>
              <button className="im-act" disabled={!friends.length} onClick={()=>setMode('pick-use')}><Icon name="shield" size={16}/> Use on friend</button>
              <button className="im-act" disabled={!friends.length} onClick={()=>setMode('pick-give')}><Icon name="bag" size={16}/> Give to friend</button>
            </div>
          )}
          {owned && !friends.length && <div className="im-noreq muted" style={{marginTop:'.5rem'}}>No other heroes to share with right now.</div>}
          {!owned && <button className="mini-btn" style={{marginTop:'1.1rem'}} onClick={onClose}>Close</button>}
        </>}

        {(mode==='pick-use' || mode==='pick-give') && (
          <div className="im-friends">
            <div className="im-friends-head">
              <button className="im-back glass-btn" onClick={()=>setMode('detail')}>← Back</button>
              <span className="im-friends-title">{mode==='pick-give' ? `Give ${item.name} to…` : `Use ${item.name} on…`}</span>
            </div>
            <div className="im-friend-list">
              {friends.map(f=>(
                <button key={f.id} className="im-friend" onClick={()=> mode==='pick-give' ? doGive(f.id) : doUseOn(f.id)}>
                  <span className="im-friend-sigil"><Icon name={CLASS_ICON[f.cls]||'shield'} size={18}/></span>
                  <span className="im-friend-info">
                    <span className="im-friend-name">{f.name.split(' ')[0]}</span>
                    <span className="im-friend-cls">{f.cls}</span>
                  </span>
                  <span className="im-friend-hp"><Icon name="shield" size={11}/> {f.hp}/{f.maxHp}</span>
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function IntelModal({ player, onClose }){
  return (
    <div className="intel-modal-bg" onClick={onClose}>
      <div className="intel-modal liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:'.6rem'}}>
          <Icon name="eye" size={22} style={{color:'hsl(var(--primary))'}}/>
          <h3 className="gold" style={{letterSpacing:'.14em'}}>INTEL</h3>
        </div>
        {(player.intelMsgs && player.intelMsgs.length) ? (
          player.intelMsgs.map((m,i)=> <div key={i} className="intel-msg">{m}</div>)
        ) : (
          <div className="muted" style={{padding:'1rem 0'}}>No intel yet. The Keeper will send word when there's something to know.</div>
        )}
        <button className="mini-btn" style={{marginTop:'1rem'}} onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

/* ---- intel arrives → a whisper rises into view, with sound ---- */
function IntelToast({ message, onClose, onOpen }){
  useEffect(()=>{
    audio.play('intel');
    haptic(HAPTIC.intel);
    const t = setTimeout(onClose, 8000);
    return ()=>clearTimeout(t);
  }, []);
  if(!message) return null;
  return (
    <div className="intel-toast-bg" onClick={onClose}>
      <div className="intel-toast liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="it-glow"></div>
        <div className="it-head">
          <span className="it-eyebrow"><Icon name="eye" size={14}/> A whisper reaches you</span>
          <span className="it-x glass-btn" onClick={onClose}>✕</span>
        </div>
        <div className="it-body">{message}</div>
        <button className="it-more" onClick={onOpen}>Read all intel →</button>
      </div>
    </div>
  );
}

/* ---- a map/handout reaches the player ---- */
function MapToast({ name, onClose, onOpen }){
  useEffect(()=>{
    audio.play('intel');
    haptic(HAPTIC.intel);
    const t = setTimeout(onClose, 8000);
    return ()=>clearTimeout(t);
  }, []);
  return (
    <div className="intel-toast-bg" onClick={onClose}>
      <div className="intel-toast liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="it-glow"></div>
        <div className="it-head">
          <span className="it-eyebrow"><Icon name="book" size={14}/> A map reaches you</span>
          <span className="it-x glass-btn" onClick={onClose}>✕</span>
        </div>
        <div className="it-body">{name}</div>
        <button className="it-more" onClick={onOpen}>Open maps →</button>
      </div>
    </div>
  );
}

/* ---- the player's map library, with a zoomable lightbox ---- */
function MapsModal({ player, onClose }){
  const [zoom, setZoom] = useState(null);     // map open fullscreen
  const maps = player.maps || [];
  return (
    <div className="intel-modal-bg" onClick={onClose}>
      <div className="maps-modal liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:'.7rem'}}>
          <Icon name="book" size={22} style={{color:'hsl(var(--primary))'}}/>
          <h3 className="gold" style={{letterSpacing:'.14em'}}>MAPS &amp; HANDOUTS</h3>
        </div>
        {maps.length===0 ? (
          <div className="muted" style={{padding:'1rem 0'}}>No maps yet. The Keeper will share what you need to see.</div>
        ) : (
          <div className="mm-grid">
            {maps.map(m=>(
              <button key={m.id} className="mm-thumb" onClick={()=>setZoom(m)}>
                <img src={m.src} alt={m.name}/>
                <span className="mm-name">{m.name}</span>
              </button>
            ))}
          </div>
        )}
        <button className="mini-btn" style={{marginTop:'1rem'}} onClick={onClose}>Close</button>
      </div>
      {zoom && <ImageZoom src={zoom.src} name={zoom.name} onClose={()=>setZoom(null)}/>}
    </div>
  );
}

/* ---- a knowledge / codex entry reaches the player ---- */
function KnowledgeToast({ name, kind, onClose, onOpen }){
  useEffect(()=>{
    audio.play('intel');
    haptic(HAPTIC.intel);
    const t = setTimeout(onClose, 8000);
    return ()=>clearTimeout(t);
  }, []);
  const k = (window.KIND_META && KIND_META[kind]) ? KIND_META[kind].label : 'Codex';
  return (
    <div className="intel-toast-bg" onClick={onClose}>
      <div className="intel-toast liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="it-glow"></div>
        <div className="it-head">
          <span className="it-eyebrow"><Icon name="book" size={14}/> {k} added to your codex</span>
          <span className="it-x glass-btn" onClick={onClose}>✕</span>
        </div>
        <div className="it-body">{name}</div>
        <button className="it-more" onClick={onOpen}>Open codex →</button>
      </div>
    </div>
  );
}

/* ---- the player's codex: Intel · Enemies & Bosses · Knowledge ---- */
function CodexModal({ player, initialTab, onClose }){
  const [tab, setTab] = useState(initialTab || 'intel');
  const knowledge = player.knowledge || [];
  const foes = knowledge.filter(k=> k.kind==='enemy' || k.kind==='boss');
  const lore = knowledge.filter(k=> k.kind==='knowledge');
  const intelMsgs = player.intelMsgs || [];
  const tabs = [
    { id:'intel', label:'Intel', n:intelMsgs.length },
    { id:'foes',  label:'Enemies & Bosses', n:foes.length },
    { id:'lore',  label:'Knowledge', n:lore.length },
  ];
  return (
    <div className="intel-modal-bg" onClick={onClose}>
      <div className="codex-modal liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:'.7rem'}}>
          <Icon name="book" size={22} style={{color:'hsl(var(--primary))'}}/>
          <h3 className="gold" style={{letterSpacing:'.14em'}}>CODEX</h3>
          <span className="codex-x glass-btn" style={{marginLeft:'auto'}} onClick={onClose}>✕</span>
        </div>
        <div className="codex-tabs">
          {tabs.map(t=>(
            <button key={t.id} className={`codex-tab ${tab===t.id?'on':''}`} onClick={()=>setTab(t.id)}>
              {t.label}{t.n>0 && <span className="codex-tab-n">{t.n}</span>}
            </button>
          ))}
        </div>

        <div className="codex-tab-body no-scrollbar">
          {tab==='intel' && (
            intelMsgs.length
              ? intelMsgs.map((m,i)=> <div key={i} className="intel-msg">{m}</div>)
              : <div className="muted codex-empty">No intel yet. The Keeper will send word.</div>
          )}
          {tab==='foes' && (
            foes.length
              ? <div className="codex-grid">{foes.map(e=> <CodexCard key={e.id} entry={e} variant="player"/>)}</div>
              : <div className="muted codex-empty">No foes recorded. Knowledge of enemies appears here.</div>
          )}
          {tab==='lore' && (
            lore.length
              ? <div className="codex-grid">{lore.map(e=> <CodexCard key={e.id} entry={e} variant="player"/>)}</div>
              : <div className="muted codex-empty">Nothing gathered yet. Clues, letters and relics land here.</div>
          )}
        </div>
        <button className="mini-btn" style={{marginTop:'.9rem'}} onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

/* ---- NPC panel — a portrait the Keeper pushes to this player (image only) ---- */
function NpcPanel({ npc, onClose }){
  const [zoom, setZoom] = useState(false);
  if(!npc) return null;
  return (
    <div className="npc-panel-bg" onClick={onClose}>
      <div className="npc-panel animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="npc-panel-glow"></div>
        <div className="npc-panel-frame" onClick={()=> npc.src && setZoom(true)}>
          <ArtImg className="npc-panel-img" src={npc.src} alt={npc.name}>
            <div className="npc-panel-fallback"><Icon name="book" size={44}/></div>
          </ArtImg>
          {npc.src && <span className="npc-panel-hint">⤢ tap to enlarge</span>}
        </div>
        <span className="npc-panel-x glass-btn" onClick={onClose}>✕</span>
      </div>
      {zoom && <ImageZoom src={npc.src} name={npc.name} onClose={()=>setZoom(false)}/>}
    </div>
  );
}

function PlayerBattleBanner({ player }){
  const game = useGame();
  const b = game.battle;
  const [strike, setStrike] = useState(false);
  useGameEvents((ev)=>{
    if(ev.type==='enemy-strike' && ev.playerId===player.id){ setStrike(true); audio.play('strike'); setTimeout(()=>setStrike(false),900); }
    else if(ev.type==='turn' && ev.turn && ev.turn.kind==='player' && ev.turn.id===player.id){ audio.play('turn'); }
  });
  if(!b.active) return null;
  const myTurn = b.turn && b.turn.kind==='player' && b.turn.id===player.id;
  const actedByMe = b.acted===player.id;
  const actingEnemy = b.turn && b.turn.kind==='enemy' ? b.enemies.find(e=>e.id===b.turn.id) : null;
  return (
    <>
      {strike && <div className="pb-strike-flash"></div>}
      <div className={`player-battle-banner ${myTurn?'mine':''}`}>
        <span className="pbb-round">⚔ Round {b.round}</span>
        {myTurn
          ? (actedByMe
              ? <span className="pbb-turn mine">Action played — hold the line</span>
              : <span className="pbb-turn mine">Your turn — play your action</span>)
          : actingEnemy
            ? <span className="pbb-turn enemy">{actingEnemy.name} is acting…</span>
            : <span className="pbb-turn">The battle rages</span>}
      </div>
    </>
  );
}

/* ============================================================
   CARD TILT — Pokémon-TCG-Pocket-style: the card leans toward the
   pointer (or device tilt on a phone), the holo foil parallaxes, and
   a crisp specular glare tracks the light. Sets CSS vars on the el.
   ============================================================ */
function useCardTilt(maxDeg=16){
  const ref = useRef(null);
  useEffect(()=>{
    const el = ref.current; if(!el) return;
    let raf=0;
    let tx=50,ty=50,trx=0,try_=0;            // targets
    let cx=50,cy=50,crx=0,cry=0;             // current (eased)
    const apply=()=>{
      cx+=(tx-cx)*0.16; cy+=(ty-cy)*0.16; crx+=(trx-crx)*0.16; cry+=(try_-cry)*0.16;
      el.style.setProperty('--px', cx.toFixed(1)+'%');
      el.style.setProperty('--py', cy.toFixed(1)+'%');
      el.style.setProperty('--rx', crx.toFixed(2)+'deg');
      el.style.setProperty('--ry', cry.toFixed(2)+'deg');
      if(Math.abs(cx-tx)>0.08||Math.abs(cy-ty)>0.08||Math.abs(crx-trx)>0.05||Math.abs(cry-try_)>0.05){ raf=requestAnimationFrame(apply); }
      else raf=0;
    };
    const kick=()=>{ if(!raf) raf=requestAnimationFrame(apply); };
    const fromXY=(px,py)=>{ const r=el.getBoundingClientRect(); const nx=Math.max(0,Math.min(1,(px-r.left)/r.width)); const ny=Math.max(0,Math.min(1,(py-r.top)/r.height));
      tx=nx*100; ty=ny*100; try_=(nx-0.5)*2*maxDeg; trx=-(ny-0.5)*2*maxDeg; el.style.setProperty('--active','1'); kick(); };
    const onMove=(e)=>{ const t=e.touches?e.touches[0]:e; fromXY(t.clientX,t.clientY); };
    const onLeave=()=>{ tx=50;ty=50;trx=0;try_=0; el.style.setProperty('--active','0'); kick(); };
    const onOrient=(e)=>{ if(e.gamma==null||e.beta==null) return; const g=Math.max(-22,Math.min(22,e.gamma)); const b=Math.max(-22,Math.min(22,e.beta-35));
      try_=(g/22)*maxDeg; trx=-(b/22)*maxDeg; tx=50+(g/22)*42; ty=50+(b/22)*42; el.style.setProperty('--active','1'); kick(); };
    el.addEventListener('pointermove',onMove);
    el.addEventListener('pointerdown',onMove);
    el.addEventListener('pointerleave',onLeave);
    window.addEventListener('deviceorientation',onOrient);
    return ()=>{ el.removeEventListener('pointermove',onMove); el.removeEventListener('pointerdown',onMove); el.removeEventListener('pointerleave',onLeave); window.removeEventListener('deviceorientation',onOrient); cancelAnimationFrame(raf); };
  },[]);
  return ref;
}

function RevealCard({ reveal, onClose, closing }){
  const ref = useCardTilt(16);
  const premium = reveal.tier==='premium';
  const [faceUp, setFaceUp] = useState(false);
  useEffect(()=>{
    if(premium){ try{ audio.play('gateOpen'); }catch(_){} try{ if(navigator.vibrate) navigator.vibrate([10,30,60,30,120]); }catch(_){} }
    const t = setTimeout(()=>setFaceUp(true), 40);   // flip back → front
    return ()=>clearTimeout(t);
  },[]);
  const label = premium?'Premium':reveal.tier==='rare'?'Rare':'Action';
  const frontSrc = (window.cardFrontSrc?cardFrontSrc(reveal.value):'');
  const backSrc = (window.cardBackSrc?cardBackSrc():'');
  return (
    <div className={`card-reveal-fx ${closing?'closing':''}`} onClick={onClose}>
      <div className={`crf-flare ${premium?'premium':''}`} aria-hidden="true"></div>
      <div className="crf-tilt is-open" ref={ref}>
        <div className={`cardflip crf-card revealing ${faceUp?'face-up':''} tier-${reveal.tier}`}
             style={{ '--card-front':`url(${frontSrc})`, '--card-back':`url(${backSrc})` }}>
          <div className="cardflip-inner">
            <div className="cardflip-face cardflip-back"></div>
            <div className="cardflip-face cardflip-front">
              <span className="tcard-glare" aria-hidden="true"></span>
            </div>
          </div>
        </div>
      </div>
      <div className="crf-hint">{premium?'A premium card!':`${label} card`} · tap to dismiss</div>
    </div>
  );
}

/* ============================================================
   ACTION DOCK — the hero's sealed deck. Players see ONLY how many
   cards remain on top; each card's value (+1 / +2 / +5) is hidden
   until played. After playing one card the hero is LOCKED until the
   moderator allows them to play again.
   ============================================================ */
/* ---- 2d6 dice roller — player asks, GM permits, then they roll (mod-gated) ---- */
function DiceRoller({ player }){
  const game = useGame();
  const dice = game.dice || {};
  const isPublic = !!dice.public;
  const last = (dice.results||{})[player.id] || (dice.last && dice.last.playerId===player.id ? dice.last : null);
  const allowed = !!(dice.allow||{})[player.id];   // moderator switched this hero's dice ON
  const [rolling, setRolling] = useState(false);
  const [land, setLand] = useState(false);
  const [face, setFace] = useState([last?last.a:6, last?last.b:6]);
  const [open, setOpen] = useState(false);

  useGameEvents((ev)=>{
    if(ev.type==='dice-allow' && ev.playerId===player.id){ setOpen(true); haptic(HAPTIC.premium); }
  });

  if(!allowed && !last) return null;   // hidden until the GM has ever enabled dice for them

  // ease-out cadence: tumbles visibly slow down before the dice settle (feels physical)
  const CADENCE = [40,42,48,58,72,92,118,150,190,232];
  const roll = ()=>{
    if(rolling || !allowed) return;
    setRolling(true); setLand(false); haptic([10,40,10]);
    let i=0;
    const tick=()=>{
      if(i < CADENCE.length){
        setFace([1+Math.floor(Math.random()*6), 1+Math.floor(Math.random()*6)]);
        setTimeout(tick, CADENCE[i++]);
      } else {
        const r=Game.rollDice(player.id); if(r) setFace([r.a,r.b]);
        setRolling(false); setLand(true); haptic(HAPTIC.premium);
        setTimeout(()=>setLand(false), 800);
      }
    };
    tick();
  };

  return (
    <>
      <button className={`dock-railbtn dice ${allowed?'ready':'idle'}`} onClick={()=>setOpen(true)} title={allowed?'Roll 2d6':'Dice — ask the GM to enable'}>
        <Pip6 n={last?last.a:6} small/>
        <span className="dock-dice-lbl">{allowed?'Roll!':'Dice'}</span>
        {allowed && <span className="dice-ready-dot"></span>}
      </button>
      {open && (
        <div className="dice-modal d2-dice" onClick={()=>!rolling&&setOpen(false)}>
          <div className="dice-stage" onClick={e=>e.stopPropagation()}>
            <div className="dice-hexframe">
              <div className="dice-title">2d6 ROLL</div>
              <div className={`dice-scope-tag ${isPublic?'main':''}`}>
                {isPublic?'◈ Shown on the Main screen':'◈ Private — only you'}
              </div>
              <div className={`dice-pair ${rolling?'rolling':''} ${land?'landed':''}`}>
                <div className="die"><Pip6 n={face[0]}/></div>
                <div className="die"><Pip6 n={face[1]}/></div>
              </div>
              <div className="dice-sum">{rolling
                ? <span className="dice-rolling-txt">rolling…</span>
                : land ? <>You rolled <b>{face[0]+face[1]}</b></>
                : last ? <>Last roll <b>{last.a+last.b}</b>{allowed && <span className="muted"> · tap to roll again</span>}</>
                : allowed ? <span className="muted">tap roll</span>
                : <span className="dice-rolling-txt">awaiting GM</span>}</div>
            </div>
            <div className="dice-actions">
              {allowed
                ? <button className="dice-roll-go" disabled={rolling} onClick={roll}>⚅ ROLL NOW</button>
                : <button className="dice-roll-go disabled" disabled>Dice is off — ask the GM</button>}
              <button className="d2-dice-close" disabled={rolling} onClick={()=>setOpen(false)}>Close</button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}
/* a single d6 face drawn with pips */
function Pip6({ n, small }){
  const layout = {1:[4],2:[0,8],3:[0,4,8],4:[0,2,6,8],5:[0,2,4,6,8],6:[0,2,3,5,6,8]}[n]||[4];
  return (
    <span className={`d6face ${small?'sm':''}`}>
      {Array.from({length:9}).map((_,i)=>(
        <span key={i} className={`d6pip ${layout.includes(i)?'on':''}`}></span>
      ))}
    </span>
  );
}

function ActionDock({ player }){
  const game = useGame();
  const b = game.battle;
  const [reveal, setReveal] = useState(null);   // the card just drawn {value,tier}
  const [closing, setClosing] = useState(false);// true while the reveal animates out
  const revealSeq = useRef(0);
  const playAtRef = useRef(0);                   // when the current reveal was opened
  const deck = player.cards.deck || [];
  const played = player.cards.played || [];
  const count = deck.length;
  const locked = !!player.cards.locked;
  const empty = count<=0;
  const myTurn = b.active && b.turn && b.turn.kind==='player' && b.turn.id===player.id;
  const actedByMe = b.active && b.acted===player.id;
  const canPlay = !player.dead && !locked && !empty;

  // dismiss with a soft fade rather than an abrupt unmount
  const closeReveal = ()=>{
    setClosing(true);
    setTimeout(()=>{ setReveal(null); setClosing(false); }, 280);
  };

  // EVERY card the hero spends — tapped from the deck, played on a battle turn,
  // or surrendered to a Keeper's use-request — surfaces the SAME +1/+2/+5 reveal
  // here, driven by the 'card' event so the animation is identical (and never
  // glitches) no matter where the play came from.
  useGameEvents((ev)=>{
    if(ev.type==='card-allow' && ev.playerId===player.id){
      // a fresh reveal must NOT be wiped by a card-allow that arrives a beat late
      if(Date.now()-playAtRef.current > 2600){ setReveal(null); setClosing(false); }
      return;
    }
    if(ev.type==='card' && ev.playerId===player.id){
      const myId = ++revealSeq.current;
      playAtRef.current = Date.now();
      setClosing(false);
      setReveal({ value:ev.value, tier:ev.tier, _k:myId });
      haptic(ev.tier==='premium' ? HAPTIC.premium : HAPTIC.card);
      setTimeout(()=>setReveal(cur=> cur && cur._k===myId ? null : cur), 2600);
    }
  });

  const play = ()=>{
    if(!canPlay) return;
    Game.playCard(player.id);   // dispatches 'card' → the reveal above fires
  };

  const state = player.dead ? 'dead'
    : empty ? 'empty'
    : locked ? 'locked'
    : (myTurn && !actedByMe) ? 'urgent' : 'ready';
  const top = deck[0];
  const label = player.dead ? 'You have fallen'
    : empty ? 'Your deck is spent'
    : locked ? (played.length ? 'Card played — await the Keeper' : 'Locked — await the Keeper')
    : myTurn && !actedByMe ? 'Your turn — play your card'
    : `Play your +${top.value} card`;
  const sub = player.dead ? '\u2014'
    : empty ? 'Sleep or rest to reshuffle the deck'
    : locked ? 'The Keeper will switch you on when it’s your moment'
    : myTurn && !actedByMe ? 'The Keeper is waiting on you'
    : 'You see the card on top — the rest of the deck stays hidden';
  const lastPlayed = played[0];

  return (
    <div className={`action-dock ${state}`}>
      <div className="ad-inner liquid-glass">
        <div className="dock-rail">
          <GMMessagePlayer player={player} variant="dock"/>
          <DiceRoller player={player}/>
        </div>
        <button className={`deck-stack ${top?'tier-'+top.tier:''} ${canPlay?'live':''} ${locked?'is-locked':''} ${empty?'is-empty':''}`}
                onClick={play} disabled={!canPlay} title={canPlay?'Play your top card':label}
                style={{ '--card-back':`url(${window.cardBackSrc?cardBackSrc():''})`, '--card-front':`url(${(!empty&&window.cardFrontSrc)?cardFrontSrc(top.value):''})` }}>
          {Array.from({length: Math.min(4, Math.max(1,count))}).map((_,i)=>(
            <span key={i} className="deck-back" style={{'--i':i}}></span>
          ))}
          {!empty
            ? <span className="deck-top-art"></span>
            : <span className="deck-top-val display empty">0</span>}
          {!empty && <span className="deck-remain" title={`${count} cards left`}>{count}</span>}
          {locked && <span className="deck-lock"><Icon name="key" size={15}/></span>}
        </button>
        <div className="ad-text">
          <div className="ad-label">{label}</div>
          <div className="ad-sub">{sub}</div>
          <div className="ad-meta">
            <span className="ad-remain"><Icon name="cards" size={13}/> {count} on the deck</span>
            {lastPlayed && <span className={`ad-last tier-${lastPlayed.tier}`}>last drawn +{lastPlayed.value}</span>}
          </div>
        </div>
      </div>

      {reveal && <RevealCard reveal={reveal} closing={closing} onClose={closeReveal}/>}
    </div>
  );
}

/* ---- first-run reveal: a proper riffle shuffle, then the deck is sealed ---- */
function CardRevealIntro({ player }){
  const key = 'torchlit-deckintro-'+player.id;
  const [show, setShow] = useState(()=> localStorage.getItem(key)!=='1');
  const [phase, setPhase] = useState('stack');   // stack → split → riffle → fan → gather
  const cards = useRef(null);
  if(!cards.current){
    const arr=[];
    (window.CARD_TIERS||[]).forEach(t=>{ for(let i=0;i<t.count;i++) arr.push({value:t.value,tier:t.tier}); });
    for(let i=arr.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; }
    cards.current = arr;
  }
  useEffect(()=>{
    if(!show) return;
    const ts = [];
    ts.push(setTimeout(()=>{ setPhase('split'); audio.play('card'); }, 420));
    ts.push(setTimeout(()=>{ setPhase('riffle'); audio.play('card'); }, 1080));
    ts.push(setTimeout(()=>{ setPhase('riffle2'); audio.play('card'); }, 1640));
    ts.push(setTimeout(()=>{ setPhase('fan'); audio.play('card'); }, 2200));
    ts.push(setTimeout(()=>{ setPhase('gather'); audio.play('card'); }, 3600));
    return ()=> ts.forEach(clearTimeout);
  },[show]);
  if(!show) return null;
  const dismiss = ()=>{ localStorage.setItem(key,'1'); setShow(false); };
  const total = cards.current.length;
  const ready = phase==='gather';
  return (
    <div className="deckintro-bg">
      <Embers count={18}/>
      <div className="deckintro animate-fade-rise">
        <div className="di-eyebrow"><Icon name="cards" size={14}/> Your action deck</div>
        <h2 className="display gold">Six cards, shuffled</h2>
        <p className="di-sub">Three <b>+1</b> (blue), two <b>+2</b> (purple), and a single <b>+5</b> (gold) — the premium. They are shuffled and sealed. From here you only see the card on top; the rest of the deck stays hidden until you reach them.</p>
        <div className={`di-fan phase-${phase}`}>
          {cards.current.map((c,i)=>(
            <div key={i} className={`tcard art di-card tier-${c.tier}`} style={{'--n':i, '--total':total, '--side': i%2===0?-1:1, '--ord':Math.floor(i/2), '--card-art':`url(${window.cardFrontSrc?cardFrontSrc(c.value):''})`}}>
              <span className="tcard-holo" aria-hidden="true"></span>
              <span className="tcard-sheen" aria-hidden="true"></span>
              <span className="tcard-corner tl">+{c.value}</span>
              <span className="di-card-val display">+{c.value}</span>
              {c.tier==='premium' && <span className="di-card-tag">✶</span>}
            </div>
          ))}
          {(phase==='gather') && <span className="di-deckcount display">{total}</span>}
        </div>
        <button className="di-go" onClick={dismiss} disabled={!ready}>
          {ready ? 'Take up your deck →' : 'Shuffling…'}
        </button>
      </div>
    </div>
  );
}

/* ---- reshuffle flourish — cards fly back into the deck when the hero rests ---- */
function ReshuffleFx({ player }){
  const [run, setRun] = useState(false);
  const cards = useRef(null);
  useGameEvents((ev)=>{
    if(ev.type==='deck-reset' && ev.playerId===player.id){
      const arr=[];
      (window.CARD_TIERS||[]).forEach(t=>{ for(let i=0;i<t.count;i++) arr.push({value:t.value,tier:t.tier}); });
      for(let i=arr.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; }
      cards.current = arr;
      setRun(true);
      audio.play('card');
      setTimeout(()=>audio.play('card'), 460);
      setTimeout(()=>setRun(false), 1700);
    }
  });
  if(!run || !cards.current) return null;
  const total = cards.current.length;
  return (
    <div className="reshuffle-fx" aria-hidden="true">
      <div className="reshuffle-cap">Deck reshuffled</div>
      <div className="reshuffle-stage">
        {cards.current.map((c,i)=>(
          <div key={i} className={`tcard art rsf-card tier-${c.tier}`} style={{'--n':i,'--total':total, '--card-art':`url(${window.cardFrontSrc?cardFrontSrc(c.value):''})`}}>
            <span className="tcard-holo"></span>
            <span className="di-card-val display">+{c.value}</span>
          </div>
        ))}
        <div className="rsf-deck"><span className="display">{total}</span></div>
      </div>
    </div>
  );
}

function GateNoteOverlay({ player }){
  const game = useGame();
  const g = game.gate;
  const [feedback, setFeedback] = useState('');   // '', 'good', 'bad'
  const [flashN, setFlashN] = useState(0);         // nonce so the flash always re-animates
  const [flashKind, setFlashKind] = useState('');
  const [tapped, setTapped] = useState(-1);
  const [remain, setRemain] = useState(0);         // ms left on the countdown
  const out = g.replays===0 && !g.solved;

  // which of the 4 notes belong to THIS player
  const myIndex = game.players.findIndex(p=>p.id===player.id);
  const assignments = noteAssignments(game.players.length || 1);
  const myNotes = (myIndex>=0 ? assignments[myIndex] : assignments[0]) || [];

  useGameEvents((ev)=>{
    if(ev.type==='gate-note'){ if(ev.playerId===player.id){ setFeedback('good'); setFlashKind('good'); setFlashN(k=>k+1); setTimeout(()=>setFeedback(''),420);} }
    else if(ev.type==='gate-wrong'){ audio.play('wrong'); if(ev.playerId===player.id && !ev.firstBeat){ setFeedback('bad'); setFlashKind('bad'); setFlashN(k=>k+1); haptic(HAPTIC.damage); setTimeout(()=>setFeedback(''),600);} }
    else if(ev.type==='gate-timeout'){ audio.play('wrong'); setFeedback('bad'); setFlashKind('bad'); setFlashN(k=>k+1); setTimeout(()=>setFeedback(''),600); haptic(HAPTIC.damage); }
  });

  // live countdown (display only — the Main screen drives the actual timeout)
  useEffect(()=>{
    if(!g.open || g.solved || g.demo || !g.deadline){ setRemain(0); return; }
    const tick = ()=> setRemain(Math.max(0, g.deadline - Date.now()));
    tick();
    const id = setInterval(tick, 100);
    return ()=> clearInterval(id);
  }, [g.open, g.solved, g.demo, g.deadline]);

  if(!g.open) return null;

  const timerActive = !!g.deadline && !g.demo && !g.solved && !out;
  const timePct = timerActive && g.timeLimit ? Math.max(0, Math.min(100, (remain/g.timeLimit)*100)) : 0;
  const secs = Math.ceil(remain/1000);

  const tap = (n)=>{
    if(g.demo || g.solved || out || player.dead || !timerActive) return;
    setTapped(n); setTimeout(()=>setTapped(-1),180);
    audio.beatNote(n);
    Game.gateInput(n, player.id);
  };

  return (
    <div className={`gatenote-overlay ${feedback}`}>
      {flashN>0 && <div key={flashN} className={`gn-flash ${flashKind}`}></div>}
      <Embers count={16}/>
      <div className="gatenote-inner animate-fade-rise">
        {g.solved ? (
          <>
            <div className="gn-flame open"></div>
            <h2 className="display gold">The gate opens</h2>
            <p className="muted" style={{textTransform:'none'}}>Your beat broke the seal. Move through.</p>
          </>
        ) : (
          <>
            <div className="gn-eyebrow">A sealed gate bars the way</div>
            <h2 className="display">{g.demo ? 'Listen to the beat…' : 'Sound your beat'}</h2>
            <p className="muted gn-hint">
              {g.demo
                ? 'Watch the order on the main screen, then echo it before time runs out.'
                : myNotes.length>1
                  ? `You hold ${myNotes.length} of the four notes. Tap them in order — beat the timer.`
                  : 'You hold one of the four notes. Tap it when the order calls — beat the timer.'}
            </p>

            {/* countdown */}
            {timerActive && (
              <div className="gn-timer">
                <div className="gn-timer-bar"><div className={`gn-timer-fill ${timePct<25?'low':''}`} style={{width:timePct+'%'}}></div></div>
                <span className="gn-timer-num">{secs}s</span>
              </div>
            )}

            <div className={`gn-pads n${myNotes.length}`}>
              {myNotes.map(n=>{
                const nm = NOTE_META[n];
                return (
                  <button key={n} className={`gn-pad glass-btn ${tapped===n?'hit':''}`}
                    style={{'--nc':nm.color, background:`radial-gradient(circle at 50% 38%, ${nm.color}, hsl(20 16% 8%))`}}
                    disabled={g.demo || out || player.dead || !timerActive}
                    onClick={()=>tap(n)}>
                    <span className="gn-pad-ring"></span>
                  </button>
                );
              })}
            </div>

            {/* shared progress — one slot per note in the beat (neutral until entered) */}
            <div className="gn-progress">
              {g.seq.map((n,i)=>{
                const lit = i<g.progress.length;
                return (
                  <span key={i} className={`gn-pip ${lit?'lit':''}`}
                    style={lit?{'--nc':NOTE_META[n].color, background:NOTE_META[n].color}:undefined}></span>
                );
              })}
            </div>
            <div className="gn-replays">
              {Array.from({length:g.maxReplays}).map((_,i)=>(
                <span key={i} className={`flame-pip ${i<g.replays?'on':''}`}></span>
              ))}
              <span className="gn-replays-lbl">{g.replays} chances left</span>
            </div>
            {g.demo && <div className="gn-wait">Watch the main screen…</div>}
            {out && <div className="gn-fail">The seal holds fast. Wait for the Keeper.</div>}
            {player.dead && <div className="gn-fail">You have fallen — you cannot raise your voice.</div>}
          </>
        )}
      </div>
    </div>
  );
}

/* ============================================================
   INVENTORY — two sections: Weapons · Items & Magic. Tap any to
   read its description and the stats required to wield it.
   ============================================================ */
function InventoryView({ player, onItem }){
  const weapons = player.weapons || [];
  const goods = player.items || [];
  if(weapons.length===0 && goods.length===0){
    return <div className="inv-empty muted">Your packs are empty. Visit a shop or earn rewards to fill them.</div>;
  }
  return (
    <div className="inv-wrap" data-invtarget="1">
      <div className="inv-section">
        <div className="inv-head"><Icon name="sword" size={14}/> Weapons <span className="inv-n">{weapons.length}</span></div>
        {weapons.length
          ? <div className="inv-grid">{weapons.map((w)=> <InvTile key={w.id||invName(w)} item={w} onClick={()=>onItem(w)}/>)}</div>
          : <div className="inv-none muted">No weapons yet.</div>}
      </div>
      <div className="inv-section">
        <div className="inv-head"><Icon name="bag" size={14}/> Items &amp; Magic <span className="inv-n">{goods.length}</span></div>
        {goods.length
          ? <div className="inv-grid">{goods.map((it)=> <InvTile key={it.id||invName(it)} item={it} onClick={()=>onItem(it)}/>)}</div>
          : <div className="inv-none muted">No items yet.</div>}
      </div>
    </div>
  );
}

/* ============================================================
   SHOP — open on the player's handset. They browse, add what they
   can afford to a cart, then send a buy request to the moderator.
   ============================================================ */
function ShopModal({ player, shop, onClose, onItem }){
  const meta = (shop.shops && shop.shops[shop.type]) || { name:'Shop', sub:'', items:[] };
  const items = meta.items || [];
  const cart = player.cart || [];
  const cartTotal = cart.reduce((s,c)=>s+(c.cost||0),0);
  const pending = (player.requests||[]).filter(r=>r.kind==='buy');
  const remaining = player.coins - cartTotal;
  const countInCart = (id)=> cart.filter(c=>c.id===id).length;

  return (
    <div className="intel-modal-bg d2-bg" onClick={onClose}>
      <div className="d2shop animate-fade-rise" onClick={e=>e.stopPropagation()}>
        {/* header */}
        <div className="d2shop-head">
          <div className="d2shop-title">
            <span className="d2shop-glyph"><Icon name="buy" size={18}/></span>
            <div>
              <h3 className="display">{meta.name}</h3>
              <div className="d2shop-sub">{meta.sub}</div>
            </div>
          </div>
          <div className="d2shop-purse"><Icon name="coin" size={16}/> <b>{player.coins}</b><span className="d2-gold-lbl">gold</span></div>
          <button className="d2shop-x" onClick={onClose}>✕</button>
        </div>

        {/* item grid (scrolls) */}
        <div className="d2shop-grid no-scrollbar">
          {items.length===0 && <div className="d2shop-empty">This shop is empty.</div>}
          {items.map(it=>{
            const inCart = countInCart(it.id);
            const canUse = meetsReqs(player, it.reqs);
            const afford = it.cost <= remaining;
            const oos = it.stock!=null && it.stock<=0;
            const stockLeft = it.stock==null ? null : Math.max(0, it.stock - inCart);
            const capped = stockLeft!=null && stockLeft<=0;
            const status = oos ? 'Sold out' : !afford ? 'No gold' : capped ? 'Max' : '';
            return (
              <div key={it.id} className={`d2item pcard ${inCart?'incart':''} ${(!afford||oos||capped)?'blocked':''} ${!canUse?'understat':''} ${oos?'soldout':''} type-${it.type}`}>
                {it.showDesc && <button className="d2item-info" onClick={()=>onItem(it)} title="Details">i</button>}
                <button className="d2item-buy pcard-buy" disabled={!afford||oos||capped}
                  onClick={()=>Game.cartAdd(player.id, it)}
                  title={oos?'Sold out':!afford?'Not enough gold':capped?'No more in stock':!canUse?`You don't meet ${reqText(it.reqs)} — you can still buy it`:'Add to cart'}>
                  <span className="pcard-name">{it.name}</span>
                  <span className="pcard-art">
                    {it.img ? <img className="pcard-img" src={it.img} alt="" draggable="false"/> : <Icon name={iconFor(it.name)} size={52}/>}
                    {it.stock!=null && <span className={`pcard-qty ${oos?'out':(stockLeft<=2?'low':'')}`}>{oos?'0 left':stockLeft+' left'}</span>}
                    {inCart>0 && <span className="pcard-incart">{inCart}</span>}
                    {oos && <span className="pcard-stamp">OUT OF STOCK</span>}
                  </span>
                  <span className="pcard-foot">
                    <span className="pcard-cost"><Icon name="coin" size={14}/> {it.cost}</span>
                    {it.reqs&&it.reqs.length>0 && (
                      <span className="pcard-reqs">
                        {it.reqs.map((r,i)=>{ const met=(Number((player.stats||{})[r.stat])||0)>=r.n; return <span key={i} className={`pcard-req ${met?'met':'unmet'}`}>{r.n} {r.stat}</span>; })}
                      </span>
                    )}
                  </span>
                  {status && !oos && <span className={`pcard-status ${(!afford||capped)?'no':'warn'}`}>{status}</span>}
                </button>
              </div>
            );
          })}
        </div>

        {/* stash / cart — pinned, always visible */}
        <div className="d2shop-stash">
          <div className="d2stash-head">
            <span className="d2stash-title"><Icon name="cart" size={14}/> Stash · {cart.length}</span>
            <span className="d2stash-total"><Icon name="coin" size={13}/> {cartTotal} <span className="d2-gold-lbl">· {remaining} left</span></span>
          </div>
          <div className="d2stash-slots no-scrollbar">
            {cart.length===0 && <div className="d2stash-empty">Tap an item to add it. You can only stash what you can afford.</div>}
            {cart.map(c=>(
              <button key={c.uid} className="d2stash-slot" onClick={()=>Game.cartRemove(player.id, c.uid)} title={`${c.name} · tap to remove`}>
                <Icon name={iconFor(c.name)} size={20}/>
                <span className="d2stash-x">✕</span>
              </button>
            ))}
          </div>
          <div className="d2stash-actions">
            {cart.length>0 && <button className="d2-btn ghost" onClick={()=>Game.cartClear(player.id)}>Clear</button>}
            <button className="d2-btn buy" disabled={cart.length===0} onClick={()=>Game.requestBuy(player.id)}>
              Request to buy · {cartTotal}g →
            </button>
          </div>
          {pending.length>0 && (
            <div className="d2shop-pending">
              {pending.map(r=>(
                <div key={r.id} className="d2pending-row">
                  <span className="sp-dot"></span> Awaiting the Keeper · {r.items.length} item{r.items.length>1?'s':''} · {r.total}g
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ---- shop opened → a herald announces it on the handset ---- */
function ShopToast({ name, onClose, onOpen }){
  useEffect(()=>{ audio.play('intel'); haptic(HAPTIC.intel); const t=setTimeout(onClose,9000); return ()=>clearTimeout(t); }, []);
  return (
    <div className="intel-toast-bg" onClick={onClose}>
      <div className="intel-toast liquid-glass animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="it-glow"></div>
        <div className="it-head">
          <span className="it-eyebrow"><Icon name="buy" size={14}/> A shop opens nearby</span>
          <span className="it-x glass-btn" onClick={onClose}>✕</span>
        </div>
        <div className="it-body">{name}</div>
        <button className="it-more" onClick={onOpen}>Browse the wares →</button>
      </div>
    </div>
  );
}

/* ---- level-up celebration on the hero's own handset ---- */
function PlayerLevelUp({ lv, onClose }){
  useEffect(()=>{
    audio.play('revive'); haptic(HAPTIC.premium);
    const t = setTimeout(onClose, 7000); return ()=>clearTimeout(t);
  }, [lv.at]);
  return (
    <div className="plevelup-bg" onClick={onClose}>
      <Embers count={22}/>
      <div className="plevelup animate-fade-rise" onClick={e=>e.stopPropagation()}>
        <div className="plu-burst"></div>
        <div className="plu-eyebrow"><Icon name="star" size={15}/> Level Up</div>
        <div className="plu-level display gold">Level {lv.level}</div>
        <div className="plu-rewards">
          {lv.coins>0 && <div className="plu-reward"><Icon name="coin" size={18}/> +{lv.coins} gold</div>}
          {lv.card && lv.card!=='none' && <div className="plu-reward"><Icon name="cards" size={18}/> {(window.CARD_UP_LABEL_P||{})[lv.card] || 'Card upgraded'}</div>}
          {(lv.items||[]).map((it,i)=>(
            <div key={i} className="plu-reward"><Icon name={iconFor(it.name)} size={18}/> {it.name}</div>
          ))}
        </div>
        <button className="plu-go" onClick={onClose}>Onward →</button>
      </div>
    </div>
  );
}
window.CARD_UP_LABEL_P = { '1to2':'+1 card upgraded to +2', '2to5':'+2 card upgraded to +5', '1to5':'+1 card upgraded to +5' };

/* ---- purchased goods fly into the hero's own inventory (mirrors the Main
   screen's fly-to-healthbar). Driven by the shared 'purchase' event. ---- */
function PlayerPurchaseFlight({ player }){
  const [flights, setFlights] = useState([]);
  const seq = useRef(0);
  useGameEvents((ev)=>{
    if(ev.type!=='purchase' || ev.playerId!==player.id) return;
    const targets = [...document.querySelectorAll('[data-invtarget]')].filter(el=>el.offsetParent!==null);
    const target = targets[0] || document.querySelector('[data-invtarget]');
    const to = target ? target.getBoundingClientRect() : { left:innerWidth/2-30, top:innerHeight*0.7, width:60, height:60 };
    const fromX = innerWidth/2, fromY = innerHeight*0.42;
    (ev.items||[]).forEach((it,i)=>{
      const id = ++seq.current;
      const dx = (to.left+to.width/2) - fromX;
      const dy = (to.top+to.height/2) - fromY;
      setFlights(f=>[...f,{ id, name:it.name, x:fromX, y:fromY, dx, dy, delay:i*150 }]);
      const land = 1000 + i*150;
      if(target){ setTimeout(()=>{ target.classList.add('fx-receive'); setTimeout(()=>target.classList.remove('fx-receive'),600); }, land); }
      setTimeout(()=> setFlights(f=>f.filter(x=>x.id!==id)), land+160);
    });
    try{ haptic(HAPTIC.heal); audio.play('coin'); }catch(_){}
  });
  return (
    <div className="flight-layer" aria-hidden="true">
      {flights.map(f=>(
        <span key={f.id} className="flight-sprite inv"
          style={{ left:f.x, top:f.y, '--dx':f.dx+'px', '--dy':f.dy+'px', animationDelay:f.delay+'ms' }}>
          <Icon name={iconFor(f.name)} size={32}/>
        </span>
      ))}
    </div>
  );
}

/* ---- a use-request the moderator gated behind a card: play one or cancel ---- */
function PendingUsePanel({ player }){
  const awaiting = (player.requests||[]).filter(r=>r.kind==='use' && r.status==='awaiting-card');
  if(!awaiting.length) return null;
  const r = awaiting[0];
  const top = (player.cards.deck||[])[0];
  const canPlay = !player.cards.locked && !!top;
  return (
    <div className="useprompt-bg">
      <div className="useprompt animate-fade-rise">
        <div className="up-eyebrow"><Icon name="cards" size={14}/> The Keeper asks for a card</div>
        <div className="up-label">{r.label}</div>
        <div className="up-sub">Play a card to perform this action — or decline.</div>
        <button className={`up-card tcard tier-${top?top.tier:'common'} ${canPlay?'':'spent'}`}
                disabled={!canPlay} onClick={()=>canPlay && Game.fulfillUseWithCard(player.id, r.id)}>
          <span className="tcard-holo" aria-hidden="true"></span>
          <span className="tcard-glare" aria-hidden="true"></span>
          <span className="up-card-back">{canPlay ? 'TAP TO PLAY' : (player.cards.locked ? 'LOCKED' : 'NO CARDS')}</span>
        </button>
        <button className="up-decline" onClick={()=>Game.declineUse(player.id, r.id)}>Decline action</button>
      </div>
    </div>
  );
}

/* ---- transient feedback for buy / give / use / receive ---- */
function FeedbackToast({ fb, onClose }){
  useEffect(()=>{ const t=setTimeout(onClose, 3400); return ()=>clearTimeout(t); }, [fb.at]);
  return (
    <div className={`fbk fbk-${fb.kind} animate-fade-rise`} onClick={onClose}>
      <span className="fbk-burst" aria-hidden="true"></span>
      <span className="fbk-ico"><Icon name={fb.icon||'use'} size={22}/></span>
      <span className="fbk-text">
        <span className="fbk-title">{fb.title}</span>
        <span className="fbk-sub">{fb.text}</span>
      </span>
    </div>
  );
}

/* ---- stat-point spend window — pops on the hero's phone when the mod grants points ---- */
function StatPointWindow({ player }){
  const pts = player.statPoints||0;
  if(pts<=0) return null;
  const spent = player.spentStat||{};
  return (
    <div className="statspend-bg">
      <Embers count={14}/>
      <div className="statspend animate-fade-rise">
        <div className="ss-eyebrow"><Icon name="star" size={15}/> Level Up — Spend Stat Points</div>
        <div className="ss-pool"><b>{pts}</b> point{pts>1?'s':''} to spend</div>
        <div className="ss-grid no-scrollbar">
          {STAT_KEYS.map(k=>{
            const v = Number((player.stats||{})[k])||0;
            const canAdd = pts>0 && v<5;
            const canSub = (spent[k]||0)>0;
            return (
              <div key={k} className={`ss-row ${(spent[k]||0)>0?'raised':''}`}>
                <div className="ss-row-id">
                  <span className="ss-row-k">{k}</span>
                  <span className="ss-row-name">{(window.STAT_META&&window.STAT_META[k])||''}</span>
                </div>
                <div className="ss-row-ctrl">
                  <button className="ss-btn" disabled={!canSub} onClick={()=>{Game.spendStatPoint(player.id,k,-1); haptic([6]);}}>−</button>
                  <span className="ss-val">{v>0?'+'+v:v}</span>
                  <button className="ss-btn plus" disabled={!canAdd} onClick={()=>{Game.spendStatPoint(player.id,k,1); haptic([8]);}}>+</button>
                </div>
              </div>
            );
          })}
        </div>
        <button className="ss-confirm glass-btn" disabled={pts>0} onClick={()=>{Game.finishStatSpend(player.id); haptic(HAPTIC.premium);}}>
          {pts>0 ? `Spend all ${pts} point${pts>1?'s':''}` : 'Confirm'}
        </button>
      </div>
    </div>
  );
}

function PlayerScreen({ playerId }){
  const game = useGame();
  const player = game.players.find(p=>p.id===playerId) || game.players[0];
  const { rootClass, overlays, dead } = usePlayerEffects(player.id);
  const [coinFx, setCoinFx] = useState(false);
  const [intelOpen, setIntelOpen] = useState(false);
  const [codexTab, setCodexTab] = useState('intel');
  const [toast, setToast] = useState(null);       // newest intel message
  const [mapsOpen, setMapsOpen] = useState(false);
  const [mapToast, setMapToast] = useState(null);  // newest map name
  const [knowToast, setKnowToast] = useState(null); // newest codex entry {name,kind}
  const [npcOpen, setNpcOpen] = useState(false);
  const [shopOpen, setShopOpen] = useState(false);  // shop modal on this handset
  const [shopToast, setShopToast] = useState(null); // shop-opened herald
  const [itemDetail, setItemDetail] = useState(null);
  const [levelToast, setLevelToast] = useState(null);
  const [feedback, setFeedbackToast] = useState(null);  // {kind,icon,title,text}

  useGameEvents((ev)=>{
    // global (no playerId) events first
    if(ev.type==='shop-summon'){
      const s = store.getState().shop;
      setShopToast((s.shops[ev.shopType]||{}).name || 'A shop');
      haptic(HAPTIC.intel);
      return;
    }
    if(ev.playerId!==player.id) return;
    if(ev.type==='coins'){ setCoinFx(true); setTimeout(()=>setCoinFx(false),650); }
    if(ev.type==='intel'){
      const msgs = (store.getState().players.find(p=>p.id===player.id)||{}).intelMsgs || [];
      setToast(msgs[0] || 'A whisper reaches you from the dark…');
    }
    if(ev.type==='map'){ setMapToast(ev.name || 'A map'); }
    if(ev.type==='knowledge'){ setKnowToast({name:ev.name||'New record', kind:ev.kind}); }
    if(ev.type==='npc-push'){ setNpcOpen(true); haptic(HAPTIC.intel); }
    if(ev.type==='levelup'){ const lv = store.getState().levelup; if(lv && lv.playerId===player.id) setLevelToast(lv); }
    if(ev.type==='feedback'){ setFeedbackToast({kind:ev.kind, icon:ev.icon, title:ev.title, text:ev.text, at:Date.now()}); haptic(HAPTIC.heal); }
    if(ev.type==='use-needs-card'){ haptic(HAPTIC.intel); }
  });

  // the shop opens automatically on the player's handset when the moderator
  // opens it, and closes when they close it (the player can still dismiss it
  // manually in between — it only re-opens on the next open).
  useEffect(()=>{
    if(game.shop.open){ if(!player.dead) setShopOpen(true); }
    else setShopOpen(false);
  }, [game.shop.open]);

  const items = player.items || [];
  const weapons = player.weapons || [];
  const hpPct = (player.hp/player.maxHp)*100;
  const lowHp = hpPct < 30;
  const xpNow = player.xp||0;
  const xpMax = (window.xpNeeded? xpNeeded(player.level||1):100);
  const pendingBuys = (player.requests||[]).filter(r=>r.kind==='buy').length;

  return (
    <div className={`player-screen vignette ${lowHp?'is-low':''}`}>
      {/* living ambience that reacts to the hero's state */}
      <div className="pscene-bg" aria-hidden="true"></div>
      <Embers count={16}/>
      <PlayerBattleBanner player={player}/>

      <div className={`player-wrap ${rootClass}`}>
        <div className="player-grid">
          {/* MAIN COLUMN */}
          <div style={{display:'flex',flexDirection:'column',gap:16}}>

            {/* hero header */}
            <div className="hero-head liquid-glass animate-fade-rise">
              <div className="hh-id">
                <div className="hh-sigil"><Icon name="shield" size={20}/></div>
                <div>
                  <div className="pname display">{player.name}</div>
                  <div className="pclass">{player.cls}</div>
                </div>
              </div>
              <div className="hh-right">
                <div className={`coins-pill pill ${coinFx?'fx-coin':''}`}>
                  <Icon name="coin" size={18}/> {player.coins}
                </div>
                <div className="level-pill pill" title={`${xpNow} / ${xpMax} XP to next level`}>
                  <Icon name="star" size={14}/> Lvl {player.level||1}
                </div>
              </div>
            </div>

            {/* xp bar */}
            <div className="xp-frame liquid-glass animate-fade-rise" style={{animationDelay:'30ms'}}>
              <div className="xp-track"><div className="xp-fill" style={{width: Math.min(100, (xpNow/xpMax)*100)+'%'}}></div></div>
              <div className="xp-cap"><span>Experience</span><span className="xp-num">{xpNow} / {xpMax}</span></div>
            </div>

            {/* traits forged at creation */}
            {(player.mastery || player.weakness || player.backstory) && (
              <div className="traits animate-fade-rise" style={{animationDelay:'40ms'}}>
                <div className="trait-chips">
                  {player.mastery && (
                    <div className="trait-chip mastery liquid-glass">
                      <span className="tc-lbl"><Icon name="spark" size={12}/> Mastery</span>
                      <span className="tc-val">{player.mastery}</span>
                    </div>
                  )}
                  {player.weakness && (
                    <div className="trait-chip weakness liquid-glass">
                      <span className="tc-lbl"><Icon name="skull" size={12}/> Weakness</span>
                      <span className="tc-val">{player.weakness}</span>
                    </div>
                  )}
                </div>
                {player.backstory && (
                  <details className="backstory liquid-glass">
                    <summary><Icon name="book" size={13}/> Backstory</summary>
                    <p>{player.backstory}</p>
                  </details>
                )}
              </div>
            )}

            {/* health */}
            <div className="animate-fade-rise" style={{animationDelay:'70ms'}}>
              <HealthBar hp={player.hp} maxHp={player.maxHp}/>
            </div>

            {/* stats */}
            <div className="animate-fade-rise" style={{animationDelay:'120ms'}}>
              <div className="section-label" style={{marginBottom:10}}>Attributes</div>
              <div className="stats-grid">
                {STAT_KEYS.map((k,i)=> <StatTile key={k} k={k} v={player.stats[k]} delay={120+i*25}/>)}
              </div>
            </div>

            {/* shop — appears while the moderator has a shop open */}
            {game.shop.open && (
              <button className="shop-open-btn glass-btn animate-fade-rise" style={{animationDelay:'180ms'}}
                      disabled={dead}
                      onClick={()=>{ setShopOpen(true); setShopToast(null); }}>
                <Icon name="buy" size={18}/> Enter the {(game.shop.shops[game.shop.type]||{}).name||'Shop'}
                {pendingBuys>0 && <span className="intel-count">{pendingBuys}</span>}
              </button>
            )}

            {/* inventory — weapons · items & magic */}
            <div className="animate-fade-rise mobile-equip" style={{animationDelay:'200ms'}}>
              <div className="section-label equip-title">Inventory</div>
              <InventoryView player={player} onItem={setItemDetail}/>
            </div>

            {/* codex — intel, enemies & bosses, knowledge */}
            <button className={`intel-btn liquid-glass glass-btn animate-fade-rise ${player.intel>0?'fx-intel-pulse':''}`}
                    style={{animationDelay:'240ms'}}
                    disabled={dead}
                    onClick={()=>{ setCodexTab('intel'); setIntelOpen(true); setToast(null); setKnowToast(null); Game.clearIntel(player.id); }}>
              <Icon name="book" size={18}/> Codex
              {(player.intel>0 || (player.knowledge||[]).length>0) && <span className="intel-count">{player.intel + (player.knowledge||[]).length}</span>}
            </button>

            {/* npc — a face the Keeper has shown this hero (image only) */}
            {player.npc && (
              <button className="intel-btn liquid-glass glass-btn animate-fade-rise"
                      style={{animationDelay:'250ms'}}
                      onClick={()=>setNpcOpen(true)}>
                <Icon name="eye" size={18}/> NPC
              </button>
            )}

            {/* maps & handouts — appears once the Keeper shares one */}
            {(player.maps||[]).length>0 && (
              <button className="intel-btn liquid-glass glass-btn animate-fade-rise"
                      style={{animationDelay:'260ms'}}
                      onClick={()=>{ setMapsOpen(true); setMapToast(null); }}>
                <Icon name="book" size={18}/> Maps
                <span className="intel-count">{(player.maps||[]).length}</span>
              </button>
            )}

            {/* self-test */}
            <details className="selftest liquid-glass animate-fade-rise" style={{animationDelay:'280ms'}}>
              <summary>Self-test effects (demo)</summary>
              <div className="row">
                <button className="mini-btn" onClick={()=>Game.triggerDamage(player.id,3)}>Damage −3</button>
                <button className="mini-btn" onClick={()=>Game.triggerHeal(player.id,3)}>Heal +3</button>
                <button className="mini-btn" onClick={()=>Game.triggerDeath(player.id)}>Death</button>
                <button className="mini-btn" onClick={()=>Game.triggerRevive(player.id)}>Revive</button>
                <button className="mini-btn" onClick={()=>Game.setCoins(player.id, player.coins+5)}>+5 coins</button>
                <button className="mini-btn" onClick={()=>Game.pushIntel(player.id,'The bridge ahead is trapped — check the third plank.')}>Push intel</button>
              </div>
            </details>
          </div>

          {/* ASIDE — inventory on laptop */}
          <div className="player-aside desktop-equip">
            <div className="panel liquid-glass animate-fade-rise" style={{animationDelay:'160ms'}}>
              <div className="section-label" style={{marginBottom:12}}>Inventory</div>
              <InventoryView player={player} onItem={setItemDetail}/>
            </div>
          </div>
        </div>
        {/* spacer so the fixed action dock never covers content */}
        <div style={{height:'120px'}}></div>
      </div>

      {overlays}
      <GateNoteOverlay player={player}/>
      <CircuitPlayerOverlay player={player}/>
      <DefuserPlayerOverlay player={player}/>
      <ConstellationPlayerOverlay player={player}/>
      <AuctionPlayerOverlay player={player}/>
      <VotePlayerOverlay player={player}/>
      <SecretPlayerCard player={player}/>
      <LightBenderPlayerOverlay player={player}/>
      <WagerPlayerOverlay player={player}/>
      <AlchemyPlayerOverlay player={player}/>
      <RitualPlayerOverlay player={player}/>
      <TotemsPlayerOverlay player={player}/>
      <StoryPlayerOverlay/>
      <PlayerEventLayer player={player}/>
      <ActionDock player={player}/>
      <CardRevealIntro player={player}/>
      <ReshuffleFx player={player}/>
      <PlayerPurchaseFlight player={player}/>
      {toast && <IntelToast message={toast} onClose={()=>setToast(null)} onOpen={()=>{ setToast(null); setCodexTab('intel'); setIntelOpen(true); Game.clearIntel(player.id); }}/>}
      {knowToast && <KnowledgeToast name={knowToast.name} kind={knowToast.kind} onClose={()=>setKnowToast(null)} onOpen={()=>{ setKnowToast(null); setCodexTab(knowToast.kind==='enemy'||knowToast.kind==='boss'?'foes':'lore'); setIntelOpen(true); }}/>}
      {intelOpen && <CodexModal player={player} initialTab={codexTab} onClose={()=>setIntelOpen(false)}/>}
      {mapToast && <MapToast name={mapToast} onClose={()=>setMapToast(null)} onOpen={()=>{ setMapToast(null); setMapsOpen(true); }}/>}
      {mapsOpen && <MapsModal player={player} onClose={()=>setMapsOpen(false)}/>}
      {npcOpen && player.npc && <NpcPanel npc={player.npc} onClose={()=>setNpcOpen(false)}/>}
      {shopToast && <ShopToast name={shopToast} onClose={()=>setShopToast(null)} onOpen={()=>{ setShopToast(null); setShopOpen(true); }}/>}
      {shopOpen && game.shop.open && <ShopModal player={player} shop={game.shop} onClose={()=>setShopOpen(false)} onItem={setItemDetail}/>}
      {itemDetail && <ItemDetailModal item={itemDetail} player={player} allPlayers={game.players} onClose={()=>setItemDetail(null)}/>}
      {levelToast && <PlayerLevelUp lv={levelToast} onClose={()=>setLevelToast(null)}/>}
      <StatPointWindow player={player}/>
      <PendingUsePanel player={player}/>
      {feedback && <FeedbackToast fb={feedback} onClose={()=>setFeedbackToast(null)}/>}
    </div>
  );
}

window.PlayerScreen = PlayerScreen;
window.Pip6 = Pip6;
window.DiceRoller = DiceRoller;
