/* App shell: hash router, nav + search, ticker, footer, and the
   Landing / Books / Book / Chapter / Merch views. */
const { useState, useEffect, useRef } = React;
const D = window.DCC;

/* ---------- global spoiler clearance — one switch for the whole site ----------
   Persisted; every later-book redaction (entry pages) reads this single state
   so a reader sets their safe zone once instead of per entry. */
const Spoil = (function(){
  let on=false; try{ on = localStorage.getItem('dcc-spoilers')==='1'; }catch(e){}
  const subs=new Set();
  return {
    get:()=>on,
    set:v=>{ on=!!v; try{localStorage.setItem('dcc-spoilers', on?'1':'0');}catch(e){} subs.forEach(f=>f()); },
    sub:f=>{ subs.add(f); return ()=>subs.delete(f); }
  };
})();
function useSpoilers(){
  const [v,setV]=useState(Spoil.get());
  useEffect(()=>Spoil.sub(()=>setV(Spoil.get())),[]);
  const set=nv=>Spoil.set(typeof nv==='function'?nv(Spoil.get()):nv);
  return [v,set];
}
window.useSpoilers = useSpoilers;

/* ---------- route <-> hash ---------- */
function encode(r){
  switch(r.view){
    case 'index': return '#/index';
    case 'type': return `#/type/${r.type}`;
    case 'books': return '#/books';
    case 'charmap': return '#/characters';
    case 'book': return `#/book/${r.book}`;
    case 'chapter': return `#/book/${r.book}/ch/${r.ch}`;
    case 'entry': return `#/entry/${r.id}`;
    case 'games': return r.game ? `#/games/${r.game}` : '#/games';
    case 'merch': return '#/merch';
    default: return '#/';
  }
}
function decode(){
  const h = (location.hash||'#/').replace(/^#\/?/,'');
  const p = h.split('/').filter(Boolean);
  if(p[0]==='index') return {view:'index'};
  if(p[0]==='type' && p[1] && D.TYPE_META[p[1]]) return {view:'type', type:p[1]};
  if(p[0]==='books') return {view:'books'};
  if(p[0]==='characters') return {view:'charmap'};
  if(p[0]==='games') return {view:'games', game:(p[1]==='crawl'||p[1]==='class')?p[1]:null};
  if(p[0]==='merch') return {view:'merch'};
  if(p[0]==='entry' && p[1] && D.ENTRIES[p[1]]) return {view:'entry', id:p[1]};
  if(p[0]==='book' && p[1]){
    const book = parseInt(p[1],10)||1;
    if(p[2]==='ch' && p[3]) return {view:'chapter', book, ch:parseInt(p[3],10)};
    return {view:'book', book};
  }
  return {view:'home'};
}

/* ---------- ticker ---------- */
function Ticker(){
  const D = window.DCC;
  const live = D.BOOKS.filter(b=>b.status==='live').map(b=>b.n);
  const liveMax = Math.max(...live);
  const partial = D.BOOKS.find(b=>b.status==='live' && b.mappedChapters < b.chapters && b.n===liveMax);
  const fullN = partial ? live.filter(n=>n!==partial.n) : live;
  const fullLabel = fullN.length>1 ? `Books ${fullN[0]}–${fullN[fullN.length-1]} mapped` : `Book ${fullN[0]} mapped`;
  const idxMsg = partial
    ? `${fullLabel} · Book ${partial.n} in progress (ch. ${partial.mappedChapters} of ${partial.chapters}) — more incoming.`
    : `${fullLabel}. The deeper floors are being catalogued.`;
  const items = [
    ['⚠ BROADCAST:','You are live on the dungeon feed. Smile.'],
    ['INDEX:', idxMsg],
    ['WARNING:','Future-book intel is sealed. Decrypt at your own risk.'],
    ['SPONSOR SLOT:','Unsold. The System is sulking about it.'],
  ];
  const run = items.concat(items);
  return (
    <div className="sysbar"><div className="track">
      {run.map((it,i)=>(<span key={i}><b>{it[0]}</b> {it[1]}</span>))}
    </div></div>
  );
}

/* ---------- nav + search ---------- */
function Nav({route, go}){
  const [q, setQ] = useState('');
  const [open, setOpen] = useState(false);
  const [menu, setMenu] = useState(false);
  const [spoil, setSpoil] = useSpoilers();
  const boxRef = useRef(null);
  const results = q.trim().length ?
    D.allEntries().filter(e=> (e.name+' '+e.sub).toLowerCase().includes(q.trim().toLowerCase())).slice(0,8) : [];
  useEffect(()=>{
    function onDoc(e){ if(boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); }
    document.addEventListener('mousedown', onDoc);
    return ()=>document.removeEventListener('mousedown', onDoc);
  },[]);
  function pick(id){ setQ(''); setOpen(false); setMenu(false); go({view:'entry', id}); }
  const is = v => route.view===v ? 'active' : '';
  const navTo = r => { setMenu(false); go(r); };
  return (
    <nav className="nav"><div className="nav-in">
      <div className="brand" onClick={()=>navTo({view:'home'})}>
        <span className="sigil">▲</span>
        <span className="bt">System AI<small>UNOFFICIAL DCC INDEX</small></span>
      </div>
      <div className="navlinks">
        <a className={is('index')+' hide-sm'} onClick={()=>navTo({view:'index'})}>Index</a>
        <a className={is('books')+' hide-sm'} onClick={()=>navTo({view:'books'})}>Books</a>
        <a className={is('charmap')+' hide-sm'} onClick={()=>navTo({view:'charmap'})}>Characters</a>
        <a className={is('games')+' hide-sm'} onClick={()=>navTo({view:'games', game:null})}>Arcade</a>
        <div className="navsearch" ref={boxRef}>
          <span className="mag-i">⌕</span>
          <input value={q} placeholder="Find any mention…"
            onChange={e=>{setQ(e.target.value); setOpen(true);}}
            onFocus={()=>setOpen(true)}
            onKeyDown={e=>{
              if(e.key==='Enter' && results.length){ pick(results[0].id); e.target.blur(); }
              if(e.key==='Escape'){ setQ(''); setOpen(false); e.target.blur(); }
            }}/>
          {open && q.trim().length>0 && (
            <div className="search-pop">
              {results.length ? results.map(e=>(
                <div className="sr" key={e.id} onClick={()=>pick(e.id)}>
                  <window.TypeTag type={e.type} outline/>
                  <span className="nm">{e.name}</span>
                  <span className="lo">B{e.book}·C{e.chapter}</span>
                </div>
              )) : <div className="empty">No mention found for “{q}”.</div>}
            </div>
          )}
        </div>
        <button className={'spoiltog'+(spoil?' on':'')} onClick={()=>setSpoil(s=>!s)}
          title={spoil?'Later-book spoilers are showing across the whole index — click to re-seal':'Future-book intel is sealed across the whole index — click to reveal'}>
          <span className="st-ic">{spoil?'◈':'▓'}</span>
          <span className="st-tx hide-sm">{spoil?'Spoilers on':'Spoilers sealed'}</span>
        </button>
        <a className="merch hide-sm" onClick={()=>navTo({view:'merch'})}>Merch</a>
        <button className="navtoggle" onClick={()=>setMenu(m=>!m)} aria-label="Menu">{menu?'✕':'☰'}</button>
      </div>
    </div>
    {menu && (
      <div className="navdrawer">
        <a className={is('index')} onClick={()=>navTo({view:'index'})}>Index</a>
        <a className={is('books')} onClick={()=>navTo({view:'books'})}>Books</a>
        <a className={is('charmap')} onClick={()=>navTo({view:'charmap'})}>Characters</a>
        <a className={is('games')} onClick={()=>navTo({view:'games', game:null})}>Arcade</a>
        <a className="merch" onClick={()=>navTo({view:'merch'})}>Merch</a>
      </div>
    )}
    </nav>
  );
}

/* ---------- landing ---------- */
function Landing({go}){
  const feat = D.entry('donut');
  const featArt = window.useArtImage(feat.id);
  const liveBooks = D.BOOKS.filter(b=>b.status==='live').length;
  const partialBook = D.BOOKS.find(b=>b.status==='live' && b.mappedChapters<b.chapters);
  const total = D.allEntries().length;
  return (
    <div className="view">
      <header className="hero"><div className="wrap">
        <div className="hero-grid">
          <div>
            <span className="live"><span className="dot"></span>Live · now broadcasting from the dungeon</span>
            <h1 className="h-title">Welcome,<br/><span className="em">Crawler.</span></h1>
            <div className="h-onair">You're on air.</div>
            <p className="h-lede">The biggest <b>Dungeon Crawler Carl</b> index in the galaxy. Lost track of where that
              cursed item, that doomed contestant, or that busted spell first showed up? The (unofficial) System AI
              has the whole feed logged — by Book and Chapter.
              <span className="feet"> Shoes off, feet pics in, let's roll the tape.</span></p>
            <div className="h-cta">
              <a className="btn" onClick={()=>go({view:'index'})}>Open the Index ▸</a>
              <a className="btn ghost" onClick={()=>go({view:'charmap'})}>Character Timeline</a>
            </div>
            <div className="chips">
              <span className="chip"><b>{liveBooks}</b> books indexed</span>
              {partialBook && <span className="chip">Book {partialBook.n} <b>in progress</b></span>}
              <span className="chip"><b>{total}</b> entries</span>
              <span className="chip"><b>{D.INDEX_ORDER.length}</b> index categories</span>
            </div>
            <div className="seal-pill"><span className="sp-ic">▓</span> Future-book spoilers <b>sealed</b> by default — flip them on anytime from the nav.</div>
          </div>
          <div className="monitor">
            <span className="onair">● ON AIR</span>
            <div className={"screen"+(featArt.url?' has-img':'')} onClick={()=>go({view:'entry', id:feat.id})}>
              {featArt.url && <image-slot id={featArt.slot} display="1" fit="contain" shape="rect" className="screen-slot"
                style={{position:'absolute', inset:0, width:'100%', height:'100%', display:'block'}}></image-slot>}
              <span className="scan"></span>
              <span className="pl"><b>{feat.name}</b>featured this floor<br/>click to open the entry ▸</span>
            </div>
            <div className="cap"><span>FEED · DUNGEON FLOOR 1</span><span className="ft">REC ●</span></div>
          </div>
        </div>
      </div></header>

      <section className="section"><div className="wrap">
        <div className="feat">
          <div className="fcell"><div className="fn">01 · Mapped</div><h3>Every mention, by Book &amp; Chapter</h3>
            <p>Browse the way you read — descend a book, open a chapter, meet everything that first appears in it.</p></div>
          <div className="fcell"><div className="fn">02 · Yours</div><h3>Bring your own art</h3>
            <p>A fan page — upload your own art to any entry and it shows everywhere that entry appears. Each page also links out to where real, artist-credited art lives.</p></div>
          <div className="fcell"><div className="fn">03 · Sealed</div><h3>Spoilers stay redacted</h3>
            <p>Future-book references are blacked out until you choose to decrypt them. Your timeline, your risk.</p></div>
        </div>
      </div></section>
    </div>
  );
}

/* ---------- index hub (the many ways in) ---------- */
function IndexHub({go}){
  const liveBooks = D.BOOKS.filter(b=>b.status==='live').length;
  const total = D.allEntries().length;
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs"><a onClick={()=>go({view:'home'})}>Home</a><span className="sep">/</span><span style={{color:'var(--ink)'}}>Index</span></div>
      <div className="sec-top">
        <h2>The <span className="am">Index</span></h2>
        <p className="note">{total} entries across {liveBooks} indexed book{liveBooks===1?'':'s'}. Pick a way in.</p>
      </div>
      <div className="hub-grid">
        <div className="hubcard book-route" onClick={()=>go({view:'books'})}>
          <div className="hubglyph">❑</div>
          <div className="hubname">By Book &amp; Chapter</div>
          <div className="hubblurb">Read-order index — descend a book, open a chapter, meet everything that first appears.</div>
          <div className="hubcount">{liveBooks} books · {D.BOOKS.reduce((n,b)=>n+D.chaptersFor(b.n).length,0)} chapters</div>
        </div>
        <div className="hubcard book-route" onClick={()=>go({view:'charmap'})}>
          <div className="hubglyph">⧉</div>
          <div className="hubname">Character Timeline</div>
          <div className="hubblurb">Every character in introduction order, Book 1 → now — lifelines, beats, and deaths across the books. Mongo gets honorary cast status.</div>
          <div className="hubcount">{D.allEntries().filter(e=>e.type==='character'||e.id==='mongo').length} characters · {liveBooks} books</div>
        </div>
        <div className="hubcard book-route" onClick={()=>go({view:'games', game:null})} style={{['--tc']:'var(--t-spell)'}}>
          <div className="hubglyph" style={{color:'var(--t-spell)'}}>◈</div>
          <div className="hubname">The Arcade</div>
          <div className="hubblurb">Two quick, session-only games on the dungeon feed — a lore trivia run that earns a loot box, and a personality quiz that determines your nature and recommends a class.</div>
          <div className="hubcount" style={{color:'var(--t-spell)'}}>2 games · trivia + quiz</div>
        </div>
        {D.INDEX_ORDER.map(type=>{
          const m = D.TYPE_META[type];
          return (
            <div key={type} className="hubcard" style={{['--tc']:`var(--t-${type})`}} onClick={()=>go({view:'type', type})}>
              <div className="hubglyph" style={{color:`var(--t-${type})`}}>{m.glyph}</div>
              <div className="hubname">{m.name}</div>
              <div className="hubblurb">{m.blurb}</div>
              <div className="hubcount">{D.typeCount(type)} entries</div>
            </div>
          );
        })}
      </div>
    </div></div>
  );
}

/* ---------- type browse (one category, all books) ---------- */
function TypeBrowse({type, go}){
  const m = D.TYPE_META[type] || {name:type};
  const ents = D.entriesByType(type);
  const byBook = {};
  ents.forEach(e=>{ (byBook[e.book] = byBook[e.book] || []).push(e); });
  const snip = (type==='lootbox' || type==='notification' || type==='spell' || type==='event' || type==='lore');
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs">
        <a onClick={()=>go({view:'index'})}>Index</a><span className="sep">/</span>
        <span style={{color:'var(--ink)'}}>{m.name}</span>
      </div>
      <div className="sec-top">
        <h2><span style={{color:`var(--t-${type})`}}>{m.glyph}</span> {m.name}</h2>
        <p className="note">{ents.length} entries · alphabetical, grouped by first-appearance book.</p>
      </div>
      {Object.keys(byBook).sort().map(bk=>(
        <div key={bk} style={{marginBottom:28}}>
          <div className="block-h">Book {bk}</div>
          <div className={snip ? 'type-list' : 'entry-grid'}>
            {byBook[bk].map(e=> snip ? (
              <div key={e.id} className="ecard snip-card" style={{['--tc']:`var(--t-${type})`}} onClick={()=>go({view:'entry', id:e.id})}>
                <div className="ebody">
                  <div className="snip-top">
                    <window.TypeTag type={e.type}/>
                    <span className="snip-occ">B{e.book}·C{e.chapter}</span>
                  </div>
                  <h3 className="enm">{e.name}</h3>
                  <p className="ed">{e.sub || e.desc}</p>
                </div>
              </div>
            ) : (
              <div key={e.id} className="ecard" style={{['--tc']:`var(--t-${type})`}} onClick={()=>go({view:'entry', id:e.id})}>
                <window.ArtThumb entry={e}/>
                <div className="ebody">
                  <h3 className="enm">{e.name}</h3>
                  <p className="ed">{e.sub || e.desc}</p>
                </div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div></div>
  );
}

/* ---------- books index ---------- */
function Books({go}){
  const live = D.BOOKS.filter(b=>b.status==='live');
  const liveLabel = live.length===1 ? `Book ${live[0].n} is` : `Books ${live.map(b=>b.n).join(', ').replace(/, (\d+)$/,' and $1')} are`;
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs"><a onClick={()=>go({view:'home'})}>Home</a><span className="sep">/</span><span style={{color:'var(--ink)'}}>Books</span></div>
      <div className="sec-top"><h2>Browse by Book &amp; <span className="am">Chapter</span></h2>
        <p className="note">{liveLabel} mapped and live. The rest are being catalogued — check back, Crawler.</p></div>
      <div className="book-grid">
        {D.BOOKS.map(b=>(
          <div key={b.id} className={"book"+(b.status==='soon'?' locked':'')}
            onClick={()=> b.status==='live' ? go({view:'book', book:b.n}) : null}
            style={{cursor:b.status==='live'?'pointer':'default'}}>
            <span className="spine"></span>
            {b.status==='live' ? <span className="badge" style={{color:'var(--mag)'}}>Live</span> : <span className="badge soon">▓ Sealed</span>}
            <div className="bn">BOOK {b.n}</div>
            <div className="bttl">{b.title}</div>
            <div className="bch">{b.status==='live' ? `${b.mappedChapters<b.chapters ? `${b.mappedChapters} of ${b.chapters}` : b.chapters} chapters · ${b.entries} entries` : b.sub}</div>
            {b.status!=='live' && <div className="blocknote">Not yet mapped · decrypts when catalogued</div>}
          </div>
        ))}
      </div>
    </div></div>
  );
}

/* ---------- single book (chapter list) ---------- */
function Book({book, go}){
  const b = D.BOOKS.find(x=>x.n===book) || D.BOOKS[0];
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs"><a onClick={()=>go({view:'books'})}>Books</a><span className="sep">/</span><span style={{color:'var(--ink)'}}>Book {b.n}</span></div>
      <div className="sec-top"><h2>{b.title}</h2>
        <p className="note">{b.mappedChapters<b.chapters
          ? `Book ${b.n} · ${b.mappedChapters} of ${b.chapters} chapters mapped so far — more on the way. Pick a chapter to meet everything that first appears in it.`
          : `Book ${b.n} · all ${b.mappedChapters} chapters mapped (incl. Epilogue). Pick a chapter to meet everything that first appears in it.`}</p></div>
      <div className="chap-list">
        {D.chaptersFor(book).map(c=>{
          const ents = D.entriesInChapter(book, c.num);
          const recur = D.allEntries().filter(e=> e.book===book && e.chapter!==c.num && (
            (e.mentions||[]).some(m=>m.ch===c.num) || (e.awards||[]).some(a=>a.b===book && a.c===c.num)
          ));
          return (
            <div key={c.num} className="chap" onClick={()=>go({view:'chapter', book:b.n, ch:c.num})}>
              <div className="cnum">{String(c.num).padStart(2,'0')}</div>
              <div><div className="ct">{c.title}</div><div className="cs">{c.summary}</div></div>
              <div className="meta">
                <span className="count">{ents.length>0 ? `${ents.length} new ${ents.length===1?'entry':'entries'}` : (recur.length ? `${recur.length} referenced` : '—')}</span>
                <span className="dots">{[...new Set((ents.length?ents:recur).map(e=>e.type))].map((t,i)=>(<i key={i} style={{background:`var(--t-${t})`}} title={t}></i>))}</span>
              </div>
            </div>
          );
        })}
      </div>
    </div></div>
  );
}

/* ---------- chapter (entry grid) ---------- */
function Chapter({book, ch, go}){
  const [filter, setFilter] = useState('all');
  useEffect(()=>{ setFilter('all'); }, [book, ch]);
  const c = D.chapter(book, ch);
  const ents = D.entriesInChapter(book, ch);
  const firstIds = new Set(ents.map(e=>e.id));
  const recurring = D.allEntries().filter(e=> e.book===book && !firstIds.has(e.id) && (
    (e.mentions||[]).some(m=>m.ch===ch) || (e.awards||[]).some(a=>a.b===book && a.c===ch)
  ));
  const types = D.INDEX_ORDER.filter(t=> ents.some(e=>e.type===t));
  const shown = filter==='all' ? ents : ents.filter(e=>e.type===filter);
  if(!c) return <div className="view"><div className="wrap" style={{padding:'40px 0'}}>Chapter not found.</div></div>;
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs">
        <a onClick={()=>go({view:'books'})}>Books</a><span className="sep">/</span>
        <a onClick={()=>go({view:'book', book})}>Book {book}</a><span className="sep">/</span>
        <span style={{color:'var(--ink)'}}>Chapter {ch}</span>
      </div>
      <div className="sec-top">
        <h2><span style={{color:'var(--mag)'}}>{String(ch).padStart(2,'0')}</span> · {c.title}</h2>
        <p className="note">{ents.length} {ents.length===1?'entry first appears':'entries first appear'} here{recurring.length?` · ${recurring.length} also referenced`:''}.</p>
      </div>
      <p style={{maxWidth:680, color:'var(--ink-dim)', marginTop:6, marginBottom:26}}>{c.summary}</p>
      {ents.length>0 && (
        <React.Fragment>
          <div className="block-h">First appearances{filter!=='all'?` · ${D.typeName[filter]}`:''}</div>
          {types.length>1 && (
            <div className="chap-filter">
              <button className={'cfilt'+(filter==='all'?' on':'')} onClick={()=>setFilter('all')}>All <i>{ents.length}</i></button>
              {types.map(t=>(
                <button key={t} className={'cfilt'+(filter===t?' on':'')} style={{['--tc']:`var(--t-${t})`}} onClick={()=>setFilter(filter===t?'all':t)}>
                  {D.typeName[t]} <i>{ents.filter(e=>e.type===t).length}</i>
                </button>
              ))}
            </div>
          )}
        </React.Fragment>
      )}
      <div className="entry-grid">
        {shown.map(e=>{
          const isSnip = (e.type==='lootbox' || e.type==='notification' || e.type==='spell' || e.type==='event' || e.type==='lore');
          if(isSnip){
            const occ = (e.awards&&e.awards.length) ? e.awards.reduce((n,a)=>n+(a.qty||1),0) : (e.mentions&&e.mentions.length ? e.mentions.length : 1);
            const tier = (D.kindName && e.kind && D.kindName[e.kind]) || (e.stats.find(s=>s[0]==='Tier'||s[0]==='Type'||s[0]==='School')||[])[1];
            const verb = e.type==='lootbox'?'awarded':(e.type==='spell'?'appears':(e.type==='event'?'occurs':(e.type==='lore'?'noted':'logged')));
            return (
              <div key={e.id} className="ecard snip-card" style={{['--tc']:`var(--t-${e.type})`}} onClick={()=>go({view:'entry', id:e.id})}>
                <div className="ebody">
                  <div className="snip-top">
                    <window.TypeTag type={e.type}/>
                    {tier && <span className="snip-tier">{tier}</span>}
                    <span className="snip-occ">{verb} {occ}×</span>
                  </div>
                  <h3 className="enm">{e.name}</h3>
                  <p className="ed">{e.desc}</p>
                </div>
              </div>
            );
          }
          return (
            <div key={e.id} className="ecard" style={{['--tc']:`var(--t-${e.type})`}} onClick={()=>go({view:'entry', id:e.id})}>
              <window.ArtThumb entry={e}/>
              <div className="ebody">
                <h3 className="enm">{e.name}</h3>
                <p className="ed">{e.desc}</p>
              </div>
            </div>
          );
        })}
      </div>
      {recurring.length>0 && (
        <React.Fragment>
          <div className="block-h" style={{marginTop:34}}>Also appears in this chapter</div>
          <div style={{display:'flex',flexWrap:'wrap',gap:'10px'}}>
            {recurring.map(e=>(
              <div key={e.id} className="mention" style={{cursor:'pointer',flex:'0 0 auto'}} onClick={()=>go({view:'entry', id:e.id})}>
                <window.TypeTag type={e.type} outline/><span style={{fontFamily:'var(--f-gothic)',fontWeight:700,fontSize:'17px'}}>{e.name}</span>
              </div>
            ))}
          </div>
        </React.Fragment>
      )}
      {ents.length===0 && recurring.length===0 && (
        <p style={{color:'var(--ink-faint)',fontFamily:'var(--f-mono)',fontSize:'13px'}}>No entries logged for this chapter yet.</p>
      )}
    </div></div>
  );
}

/* ---------- merch (minimal) ---------- */
function Merch({go}){
  return (
    <div className="view"><div className="wrap">
      <div className="crumbs"><a onClick={()=>go({view:'home'})}>Home</a><span className="sep">/</span><span style={{color:'var(--ink)'}}>Merch</span></div>
      <div className="merch-wrap">
        <div className="badge">Official Store</div>
        <h2>Gear up, Crawler.</h2>
        <p>Official <b>Dungeon Crawler Carl</b> merch keeps this index running. This link will point to the official store —
          drop the storefront URL and I'll wire it in.</p>
        <a className="btn amber" href="#" onClick={e=>e.preventDefault()}>Visit the official store ▸</a>
      </div>
    </div></div>
  );
}

/* ---------- footer ---------- */
function Footer(){
  const D = window.DCC;
  const live = D.BOOKS.filter(b=>b.status==='live');
  const liveMax = live.length ? Math.max(...live.map(b=>b.n)) : 1;
  const partial = D.BOOKS.find(b=>b.status==='live' && b.mappedChapters<b.chapters && b.n===liveMax);
  const fullN = partial ? live.filter(b=>b.n!==partial.n).map(b=>b.n) : live.map(b=>b.n);
  const fullLabel = fullN.length>1 ? `Books ${fullN[0]}–${fullN[fullN.length-1]} complete` : `Book ${fullN[0]} complete`;
  const status = partial ? `${fullLabel}, Book ${partial.n} in progress` : fullLabel;
  return (
    <footer className="foot"><div className="wrap"><div className="foot-in">
      <div className="brand"><span className="sigil">▲</span><span className="bt">System AI<small>UNOFFICIAL DCC INDEX</small></span></div>
      <p className="disc">
        <b>Unofficial, fan-made index.</b> Not affiliated with, endorsed by, or sponsored by the author or publisher.
        <em> Dungeon Crawler Carl</em> and all related names belong to their respective owners.<br/>
        Prototype · {status} · {D.allEntries().length} entries · descriptions fan-written · later books being catalogued.
        Future-book references stay sealed until you decrypt them.
      </p>
    </div></div></footer>
  );
}

/* ---------- back to top ---------- */
function BackTop(){
  const [show, setShow] = useState(false);
  useEffect(()=>{
    function onScroll(){ setShow(window.scrollY > 720); }
    window.addEventListener('scroll', onScroll, {passive:true});
    onScroll();
    return ()=>window.removeEventListener('scroll', onScroll);
  },[]);
  return (
    <button className={'backtop'+(show?' on':'')} aria-label="Back to top"
      onClick={()=>window.scrollTo({top:0, behavior:'smooth'})}>▲<span>TOP</span></button>
  );
}

/* ---------- app ---------- */
function App(){
  const [route, setRoute] = useState(decode());
  useEffect(()=>{
    function onHash(){ setRoute(decode()); window.scrollTo(0,0); }
    window.addEventListener('hashchange', onHash);
    return ()=>window.removeEventListener('hashchange', onHash);
  },[]);
  function go(r){ const h = encode(r); if(location.hash===h){ setRoute(r); window.scrollTo(0,0);} else location.hash=h; }


  let body;
  switch(route.view){
    case 'index': body = <IndexHub go={go}/>; break;
    case 'type': body = <TypeBrowse type={route.type} go={go}/>; break;
    case 'books': body = <Books go={go}/>; break;
    case 'charmap': body = <window.CharFlow go={go}/>; break;
    case 'book': body = <Book book={route.book} go={go}/>; break;
    case 'chapter': body = <Chapter book={route.book} ch={route.ch} go={go}/>; break;
    case 'games': body = <window.GamesPage game={route.game} go={go}/>; break;
    case 'entry': body = <window.EntryDetail key={route.id} entry={D.entry(route.id)} go={go}/>; break;
    case 'merch': body = <Merch go={go}/>; break;
    default: body = <Landing go={go}/>;
  }
  return (
    <React.Fragment>
      <Ticker/>
      <div className="hazard"></div>
      <Nav route={route} go={go}/>
      <main>{body}</main>
      <Footer/>
      <BackTop/>
    </React.Fragment>
  );
}

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