/* ============================================================
   SCENE BACKGROUNDS — atmospheric CSS stand-ins (swap for video)
   ============================================================ */
const SCENES = {
  'forest': {
    label:'Forest', sub:'The Verdant Reach',
    layers:`radial-gradient(ellipse at 50% 0%, hsl(90 30% 22% / .5), transparent 60%),
            repeating-linear-gradient(95deg, hsl(120 18% 9%) 0 38px, hsl(120 22% 12%) 38px 46px, hsl(120 14% 7%) 46px 92px),
            linear-gradient(180deg, hsl(120 25% 14%), hsl(140 30% 5%))`,
  },
  'deep-forest': {
    label:'Blackroot', sub:'Where the light dies',
    layers:`radial-gradient(ellipse at 50% 30%, hsl(150 20% 14% / .6), transparent 55%),
            repeating-linear-gradient(90deg, hsl(150 14% 5%) 0 50px, hsl(150 18% 8%) 50px 58px),
            linear-gradient(180deg, hsl(155 22% 8%), hsl(160 30% 3%))`,
  },
  'gate': {
    label:'The Gate', sub:'Ancient stone, sealed',
    layers:`radial-gradient(circle at 50% 65%, hsl(38 80% 45% / .25), transparent 45%),
            repeating-linear-gradient(0deg, hsl(30 8% 12%) 0 60px, hsl(30 10% 16%) 60px 64px),
            linear-gradient(180deg, hsl(30 8% 14%), hsl(24 12% 6%))`,
  },
  'warcamp': {
    label:'War Camp', sub:'Smoke and iron',
    layers:`radial-gradient(ellipse at 50% 80%, hsl(20 90% 45% / .4), transparent 55%),
            radial-gradient(ellipse at 20% 20%, hsl(10 60% 20% / .5), transparent 50%),
            linear-gradient(180deg, hsl(15 30% 12%), hsl(5 40% 5%))`,
  },
  'hall': {
    label:'Great Hall', sub:'Torchlit and waiting',
    layers:`radial-gradient(ellipse at 50% 40%, hsl(38 70% 40% / .3), transparent 60%),
            repeating-linear-gradient(90deg, hsl(28 20% 12%) 0 80px, hsl(28 24% 15%) 80px 88px),
            linear-gradient(180deg, hsl(30 30% 16%), hsl(24 20% 7%))`,
  },
  'house': {
    label:'The House', sub:'A quiet hearth',
    layers:`radial-gradient(ellipse at 60% 60%, hsl(40 60% 38% / .3), transparent 55%),
            linear-gradient(180deg, hsl(34 28% 16%), hsl(28 22% 8%))`,
  },
};
const sceneInfo = (k)=> SCENES[k] || SCENES.forest;

function SceneBg({ scene }){
  const info = sceneInfo(scene);
  const vid = ASSETS.sceneVideo[scene];
  // crossfade: keep the outgoing scene mounted briefly under the incoming one
  const [layers, setLayers] = useState([{ scene, key:0 }]);
  const seq = useRef(0);
  const wrapRef = useRef(null);
  useEffect(()=>{
    if(layers[layers.length-1].scene===scene) return;
    const key = ++seq.current;
    setLayers(ls=> [...ls.slice(-1), { scene, key }]);   // keep only prev + new
    const t = setTimeout(()=> setLayers([{ scene, key }]), 1300);
    return ()=>clearTimeout(t);
  }, [scene]);
  // pointer / tilt parallax
  useEffect(()=>{
    const el = wrapRef.current; if(!el) return;
    let raf=0, tx=0,ty=0,cx=0,cy=0;
    const loop=()=>{ cx+=(tx-cx)*0.06; cy+=(ty-cy)*0.06;
      el.style.setProperty('--sx', cx.toFixed(2)); el.style.setProperty('--sy', cy.toFixed(2));
      if(Math.abs(tx-cx)>0.001||Math.abs(ty-cy)>0.001) raf=requestAnimationFrame(loop); else raf=0; };
    const kick=()=>{ if(!raf) raf=requestAnimationFrame(loop); };
    const move=(e)=>{ tx=(e.clientX/window.innerWidth-0.5)*2; ty=(e.clientY/window.innerHeight-0.5)*2; kick(); };
    window.addEventListener('pointermove',move);
    return ()=>{ window.removeEventListener('pointermove',move); cancelAnimationFrame(raf); };
  },[]);
  return (
    <div ref={wrapRef} className="scene-wrap">
      {layers.map((L,i)=>{
        const li = sceneInfo(L.scene);
        const lv = ASSETS.sceneVideo[L.scene];
        const top = i===layers.length-1;
        return (
          <div key={L.key} className={`scene-bg scene-parallax ${top?'scene-fade-in':'scene-fade-out'}`}
               style={{ background: li.layers, backgroundSize:'cover' }}>
            <BgVideo src={lv} className="scene-video"/>
            <div className="scene-vignette"></div>
          </div>
        );
      })}
      <div className="scene-emberglow"></div>
    </div>
  );
}

/* ============================================================
   PARTY STRIP
   ============================================================ */
function PartyStrip({ players }){
  return (
    <div className="party-strip">
      {players.map(p=>{
        const pct = (p.hp/p.maxHp)*100;
        const low = pct<30;
        const req = (p.requests||[])[0];
        return (
          <div key={p.id} data-healthbar={p.id} className={`party-card liquid-glass ${p.dead?'dead':''}`}>
            <div className="pc-name display">
              <span className="pc-dot"></span>{p.name.split(' ')[0]}
            </div>
            <div className="pc-hp-track">
              <div className="pc-hp-fill" style={{ width:pct+'%',
                background: low?'hsl(var(--damage))':'linear-gradient(90deg,hsl(var(--heal)),hsl(var(--primary)))'}}></div>
            </div>
            <div className="pc-meta">
              <div className="pc-cards" title={`${(p.cards.deck||[]).length} cards left`}>
                <Icon name="cards" size={13}/>
                <span className="pc-card-n">{(p.cards.deck||[]).length}</span>
                {p.cards.locked && <span className="pc-card-lock" title="Awaiting the Keeper">●</span>}
              </div>
              {req && <span className="pc-req" title={req.label}><Icon name={req.icon||'use'} size={16}/></span>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ============================================================
   CENTER MODES
   ============================================================ */
const SHOP_PER_PAGE = 12;
function ShopView({ shop, players }){
  const meta = (shop.shops && shop.shops[shop.type]) || { name:'Shop', sub:'', items:[] };
  const npc = (window.SHOP_NPC && SHOP_NPC[shop.type]) || { name:'The Merchant', welcome:'Welcome, traveler.' };
  const items = meta.items || [];
  const pages = Math.max(1, Math.ceil(items.length / SHOP_PER_PAGE));
  const page = Math.min(shop.page||0, pages-1);
  const slice = items.slice(page*SHOP_PER_PAGE, page*SHOP_PER_PAGE + SHOP_PER_PAGE);

  const bgSpec = (window.ASSETS && ASSETS.shopBg && ASSETS.shopBg[shop.type]) || {};
  const bgVideo = bgSpec.video;
  const bgImg = meta.bg || bgSpec.img;   // moderator upload wins, else the shipped art
  const hasVideo = !!bgVideo;

  /* SEQUENCE — the backdrop OWNS the whole Main screen first.
     image : full bg holds 5s → NPC rises to centre → 4s → NPC drops, panel opens.
     video : the clip plays to its LAST frame (held) → NPC appears → 4s → panel opens.
     A safety timer ALWAYS advances the sequence even if the video errors, is
     blocked from autoplay (common on mobile), or never fires `onEnded`. */
  const [phase, setPhase] = useState('bg');   // 'bg' → 'npc' → 'panel'
  const [vidFailed, setVidFailed] = useState(false);
  const timers = useRef([]);
  const advanced = useRef(false);
  const toNpc = ()=>{
    if(advanced.current) return; advanced.current = true;
    setPhase('npc'); try{audio.play('intel');}catch(_){}
    timers.current.push(setTimeout(()=> setPhase('panel'), 4000));
  };
  useEffect(()=>{
    setPhase('bg'); setVidFailed(false); advanced.current = false;
    timers.current.forEach(clearTimeout); timers.current = [];
    if(!hasVideo){
      timers.current.push(setTimeout(()=>{ advanced.current=true; setPhase('npc'); try{audio.play('intel');}catch(_){} }, 5000));
      timers.current.push(setTimeout(()=> setPhase('panel'), 5000+4000));
    } else {
      // video path is driven by onEnded — but guarantee progress with a hard cap
      timers.current.push(setTimeout(toNpc, 11000));
    }
    return ()=> timers.current.forEach(clearTimeout);
  }, [shop.type]);
  const onVideoEnded = ()=> toNpc();
  const onVideoFail  = ()=>{ setVidFailed(true); timers.current.push(setTimeout(toNpc, 1500)); };

  const showNpc = phase==='npc' || phase==='panel';
  const paneled = phase==='panel';

  return (
    <>
      {/* FULL-SCREEN backdrop — the Main screen's whole background while shopping */}
      {hasVideo && !vidFailed
        ? <BgVideo src={bgVideo} poster={bgSpec.poster} loop={false} onEnded={onVideoEnded} onFail={onVideoFail}
            className={`shop-stage-bg shop-stage-video ${paneled?'dim':''}`}/>
        : <div className={`shop-stage-bg shop-stage-img ${paneled?'dim':''} ${(hasVideo&&vidFailed&&!bgImg)?'shop-stage-fallback':''}`}
            style={bgImg?{backgroundImage:`url(${bgImg})`}:undefined}></div>}
      <div className={`shop-stage-scrim ${paneled?'on':''}`}></div>

      {/* NPC keeper — rises to centre, greets, then drops as the panel opens */}
      {showNpc && (
        <div className={`shop-npc-stage ${paneled?'docked':''}`}>
          <div className="shop-npc">
            <image-slot id={`shop-npc-${shop.type}`} className="shop-npc-art" shape="rounded" radius="18"
              src={(window.ASSETS && ASSETS.shopKeeper && ASSETS.shopKeeper[shop.type]) || undefined}
              fit="contain"
              style={{width:'100%',height:'100%',display:'block'}}
              placeholder={`${npc.name} — drop portrait`}></image-slot>
          </div>
          <div className="shop-welcome liquid-glass">
            <span className="shop-welcome-name display">{npc.name}</span>
            <span className="shop-welcome-line">“{npc.welcome}”</span>
          </div>
        </div>
      )}

      {/* the wares panel — only once the keeper steps aside */}
      {paneled && (
        <div className="d2main-wrap shopcine">
          <div className="d2main-inner shop-panel in">
            <div className="d2main-head">
              <div className="d2main-title">
                <span className="d2main-glyph"><Icon name="buy" size={20}/></span>
                <div>
                  <h2 className="display">{meta.name}</h2>
                  <div className="d2main-sub">{meta.sub} · <span className="d2main-keeper">{npc.name}</span></div>
                </div>
              </div>
              <div className="d2main-coins">
                {players.map(p=>(
                  <div key={p.id} className="d2main-coin">
                    <Icon name="coin" size={14}/>
                    <span className="nm">{p.name.split(' ')[0]}</span>
                    <span className="c">{p.coins}</span>
                  </div>
                ))}
              </div>
            </div>

            {items.length===0
              ? <div className="d2main-empty">This shop has no wares yet — the moderator can stock it in the Shop Forge.</div>
              : <div className="d2main-grid rack-grid">
                  {slice.map(it=>{
                    const oos = it.stock!=null && it.stock<=0;
                    const hung = it.type==='weapon';
                    return (
                    <div key={it.id} className={`d2main-card rack-slot type-${it.type} ${oos?'out':''}`} data-ware={it.id}>
                      <div className="rack-name">{it.name}</div>
                      <div className={`rack-art ${hung?'hung':''}`}>
                        {it.img
                          ? <img className="rack-img" src={it.img} alt="" draggable="false"/>
                          : <span className="rack-glyph"><Icon name={iconFor(it.name)} size={64}/></span>}
                        {it.stock!=null && <span className={`rack-qty ${oos?'out':it.stock<=2?'low':''}`}>{oos?'0':'×'+it.stock}</span>}
                        {oos && <span className="stock-stamp">OUT OF STOCK</span>}
                      </div>
                      <div className="rack-foot">
                        <span className="rack-cost"><Icon name="coin" size={16}/> {it.cost}</span>
                        {(it.reqs&&it.reqs.length>0) && (
                          <span className="rack-reqs">
                            {it.reqs.map((r,i)=> <span key={i} className="rack-req">{r.n} {r.stat}</span>)}
                          </span>
                        )}
                      </div>
                      {it.showDesc && it.desc && <div className="rack-desc">{it.desc}</div>}
                    </div>
                    );
                  })}
                </div>}

            <div className="d2main-foot">
              <span className="d2main-foot-name">{meta.name}</span>
              {pages>1 && (
                <span className="d2main-pages">
                  {Array.from({length:pages}).map((_,i)=>(
                    <span key={i} className={`d2main-dot ${i===page?'on':''}`}></span>
                  ))}
                  <span className="d2main-page-lbl">Page {page+1} / {pages}</span>
                </span>
              )}
              <span className="d2main-hint">Open on every hero's handset — tap to add to cart &amp; request</span>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

/* ---- flying-purchase layer: a bought item arcs from the wares to the buyer's
   health card, which then pulses gold. Measures live rects so it scales. ---- */
/* ---- purchase SHOWCASE (Genshin-grade): bought items gather into a ring at
   center, orbit as a "hero hold" with a glow-ring + sparks, then peel off and
   arc to each buyer's health card, which pulses gold. Driven by WAAPI. ---- */
const SHOWCASE_GLYPH = { weapon:'⚔', magic:'✦', item:'◈' };
function scWait(ms){ return new Promise(r=>setTimeout(r,ms)); }
function scTile(p){
  const el = document.createElement('div'); el.className='showcase-tile'; el._pid=p.playerId;
  if(p.img){ const im=document.createElement('img'); im.src=p.img; im.className='showcase-tile-img'; el.appendChild(im); }
  else { const g=document.createElement('span'); g.className='showcase-tile-glyph'; g.textContent=SHOWCASE_GLYPH[p.type]||'◈'; el.appendChild(g); }
  return el;
}
function scSpark(layer, x, y){
  const s=document.createElement('span'); s.className='spark';
  const a=Math.random()*6.283, d=42+Math.random()*72;
  s.style.left=x+'px'; s.style.top=y+'px';
  s.style.setProperty('--tx',(Math.cos(a)*d)+'px'); s.style.setProperty('--ty',(Math.sin(a)*d)+'px');
  layer.appendChild(s); setTimeout(()=>s.remove(),820);
}
const scAt = (x,y,s)=>`translate(${x}px,${y}px) translate(-50%,-50%) scale(${s})`;
async function runShowcase(purchases, layer){
  if(!layer || !purchases.length) return;
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const cx=innerWidth/2, cy=innerHeight*0.42, R=Math.min(150, innerWidth*0.2);
  const panel=document.querySelector('.shop-panel')||document.querySelector('.d2main-inner');
  const o=panel?panel.getBoundingClientRect():{left:cx-30,top:cy-30,width:60,height:60};
  const ox=o.left+o.width/2, oy=o.top+o.height*0.45;
  const tiles=purchases.slice(0,12).map(p=>{ const el=scTile(p); layer.appendChild(el); return el; });
  const hb=(el)=>document.querySelector(`[data-healthbar="${el._pid}"]`);
  if(reduce){
    await Promise.all(tiles.map(el=>new Promise(res=>{
      const t=hb(el), to=t?t.getBoundingClientRect():{left:cx,top:cy,width:0,height:0};
      el.style.transform=scAt(to.left+to.width/2,to.top+to.height/2,1);
      el.animate([{opacity:1},{opacity:0}],{duration:380,fill:'forwards'}).onfinish=()=>{ if(t){t.classList.add('fx-receive');setTimeout(()=>t.classList.remove('fx-receive'),600);} el.remove(); res(); };
    }))); return;
  }
  const N=tiles.length, ang=tiles.map((_,i)=>(i/N)*2*Math.PI - Math.PI/2);
  // Phase 1 — gather to the ring
  tiles.forEach((el,i)=>{ el.style.transform=scAt(ox,oy,.7);
    el.animate([{transform:scAt(ox,oy,.7),opacity:.35},{transform:scAt(cx+R*Math.cos(ang[i]),cy+R*Math.sin(ang[i]),1.35),opacity:1}],
      {duration:600,easing:'cubic-bezier(.2,.8,.2,1)',fill:'forwards'}); });
  await scWait(600);
  const ring=document.createElement('div'); ring.className='glow-ring';
  ring.style.cssText=`left:${cx}px;top:${cy}px;width:${2*R}px;height:${2*R}px;`;
  layer.appendChild(ring); setTimeout(()=>ring.remove(),900);
  for(let k=0;k<16;k++) scSpark(layer,cx,cy);
  // Phase 2 — orbit (~1.4 turns), the hero hold
  const DUR=1200, TURNS=1.4, t0=performance.now();
  await new Promise(done=>{ (function spin(t){ const k=Math.min((t-t0)/DUR,1), rot=k*TURNS*2*Math.PI;
    tiles.forEach((el,i)=>{ const a=ang[i]+rot; el.style.transform=scAt(cx+R*Math.cos(a),cy+R*Math.sin(a),1.35); });
    k<1?requestAnimationFrame(spin):done(); })(t0); });
  try{ audio.play('coin'); }catch(_){}
  // Phase 3 — distribute to each buyer's health card (staggered arc)
  await Promise.all(tiles.map((el,i)=>new Promise(res=>setTimeout(()=>{
    const t=hb(el), a=ang[i]+TURNS*2*Math.PI, fx=cx+R*Math.cos(a), fy=cy+R*Math.sin(a);
    const to=t?t.getBoundingClientRect():{left:fx,top:fy,width:0,height:0};
    const tx=to.left+to.width/2, ty=to.top+to.height/2, dx=tx-fx, dy=ty-fy;
    el.animate([
      {transform:scAt(fx,fy,1.35),opacity:1,offset:0},
      {transform:scAt(fx+dx*0.5,fy+dy*0.5-80,1),opacity:1,offset:.5},
      {transform:scAt(tx,ty,.28),opacity:.35,offset:1}
    ],{duration:820,easing:'cubic-bezier(.5,.05,.5,1)',fill:'forwards'}).onfinish=()=>{
      el.remove(); if(t){ t.classList.add('fx-receive'); setTimeout(()=>t.classList.remove('fx-receive'),600); scSpark(layer,tx,ty); scSpark(layer,tx,ty); }
      res();
    };
  }, i*120))));
}

function PurchaseFlights(){
  const layerRef = useRef(null);
  const buf = useRef([]); const timer = useRef(null);
  useGameEvents((ev)=>{
    if(ev.type!=='purchase') return;
    (ev.items||[]).forEach(it=> buf.current.push({ name:it.name, type:it.type, img:it.img, playerId:ev.playerId }));
    clearTimeout(timer.current);
    timer.current = setTimeout(()=>{ const batch=buf.current; buf.current=[]; runShowcase(batch, layerRef.current); }, 240);
  });
  return <div className="showcase-layer" ref={layerRef} aria-hidden="true"></div>;
}

/* ---- MAIN-SCREEN CARD MIRROR — every card a hero plays flips into view here,
   back → front, labelled with the player's name, then fades. Listens to the
   global 'card' event so it mirrors plays from anywhere (battle, free scene…). */
function CardPlayMain(){
  const [show, setShow] = useState(null);   // {value, name, key}
  const seq = useRef(0);
  const players = useGame().players;
  const playersRef = useRef(players); playersRef.current = players;
  useGameEvents((ev)=>{
    if(ev.type!=='card') return;
    if(ev.scope && ev.scope!=='public') return;   // private/off card plays don't mirror to Main
    const p = (playersRef.current||[]).find(x=>x.id===ev.playerId);
    const name = p ? p.name.split(' ')[0] : 'A hero';
    const k = ++seq.current;
    setShow({ value:ev.value, name, key:k });
    try{ audio.play(ev.tier==='premium'?'gateOpen':'card'); }catch(_){}
    setTimeout(()=> setShow(cur=> cur && cur.key===k ? null : cur), 2400);
  });
  if(!show) return null;
  const frontSrc = window.cardFrontSrc?cardFrontSrc(show.value):'';
  const backSrc = window.cardBackSrc?cardBackSrc():'';
  return (
    <div className="cardmain-layer" aria-hidden="true">
      <CardMainSparks/>
      <div className="cardmain" key={show.key}>
        <div className="cardmain-bloom"></div>
        <div className="cardmain-ring"></div>
        <CardMainFlip front={frontSrc} back={backSrc}/>
        <div className="cardmain-label"><b>{show.name}</b> played +{show.value}</div>
      </div>
    </div>
  );
}
window.CardPlayMain = CardPlayMain;

/* ---- MAIN-SCREEN DICE — shown when a roll's scope is 'main' ---- */
function DiceMain(){
  const dice = useGame().dice;
  const m = dice && dice.main;
  if(!m) return null;
  const Pip = window.Pip6;
  return (
    <div className="dicemain" key={m.at} aria-hidden="true">
      <div className="dicemain-pair">
        <div className="die">{Pip?<Pip n={m.a}/>:m.a}</div>
        <div className="die">{Pip?<Pip n={m.b}/>:m.b}</div>
      </div>
      <div className="dicemain-label"><b>{m.name}</b> rolled {m.a} + {m.b} = <b>{m.sum}</b></div>
    </div>
  );
}
window.DiceMain = DiceMain;

function CardMainFlip({ front, back }){
  const [up, setUp] = useState(false);
  useEffect(()=>{ const t=setTimeout(()=>setUp(true), 180); return ()=>clearTimeout(t); }, []);
  return (
    <div className={`cardflip ${up?'face-up':''}`} style={{ '--card-front':`url(${front})`, '--card-back':`url(${back})` }}>
      <div className="cardflip-inner">
        <div className="cardflip-face cardflip-back"></div>
        <div className="cardflip-face cardflip-front"></div>
      </div>
    </div>
  );
}
function CardMainSparks(){
  const sparks = React.useMemo(()=> Array.from({length:14}, ()=>{
    const a=Math.random()*6.283, d=60+Math.random()*120;
    return { tx:Math.cos(a)*d, ty:Math.sin(a)*d, delay:Math.random()*200 };
  }), []);
  return <>{sparks.map((s,i)=>(
    <span key={i} className="cardmain-spark" style={{ left:'50%', top:'44%', '--tx':s.tx+'px', '--ty':s.ty+'px', animationDelay:s.delay+'ms' }}></span>
  ))}</>;
}

/* ---- close-out purchase summary — who bought what; host dismisses it ---- */
function ShopSummary({ summary, players }){
  const nameOf = (id)=> (players.find(p=>p.id===id)||{}).name || '';
  return (
    <div className="shop-summary-bg">
      <div className="shop-summary liquid-glass animate-fade-rise">
        <div className="ss-head">
          <span className="ss-eyebrow"><Icon name="buy" size={15}/> {summary.shopName} — ledger</span>
          <h2 className="display">Purchases this visit</h2>
        </div>
        {summary.purchases.length===0
          ? <div className="ss-empty">No coin changed hands.</div>
          : <div className="ss-list no-scrollbar">
              {summary.purchases.map((r,i)=>(
                <div key={i} className="ss-row">
                  <span className="ss-buyer display">{r.name || nameOf(r.playerId)}</span>
                  <span className="ss-items">
                    {r.items.map((it,j)=>(
                      <span key={j} className={`ss-chip type-${it.type}`}><Icon name={iconFor(it.name)} size={14}/> {it.name}</span>
                    ))}
                  </span>
                  <span className="ss-total gold"><Icon name="coin" size={14}/> {r.total}</span>
                </div>
              ))}
            </div>}
        <button className="ss-close" onClick={()=>Game.dismissShopSummary()}>Close the ledger</button>
      </div>
    </div>
  );
}

/* state-driven beat demonstration — plays the sequence exactly once per
   `demoId`, on whichever device mounts this hook (Main screen, and the
   Moderator for feedback). Race-proof: doesn't depend on event timing or
   listener registration, and never double-plays the same demo. Returns the
   note index currently lit (for the Main-screen visuals), or -1. */
function useGateDemo(gate){
  const [demoNote, setDemoNote] = useState(-1);
  const timers = useRef([]);
  const lastId = useRef(0);
  useEffect(()=>{
    if(!gate.demo || !gate.demoId || gate.demoId===lastId.current) return;
    lastId.current = gate.demoId;
    timers.current.forEach(clearTimeout); timers.current = [];
    const seq = gate.seq || [];
    seq.forEach((n,i)=>{
      timers.current.push(setTimeout(()=>{ setDemoNote(n); audio.beatNote(n); }, 700*i));
      timers.current.push(setTimeout(()=>{ setDemoNote(-1); }, 700*i+520));
    });
  }, [gate.demo, gate.demoId]);
  useEffect(()=>()=>timers.current.forEach(clearTimeout), []);
  return demoNote;
}

function GateView({ gate }){
  const slotRef = useRef(null);
  const [imgUrl, setImgUrl] = useState('');
  const demoNote = useGateDemo(gate);              // state-driven demo (audio + visual)
  const [pulse, setPulse] = useState(-1);         // progress position just lit
  const [wrong, setWrong] = useState(false);
  const [wrongN, setWrongN] = useState(0);        // nonce — bumps on every wrong tap so the flash always re-animates
  const [remain, setRemain] = useState(0);        // ms left on the countdown
  const opening = gate.solved;
  const timers = useRef([]);
  const out = gate.replays===0 && !gate.solved;

  useEffect(()=>{
    const id = setInterval(()=>{
      const el = slotRef.current;
      const im = el && el.shadowRoot && el.shadowRoot.querySelector('img');
      const src = im && im.getAttribute('src');
      setImgUrl(prev => (src && src!==prev) ? src : prev);
    }, 600);
    return ()=> clearInterval(id);
  }, []);

  useGameEvents((ev)=>{
    if(ev.type==='gate-wrong'){ audio.play('wrong'); if(!ev.firstBeat){ setWrong(true); setWrongN(k=>k+1); setTimeout(()=>setWrong(false),700); } }
    else if(ev.type==='gate-timeout'){ audio.play('wrong'); setWrong(true); setWrongN(k=>k+1); setTimeout(()=>setWrong(false),700); }
    else if(ev.type==='gate-open'){ audio.play('gateOpen'); }
  });
  // Correct-note tone + pulse are driven by gate.progress (the same state that
  // fills the bar) so on the Main screen the beat is HEARD exactly as it lights
  // up — not ahead of it via the faster event channel.
  const progLen = (gate.progress||[]).length;
  const prevProg = useRef(progLen);
  useEffect(()=>{
    const prev = prevProg.current; prevProg.current = progLen;
    if(gate.demo || gate.solved || progLen<=prev) return;
    const prog = gate.progress || [];
    for(let i=prev;i<progLen;i++){ const note=prog[i]; setTimeout(()=>{ try{ audio.beatNote(note); }catch(_){} }, (i-prev)*80); }
    setPulse(progLen-1); setTimeout(()=>setPulse(-1),460);
  }, [progLen]);
  useEffect(()=>()=>timers.current.forEach(clearTimeout), []);

  // the Main screen is the authority that drives the countdown → timeout
  useEffect(()=>{
    if(!gate.open || gate.solved || gate.demo || !gate.deadline){ setRemain(0); return; }
    const tick = ()=>{
      const left = gate.deadline - Date.now();
      setRemain(Math.max(0, left));
      if(left<=0){ Game.gateTimeout(gate.deadline); }
    };
    tick();
    const id = setInterval(tick, 120);
    return ()=> clearInterval(id);
  }, [gate.open, gate.solved, gate.demo, gate.deadline]);

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

  const doorStyle = imgUrl ? { backgroundImage:`url(${imgUrl})` } : {};
  const crackPct = gate.seq && gate.seq.length ? Math.min(1, (gate.progress.length)/gate.seq.length) : 0;
  return (
    <div className={`gate-stage ${opening?'is-open':''} ${wrong?'is-wrong':''}`}>
      {wrongN>0 && <div key={wrongN} className="gate-wrong-flash"></div>}
      <image-slot ref={slotRef} id="gate-bg" className="gate-img"
        style={{width:'100%',height:'100%'}} fit="cover"
        placeholder="Drop the gate image"></image-slot>
      {!imgUrl && !opening && (
        <div className="gate-drop-hint">⤓ Drag the gate image anywhere here to set the backdrop</div>
      )}

      <div className="gate-portal"></div>
      <div className="gate-door left" style={doorStyle}>
        <div className="gate-runes gate-runes-l" style={{'--crack': crackPct}}>
          {[0,1,2].map(i=> <span key={i} className={`grune ${crackPct*3>i+0.4?'lit':''}`}>{['ᚦ','ᚱ','ᛟ'][i]}</span>)}
        </div>
      </div>
      <div className="gate-door right" style={doorStyle}>
        <div className="gate-runes gate-runes-r" style={{'--crack': crackPct}}>
          {[0,1,2].map(i=> <span key={i} className={`grune ${crackPct*3>i+0.4?'lit':''}`}>{['ᛉ','ᚨ','ᛞ'][i]}</span>)}
        </div>
      </div>
      <div className="gate-crack" style={{'--crack': crackPct}} aria-hidden="true">
        <svg viewBox="0 0 40 200" preserveAspectRatio="none">
          <path d="M20 0 L17 26 L23 44 L16 74 L24 100 L15 128 L22 156 L18 200" className="gate-crack-main"/>
          <path d="M20 40 L8 58 M23 44 L34 60 M16 110 L6 126 M24 100 L36 118 M22 156 L10 172 M18 156 L30 174" className="gate-crack-branch"/>
        </svg>
      </div>
      <div className="gate-seam"></div>
      <div className="gate-burst"></div>
      {opening && <BgVideo src={ASSETS.gateOpenVideo} className="gate-open-video" loop={false}/>}

      <div className="gate-hud">
        {!opening ? (
          <>
            <div className="gate-title display">The Sealed Gate</div>
            <div className="gate-sub">{gate.demo ? 'Listen — the beat plays' : 'Sound the beat in the order you heard, before time runs out'}</div>

            {/* the four notes — the demo lights them in order (colour only) */}
            <div className="gate-notes">
              {NOTE_META.map((nm,i)=>(
                <div key={i} className={`gnote ${demoNote===i?'demo':''}`}
                  style={{'--nc':nm.color, background:`radial-gradient(circle at 50% 38%, ${nm.color}, hsl(20 16% 8%))`,
                    boxShadow: demoNote===i?`0 0 46px ${nm.color}`:undefined}}>
                </div>
              ))}
            </div>

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

            {/* progress: one slot per note (neutral until echoed correctly) */}
            <div className="gate-progress-row">
              {gate.seq.map((n,i)=>{
                const filled = i < gate.progress.length;
                return <div key={i} className={`gprog ${filled?'lit':''} ${pulse===i?'pulse':''}`}
                  style={filled?{'--nc':NOTE_META[n].color, background:NOTE_META[n].color}:undefined}></div>;
              })}
            </div>

            <div className="gate-replays">
              {Array.from({length:gate.maxReplays}).map((_,i)=>(
                <span key={i} className={`flame-pip ${i<gate.replays?'on':''}`}></span>
              ))}
              <span className="gate-replays-lbl">{gate.replays} chances remain</span>
            </div>
            {out && <div className="gate-fail">The seal holds fast — the Keeper must intervene.</div>}
          </>
        ) : (
          <div className="gate-open-text display">THE GATE OPENS</div>
        )}
      </div>
    </div>
  );
}

/* ============================================================
   ACTION SEQUENCE — the battle field (Main screen)
   ============================================================ */
function BattleEnemy({ e, isTurn }){
  const [hit, setHit] = useState(false);
  const dead = e.hp<=0;
  const slug = enemySlug(e.name);
  const hue = NPC_ART[e.name] || `${(slug.length*47)%360} 40% 30%`;
  useGameEvents((ev)=>{
    if(ev.type==='battle-enemy-hit' && ev.id===e.id){ setHit(true); setTimeout(()=>setHit(false),380); }
  });
  const pct = (e.hp/e.maxHp)*100;
  const hasStats = e.stats && Object.keys(e.stats).length>0;
  return (
    <div className={`bf-enemy ${hit?'fx-enemy-hit':''} ${dead?'slain':''} ${isTurn?'acting':''}`}>
      <div className="bf-portrait liquid-glass" style={{background:`radial-gradient(ellipse at 50% 30%, hsl(${hue}), hsl(20 18% 7%))`}}>
        {e.src
          ? <img src={e.src} className="bf-art" alt={e.name}/>
          : <ArtImg src={ASSETS.enemyArt(slug)} className="bf-art" alt={e.name}>
              <Icon name="skull" size={56} style={{color:'hsl(var(--foreground)/.35)'}}/>
            </ArtImg>}
        {isTurn && <div className="bf-turn-ring"></div>}
        {dead && <div className="bf-slain-x">✕</div>}
      </div>
      <div className="bf-name display">{e.name}</div>
      <div className="bf-hp-track">
        <div className="bf-hp-fill" style={{width:pct+'%'}}></div>
      </div>
      <div className="bf-hp-num">{e.hp}/{e.maxHp}</div>
      {e.intel && (
        <div className="bf-intel liquid-glass">
          {hasStats
            ? Object.entries(e.stats).map(([k,v])=>(
                <React.Fragment key={k}><span className="k">{k}</span><span className="v">{v}</span></React.Fragment>
              ))
            : <div className="bf-intel-desc">{e.desc || 'No lore recorded.'}</div>}
        </div>
      )}
    </div>
  );
}

function BattleView({ battle, players }){
  const [shake, setShake] = useState(false);
  useGameEvents((ev)=>{
    if(ev.type==='enemy-strike'){ setShake(true); setTimeout(()=>setShake(false),500); }
  });
  const turn = battle.turn;
  const turnLabel = turn
    ? (turn.kind==='player'
        ? (players.find(p=>p.id===turn.id)?.name.split(' ')[0] || 'A hero') + '’s turn'
        : (battle.enemies.find(e=>e.id===turn.id)?.name || 'Enemy') + '’s turn')
    : null;

  return (
    <div className={`battle-field ${shake?'fx-shake':''}`}>
      <div className="bf-topband">
        <span className="bf-round">Round {battle.round}</span>
        {turnLabel && <span className={`bf-turnlabel ${turn.kind}`}>{turnLabel}</span>}
      </div>

      <div className="bf-line">
        {battle.enemies.length===0 && (
          <div className="bf-empty">The field is clear… for now.</div>
        )}
        {battle.enemies.map(e=>(
          <BattleEnemy key={e.id} e={e} isTurn={turn && turn.kind==='enemy' && turn.id===e.id}/>
        ))}
      </div>

      {battle.banner && (
        <div className={`bf-banner ${battle.banner.kind}`}>{battle.banner.text}</div>
      )}
    </div>
  );
}

const NPC_ART = {
  'Old Lady':'40 30% 30%','Soldier':'210 20% 30%','Orc':'110 25% 22%','Dwarf':'25 40% 28%','Merchant':'280 20% 30%'
};
function NpcView({ npc }){
  const hue = NPC_ART[npc.name] || '38 30% 28%';
  const vid = ASSETS.npcVideo[npc.name];
  return (
    <div className="npc-wrap animate-fade-rise">
      <div className="npc-portrait liquid-glass" style={{ background:`radial-gradient(ellipse at 50% 35%, hsl(${hue} / .9), hsl(24 16% 8%))`}}>
        <BgVideo src={vid} className="npc-video">
          <div style={{textAlign:'center',color:'hsl(var(--foreground)/.5)'}}>
            <div style={{width:120,height:120,borderRadius:'50%',margin:'0 auto 1rem',
              background:`radial-gradient(circle at 40% 35%, hsl(${hue}), hsl(24 16% 6%))`,
              boxShadow:'0 0 50px hsl(38 80% 50% / .2)'}}></div>
            <div className="tag" style={{fontFamily:'monospace'}}>npc portrait / animation</div>
          </div>
        </BgVideo>
      </div>
      <div className="npc-dialogue liquid-glass">
        <div className="nm">{npc.name}</div>
        <div className="ln">{npc.line}</div>
      </div>
    </div>
  );
}

function EnemyView({ enemy }){
  const [hit, setHit] = useState(false);
  const dead = enemy.hp<=0;
  useGameEvents((ev)=>{ if(ev.type==='enemy-hit'){ setHit(true); setTimeout(()=>setHit(false),360);} });
  const pct = (enemy.hp/enemy.maxHp)*100;
  return (
    <div className={`enemy-wrap animate-fade-rise ${hit?'fx-enemy-hit':''} ${dead?'fx-enemy-dead':''}`}>
      <div className="enemy-name display">{enemy.name}</div>
      <div className="enemy-hp-track">
        <div className="enemy-hp-fill" style={{width:pct+'%'}}></div>
        <div className="enemy-hp-num">{enemy.hp} / {enemy.maxHp}</div>
      </div>
      {enemy.intel && (
        <div className="enemy-intel liquid-glass">
          {Object.entries(enemy.stats).map(([k,v])=>(
            <React.Fragment key={k}><span className="k">{k}</span><span className="v">{v}</span></React.Fragment>
          ))}
        </div>
      )}
      {dead && <div className="dmg-text display" style={{marginTop:'2vh',letterSpacing:'.3em'}}>SLAIN</div>}
    </div>
  );
}

function MainIntelCard({ card }){
  return (
    <div className="main-intel liquid-glass">
      <span className="x glass-btn" onClick={()=>Game.dismissMainIntel()}>✕</span>
      <div className="lbl">Intel</div>
      <div className="tx">{card.text}</div>
    </div>
  );
}

function MapView({ map }){
  return (
    <div className="map-view animate-fade-rise">
      <img className="map-view-img" src={map.src} alt={map.name}/>
      <div className="map-view-name display">{map.name}</div>
    </div>
  );
}

/* ============================================================
   ITEM SPOTLIGHT — Dota-2 themed "dex" detail panel on the Main
   screen. The moderator clicks an item in their scroll list and its
   full detail blooms here: big icon, type, cost, stat reqs, lore.
   ============================================================ */
function ItemSpotlight({ item }){
  useEffect(()=>{ try{ audio.play('intel'); }catch(_){} }, [item && item.id]);
  const typeLabel = (window.ITEM_TYPES && ITEM_TYPES[item.type]) ? ITEM_TYPES[item.type].label : 'Item';
  const reqs = item.reqs || [];
  const oos = item.stock!=null && item.stock<=0;
  return (
    <div className="ware-showcase">
      <div className="ware-veil"></div>
      <div className="ware-hero">
        <div className="ware-hero-bloom"></div>
        <div className="ware-hero-ring"></div>
        {item.img
          ? <img className="ware-hero-art showcase-item" src={item.img} alt="" draggable="false"/>
          : <div className="ware-hero-art ware-hero-glyph showcase-item"><Icon name={iconFor(item.name)} size={150}/></div>}
        <div className="ware-sparkles"><span></span><span></span><span></span><span></span></div>
      </div>
      <div className="ware-detail detail-window liquid-glass">
        <div className="ware-detail-type">{typeLabel}</div>
        <h2 className="ware-detail-name display">{item.name}</h2>
        <div className="ware-detail-rule"></div>
        {item.desc && <p className="ware-detail-desc">{item.desc}</p>}
        <div className="ware-detail-reqs">
          <span className="ware-reqs-lbl">Requires</span>
          {reqs.length>0
            ? reqs.map((r,i)=> <span key={i} className="ware-req">{r.n} {r.stat}</span>)
            : <span className="ware-noreq">No requirements</span>}
        </div>
        <div className="ware-detail-foot">
          <span className="ware-price"><Icon name="coin" size={22}/> {item.cost} <i>gold</i></span>
          <span className={`ware-stock ${oos?'out':''}`}>
            {item.stock==null ? 'In stock' : oos ? 'Out of stock' : `${item.stock} in stock`}
          </span>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   LEVEL-UP CELEBRATION (Main screen) — golden burst + reward list
   ============================================================ */
const CARD_UP_LABEL = { '1to2':'+1 card → +2', '2to5':'+2 card → +5', '1to5':'+1 card → +5' };
function MainLevelUp({ lv, players }){
  const p = players.find(x=>x.id===lv.playerId);
  useEffect(()=>{ audio.play('gateOpen'); }, [lv.at]);
  const name = p ? p.name.split(' ')[0] : 'A hero';
  return (
    <div className="levelup-main">
      <Embers count={30}/>
      <div className="levelup-burst"></div>
      <div className="levelup-card animate-fade-rise">
        <div className="lu-eyebrow"><Icon name="star" size={16}/> Level Up</div>
        <div className="lu-name display">{name}</div>
        <div className="lu-level display">reaches <span className="gold">Level {lv.level}</span></div>
        <div className="lu-rewards">
          {lv.coins>0 && <div className="lu-reward"><Icon name="coin" size={20}/> +{lv.coins} gold</div>}
          {lv.card && lv.card!=='none' && <div className="lu-reward"><Icon name="cards" size={20}/> {CARD_UP_LABEL[lv.card]||'Card upgraded'}</div>}
          {(lv.items||[]).map((it,i)=>(
            <div key={i} className="lu-reward"><Icon name={iconFor(it.name)} size={20}/> {it.name}</div>
          ))}
          {!(lv.coins>0) && (!lv.card||lv.card==='none') && !(lv.items||[]).length && <div className="lu-reward muted">A new level of power</div>}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   MAIN SCREEN
   ============================================================ */
function MainScreen(){
  const game = useGame();

  useGameEvents((ev)=>{
    // NPC sound — plays ONLY on the Main screen, when the moderator pushes an NPC here
    if(ev.type==='npc-sound' && ev.sound){
      try{
        if(window.__npcAudio){ window.__npcAudio.pause(); }
        const a = new Audio(ev.sound);
        a.volume = 0.9;
        window.__npcAudio = a;
        const p = a.play(); if(p&&p.catch) p.catch(()=>{});
      }catch(_){}
    }
  });
  // music director follows the strongest active context (Main screen only)
  useEffect(()=>{
    if(!audio.unlocked) return;
    const ctx = game.battle.active ? 'battle'
              : (game.gate.open || game.circuit.open || game.defuser.open || game.lightbender.open || game.alchemy.open) ? 'gate'
              : (game.ritual.active || game.totems.active) ? 'battle'
              : game.constellation.open ? game.scene
              : game.shop.open ? (['mega','scroll','armory_magic'].includes(game.shop.type) ? 'magicshop' : 'shop')
              : game.scene;
    audio.music(ctx);
  }, [game.battle.active, game.gate.open, game.circuit.open, game.defuser.open, game.lightbender.open, game.alchemy.open, game.ritual.active, game.totems.active, game.constellation.open, game.shop.open, game.shop.type, game.scene]);
  useEffect(()=>{
    if(!audio.unlocked) return;
    const ctx = game.battle.active ? 'battle' : (game.gate.open || game.circuit.open || game.defuser.open) ? 'gate'
              : game.shop.open ? (['mega','scroll','armory_magic'].includes(game.shop.type)?'magicshop':'shop') : game.scene;
    audio.music(ctx);
  }, []);

  const info = sceneInfo(game.scene);

  // intel fades from the Main screen on its own after 30 seconds
  useEffect(()=>{
    if(!game.intelCard) return;
    const at = game.intelCard.at || Date.now();
    const left = Math.max(0, 30000 - (Date.now() - at));
    const id = setTimeout(()=> Game.dismissMainIntel(), left);
    return ()=> clearTimeout(id);
  }, [game.intelCard && game.intelCard.at]);

  // A story scene plays full-screen on top of everything else
  if(game.story && game.story.playing){
    const sc = (game.story.scenes||[]).find(s=>s.id===game.story.playing);
    if(sc && sc.src) return <StoryMainView scene={sc}/>;
  }

  // The gate takes over the whole screen when shown
  if(game.gate.open){
    return (
      <div className="main-screen gate-mode">
        <SceneBg scene="gate"/>
        <div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <GateView gate={game.gate}/>
        {game.intelCard && <MainIntelCard card={game.intelCard}/>}
        {game.levelup && <MainLevelUp lv={game.levelup} players={game.players}/>}
      </div>
    );
  }

  // The Circuit Lock (second gate type) takes over the whole screen
  if(game.circuit.open){
    return (
      <div className="main-screen circuit-mode">
        <SceneBg scene="gate"/>
        <div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <CircuitMainView circuit={game.circuit}/>
        {game.intelCard && <MainIntelCard card={game.intelCard}/>}
        {game.levelup && <MainLevelUp lv={game.levelup} players={game.players}/>}
      </div>
    );
  }

  // The Defuser — a comms trap takeover
  if(game.defuser.open){
    return (
      <div className="main-screen defuser-mode">
        <SceneBg scene="gate"/>
        <div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <DefuserMainView defuser={game.defuser}/>
        <SecretMainLayer/>
      </div>
    );
  }

  // Constellation — a calm alignment takeover
  if(game.constellation.open){
    return (
      <div className="main-screen constellation-mode">
        <PartyStrip players={game.players}/>
        <ConstellationMainView constellation={game.constellation}/>
        <SecretMainLayer/>
      </div>
    );
  }

  // Loot Auction — a sealed-bid takeover
  if(game.auction.open){
    return (
      <div className="main-screen auction-mode">
        <SceneBg scene={game.scene}/>
        <div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <AuctionMainView auction={game.auction}/>
        <SecretMainLayer/>
      </div>
    );
  }

  // Branching Vote — a story-fork takeover
  if(game.vote.open){
    return (
      <div className="main-screen vote-mode">
        <SceneBg scene={game.scene}/>
        <div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <VoteMainView vote={game.vote}/>
        <SecretMainLayer/>
      </div>
    );
  }

  // ===== PACK #2 TAKEOVERS =====
  if(game.lightbender.open){
    return (
      <div className="main-screen lb-mode">
        <SceneBg scene="gate"/><div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <LightBenderMainView lightbender={game.lightbender}/>
        <SecretMainLayer/>
      </div>
    );
  }
  if(game.ritual.active){
    return (
      <div className="main-screen ritual-mode">
        <SceneBg scene="battle"/><div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <RitualMainView ritual={game.ritual}/>
        <MainEventLayer/>
      </div>
    );
  }
  if(game.totems.active){
    return (
      <div className="main-screen totems-mode">
        <SceneBg scene="battle"/><div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <TotemsMainView totems={game.totems}/>
        <MainEventLayer/>
      </div>
    );
  }
  if(game.wager.open){
    return (
      <div className="main-screen wager-mode">
        <SceneBg scene={game.scene}/><div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <WagerMainView wager={game.wager}/>
        <SecretMainLayer/>
      </div>
    );
  }
  if(game.alchemy.open){
    return (
      <div className="main-screen alch-mode">
        <SceneBg scene={game.scene}/><div className="scene-grad"></div>
        <PartyStrip players={game.players}/>
        <AlchemyMainView alchemy={game.alchemy}/>
        <SecretMainLayer/>
      </div>
    );
  }

  // The action sequence takes over when battle is active
  if(game.battle.active){
    return (
      <div className="main-screen battle-mode">
        <SceneBg scene="battle"/>
        <div className="scene-grad"></div>
        <Embers count={18}/>
        <PartyStrip players={game.players}/>
        <BattleView battle={game.battle} players={game.players}/>
        <div className="scene-name display">Action<span className="sub">The clash of steel</span></div>
        {game.intelCard && <MainIntelCard card={game.intelCard}/>}
        {game.levelup && <MainLevelUp lv={game.levelup} players={game.players}/>}
        <MainEventLayer/>
      </div>
    );
  }

  let center;
  if(game.mainMap) center = <MapView map={game.mainMap}/>;
  else center = (
    <div className="idle-hint animate-fade-rise">
      <div className="big">{sceneInfo(game.scene).label}</div>
      <div className="tag" style={{marginTop:8}}>The party presses on…</div>
    </div>
  );

  // ambient codex portraits sit over the open scene (not during shop/map/codex)
  const figures = game.mainEnemies || [];
  const sceneClear = !game.mainMap && !game.shop.open && !game.mainCodex;
  const showNpc = sceneClear && game.mainNpc;
  const showFigures = sceneClear && figures.length>0;
  const idleCenter = sceneClear && !showNpc && !showFigures;

  return (
    <div className="main-screen">
      <SceneBg scene={game.scene}/>
      <div className="scene-grad"></div>
      <Embers count={22}/>

      <PartyStrip players={game.players}/>

      {game.shop.open && <ShopView shop={game.shop} players={game.players}/>}
      {game.shop.open && game.shopItemSpotlight && <ItemSpotlight item={game.shopItemSpotlight}/>}
      {showNpc && <MainNpcLayer npc={game.mainNpc}/>}
      {showFigures && <MainFiguresLayer figures={figures}/>}
      {!game.shop.open && (!sceneClear || idleCenter) && <div className="main-center">{center}</div>}

      {game.mainCodex && <MainCodexOverlay entry={game.mainCodex}/>}

      {!game.mainMap && !showFigures && <div className="scene-name display">{info.label}<span className="sub">{info.sub}</span></div>}
      {game.intelCard && <MainIntelCard card={game.intelCard}/>}
      {game.levelup && <MainLevelUp lv={game.levelup} players={game.players}/>}

      <PurchaseFlights/>
      {game.shopSummary && <ShopSummary summary={game.shopSummary} players={game.players}/>}
      <SecretMainLayer/>
      <MainEventLayer/>
      {window.MainSoundGate && <window.MainSoundGate game={game}/>}
    </div>
  );
}

window.MainScreen = MainScreen;
window.useGateDemo = useGateDemo;

/* Sound gate — after a reload/reconnect the Main display has no user gesture,
   so the browser blocks audio. Show one tap-to-enable button, then kick music. */
function MainSoundGate({ game }){
  const [armed, setArmed] = React.useState(()=> !!(window.audio && audio.unlocked));
  React.useEffect(()=>{
    if(armed) return;
    const iv = setInterval(()=>{ if(window.audio && audio.unlocked) { setArmed(true); clearInterval(iv); } }, 1000);
    return ()=> clearInterval(iv);
  }, [armed]);
  if(armed) return null;
  const enable = ()=>{
    try{
      audio.unlock(); audio.resume();
      const g = store.getState();
      const ctx = g.battle.active ? 'battle'
        : (g.gate.open || g.circuit.open || g.defuser.open || g.lightbender.open || g.alchemy.open) ? 'gate'
        : (g.ritual.active || g.totems.active) ? 'battle'
        : g.shop.open ? (['mega','scroll','armory_magic'].includes(g.shop.type) ? 'magicshop' : 'shop')
        : g.scene;
      audio.setScene(g.scene);
      audio.replayMusic(ctx);
    }catch(_){}
    setArmed(true);
  };
  return (
    <button className="main-sound-gate" onClick={enable} title="Browsers mute audio until you interact — tap to turn the music back on">
      <span className="msg-ico">🔊</span>
      <span className="msg-txt">Tap to enable sound</span>
    </button>
  );
}
window.MainSoundGate = MainSoundGate;
