/* Entry detail view + redaction-decrypt spoiler + appearance-map timeline */
const { useState, useRef } = React;

/* Single art slot per entry. This is a fan page: the only in-page image is one
   the user uploads themselves (drag-drop, persisted via <image-slot>). The old
   official/semi/fan/AI tiers were removed — we never ship or re-host artwork.
   Where to legitimately *find* real art is surfaced via each entry's `artRef`
   credits (see ArtRefs / data.js / DCCArt). */
const ART_SLOT = entryId => 'art-' + entryId;

function TypeTag({type, outline}){
  const label = window.DCC.typeName[type] || type;
  const cls = outline ? 'etag-out' : 'etype';
  return <span className={cls} style={{['--tc']:`var(--t-${type})`}}>{label}</span>;
}

/* Read an entry's saved art from the shared <image-slot> sidecar (via
   window.ImageSlots). One slot per entry now — art-<id>. Live-updates when art
   is dropped or the sidecar finishes loading. Powers the index/chapter grid
   thumbnails so a user's uploaded image shows EVERYWHERE the entry appears,
   not just on its detail page. */
function useArtImage(entryId){
  const [st, setSt] = useState({url:null, slot:null});
  React.useEffect(()=>{
    if(!window.ImageSlots){ setSt({url:null, slot:null}); return; }
    const id = ART_SLOT(entryId);
    const read = ()=>{
      const u = window.ImageSlots.get(id);
      setSt(u ? {url:u, slot:id} : {url:null, slot:null});
    };
    const unsub = window.ImageSlots.subscribe(read);
    read();
    return unsub;
  }, [entryId]);
  return st;
}

/* Grid/index thumbnail. Renders the saved art through a READ-ONLY image-slot
   (display mode) — the very same component, fit and stored crop the detail
   page uses — so a character looks identical in the index, the landing
   monitor, and on their entry page. Falls back to the striped placeholder. */
function ArtThumb({entry, label}){
  const {url, slot} = useArtImage(entry.id);
  return (
    <div className={'ethumb'+(url?' filled':'')}>
      <TypeTag type={entry.type}/>
      {url
        ? <image-slot id={slot} display="1" fit="contain" shape="rect" className="ethumb-slot"
            style={{position:'absolute', inset:0, width:'100%', height:'100%', display:'block'}}></image-slot>
        : <span className="pl">{label || 'Art'} · slot</span>}
    </div>
  );
}

/* Inline cross-links. A description links the proper nouns it actually
   names — characters, creatures and places — plus anything in this entry's
   curated `related` list (which also covers short forms like "Bea"/"Donut").
   Matching is case-sensitive with letter boundaries so common words
   ("torch", "poker") never false-trigger, and each target links at most once
   (its first mention). Self-references are skipped. */
function escRe(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); }
function nameTerms(nm, minLen, withLastWord){
  const set = new Set([nm]);
  const stripped = nm.replace(/\s*\([^)]*\)\s*/g,' ').replace(/\s+/g,' ').trim();
  if(stripped) set.add(stripped);
  const paren = (nm.match(/\(([^)]+)\)/)||[])[1];
  if(paren) set.add(paren.trim());
  if(withLastWord){ const w = stripped.split(' '); if(w.length>1 && w[w.length-1].length>=4) set.add(w[w.length-1]); }
  return [...set].filter(t=>t && t.length>=minLen);
}
function globalTerms(){
  if(window.__dccGlobalTerms) return window.__dccGlobalTerms;
  const list = [];
  window.DCC.allEntries().forEach(t=>{
    if(!(t.type==='character'||t.type==='creature'||t.type==='place')) return;
    nameTerms(t.name, 4, false).forEach(term=>list.push({term, id:t.id}));
  });
  window.__dccGlobalTerms = list;
  return list;
}
function relatedTerms(entry){
  const E = window.DCC.ENTRIES, list = [];
  (entry.related||[]).forEach(id=>{
    const t = E[id]; if(!t || id===entry.id) return;
    nameTerms(t.name, 3, true).forEach(term=>list.push({term, id}));
  });
  return list;
}
function buildTerms(entry){
  const all = relatedTerms(entry).concat(globalTerms().filter(x=>x.id!==entry.id));
  const map = new Map();
  for(const {term,id} of all){ if(!map.has(term)) map.set(term, id); }   // related first → wins ties
  const terms = [...map.keys()].sort((a,b)=>b.length-a.length);          // longest-first alternation priority
  return {map, terms};
}
function linkify(text, entry, go){
  if(!text || typeof text!=='string') return text;
  const {map, terms} = buildTerms(entry);
  if(!terms.length) return text;
  let re;
  try{ re = new RegExp('(?<![A-Za-z0-9])('+terms.map(escRe).join('|')+')(?![A-Za-z0-9])','g'); }
  catch(e){ return text; }
  const used = new Set(), nodes = [];
  let last = 0, m;
  while((m = re.exec(text))){
    const id = map.get(m[1]);
    if(!id || id===entry.id || used.has(id)) continue;
    used.add(id);
    if(m.index>last) nodes.push(text.slice(last, m.index));
    nodes.push(<a className="xlink" key={'x'+m.index} onClick={e=>{e.stopPropagation(); go({view:'entry', id});}}>{m[1]}</a>);
    last = m.index + m[1].length;
  }
  if(last < text.length) nodes.push(text.slice(last));
  return nodes.length ? nodes : text;
}

/* Canon naming convention: the System appends a number to crawlers who
   share a name with an already-registered crawler (Chris Andrews 2,
   Yolanda Martinez 13…). Explain it so it doesn't read as a typo. */
function DupNameNote({entry}){
  if(entry.type!=='character') return null;
  const m = /^(.+?)\s+(\d+)$/.exec(entry.name);
  if(!m) return null;
  return (
    <div className="dupnote"><span className="sig">※</span> Not a typo — the System numbers duplicate names. Another crawler had already registered “{m[1]}”.</div>
  );
}

/* Reference-art credits: where to legitimately VIEW real art for this entry.
   Link + credit only — we never re-host. Renders entry.artRef (from data.js /
   DCCArt). Renders nothing if an entry has no known sources. */
function ArtRefs({entry}){
  const refs = entry.artRef || [];
  if(!refs.length) return null;
  return (
    <div className="art-ref">
      <div className="art-ref-h">Reference art <span className="art-ref-sub">— view at source · credit the artist</span></div>
      <ul className="art-ref-list">
        {refs.map((r,i)=>(
          <li key={i}>
            <a href={r.url} target="_blank" rel="noopener noreferrer" className="art-ref-link">{r.label}</a>
            {r.kind && <span className="art-ref-kind">{r.kind}</span>}
            {r.credit && <span className="art-ref-credit">{r.credit}</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

/* Single user-upload art slot built on the <image-slot> web component. This is
   a fan page: readers upload their OWN art (drag-drop or click), persisted to
   the sidecar (shareable) under a stable id: art-<entry>. We ship no art — see
   ArtRefs for where to find real, credited artwork. */
function ArtStack({entry}){
  return (
    <div className="art-stack">
      <div className="slotwrap hero">
        <image-slot id={ART_SLOT(entry.id)} className="islot" shape="rounded" radius="8" fit="contain"
          placeholder={'Upload your art — '+entry.name}
          style={{display:'block', width:'100%', height:'330px'}}></image-slot>
        <span className="art-badge">Your upload</span>
      </div>
      <div className="art-note"><b>Your art here.</b> Drag an image onto the slot (or click to browse) — it saves to the page and shows everywhere this entry appears. This is a fan page; we don't ship official, fan, or AI art. Please only upload art you have the right to use.</div>
      <ArtRefs entry={entry}/>
    </div>
  );
}

/* Appearance map. A node from a LATER book than the entry's debut is a
   future-book spoiler: redacted until the reader decrypts. One rule, no
   manual flags — add a real node and it seals/reveals itself correctly. */
function Timeline({entry, decrypted}){
  return (
    <div className="timeline">
      <div className="tl-track">
        {entry.timeline.map((n,i)=>{
          const later = n.book > entry.book;
          const sealed = later && !decrypted;
          const cls = 'tl-node'+(n.first?' first':'')+(later?' later':'')+(sealed?' sealed':'');
          return (
            <div key={i} className={cls}>
              <span className="dot"></span>
              <span className="loc">{`B${n.book}·C${n.ch}`}</span>
              <span className="lab">{n.first?'First appearance':(n.label||'Reappears')}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* One clearance control for the whole entry: a single toggle that unseals
   every later-book element at once (appearance map, per-book events, form
   log, future refs). Shown only when the entry actually has sealed content. */
function DecryptBar({decrypted, setDecrypted, name}){
  return (
    <div className={"decrypt-bar"+(decrypted?' is-on':'')}>
      <div className="db-warn"><span className="ic">{decrypted?'◈':'▓'}</span>{decrypted?'Clearance granted':'Later-book intel · sealed'}</div>
      <div className="db-mid">{decrypted
        ? `Everything the System logged about ${name} after their debut book is now visible.`
        : `${name} recurs in later books. Reveal those events at your own risk, Crawler.`}</div>
      <button className="decrypt-btn" onClick={()=>setDecrypted(d=>!d)}>{decrypted?'Re-seal ◂':'Decrypt ▸'}</button>
    </div>
  );
}

/* Per-book “major events” — the chapter beats, grouped by book. A book
   later than the debut book is a spoiler: its notes stay redacted until the
   reader decrypts. mentions[].book defaults to the entry's debut book. */
function MajorEvents({entry, decrypted, go}){
  if(!entry.mentions || !entry.mentions.length) return null;
  const byBook = {};
  entry.mentions.forEach(m=>{ const b=m.book||entry.book; (byBook[b]=byBook[b]||[]).push(m); });
  const books = Object.keys(byBook).map(Number).sort((a,b)=>a-b);
  const beatKey = new Set((entry.timeline||[]).map(n=>n.book+'-'+n.ch));
  return (
    <div className="section" style={{paddingBottom:0}}>
      {beatKey.size>0 && <div className="mev-key"><b className="beatdot">◆</b> a beat charted on the Character Timeline · the rest are other logged appearances</div>}
      {books.map(b=>{
        const later = b>entry.book;
        const sealed = later && !decrypted;
        return (
          <div className="mbook" key={b}>
            <div className="block-h">Appears in Book {b}{later?(sealed?' · sealed':' · later book'):''}</div>
            <div className="mentions">
              {byBook[b].map((m,i)=>{
                const isBeat = beatKey.has(b+'-'+m.ch);
                return (
                <div className={"mention"+(isBeat?' isbeat':'')+(sealed?' sealed':'')} key={i}
                  onClick={()=> sealed ? null : go({view:'chapter', book:b, ch:m.ch})}>
                  <span className="loc">{isBeat && <b className="beatdot">◆</b>}B{b}·C{m.ch}</span>
                  <span className="mtxt">{m.note}</span>
                  {sealed ? <i className="mbar"></i> : <span className="arr">↗</span>}
                </div>
                );
              })}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* Form log — for shapeshifters & floor-reassigned NPCs (Mordecai, Katia…).
   Rows from the entry's first book are always visible; later-book forms
   stay redacted until the reader decrypts (same clearance state as the
   future-refs spoiler). */
function FormLog({entry, decrypted}){
  if(!entry.formLog || !entry.formLog.length) return null;
  return (
    <div className={"spoiler redact"+(decrypted?' is-on':'')}>
      <div className="sp-top">
        <div className="sp-warn"><span className="ic">{decrypted?'◈':'▓'}</span>{decrypted?'Form log · full record':'Form log · later forms sealed'}</div>
      </div>
      <p className="sp-desc">The dungeon issues {entry.name} a new body as the floors change. Forms from later books stay redacted until you decrypt above.</p>
      {entry.formLog.map((r,i)=>{
        const open = decrypted || r.b<=entry.book;
        return (
          <div className="xref" key={i}>
            <div className="rloc">{'B'+r.b+'·C'+r.c}</div>
            <div className="rtxt">
              <span className="inner" style={{filter: open ? 'none' : 'blur(3px)'}}>
                <b className="formname">{r.form}</b> — {r.note}
              </span>
              <i className="bar" style={{transform: open ? 'scaleX(0)' : 'scaleX(1)'}}></i>
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* Loot box ledger — per-book tally of boxes the System logged to this
   character. Counts only awards whose recipient was broadcast (awards[].to).
   Books later than the debut stay sealed until the reader decrypts. */
const BOX_TIERS = ['Bronze','Silver','Gold','Platinum','Legendary','Celestial','Mixed'];
const BOX_TIER_COLOR = {Bronze:'#cd8a4f', Silver:'#cdd5dc', Gold:'#ffd34d', Platinum:'#8be9e2', Legendary:'#b98bff', Celestial:'#fff3f9', Mixed:'#9a8a92'};
function boxTier(box){
  const t = ((box.stats||[]).find(s=>/^tier$/i.test(s[0]))||[])[1] || box.sub || '';
  const m = /bronze|silver|gold|platinum|legendary|celestial/i.exec(String(t));
  return m ? m[0][0].toUpperCase()+m[0].slice(1).toLowerCase() : 'Mixed';
}
/* tier mini-bar: a thin proportional band of a book's tier mix. Used in the
   collapsed book header and the lifetime summary so tier colour is the one
   constant primitive from summary → book → chip. */
function tierMiniBar(tiers, cls){
  const tot = BOX_TIERS.reduce((n,t)=>n+(tiers[t]||0),0);
  if(!tot) return null;
  return (
    <span className={'led-minibar'+(cls?' '+cls:'')}>
      {BOX_TIERS.filter(t=>tiers[t]).map(t=>(
        <span key={t} style={{width:(tiers[t]/tot*100)+'%', background:BOX_TIER_COLOR[t]}}></span>
      ))}
    </span>
  );
}

function BoxLedger({entry, decrypted, go}){
  const D = window.DCC;
  const byBook = {};
  D.allEntries().forEach(box=>{
    if(box.type!=='lootbox') return;
    (box.awards||[]).forEach(a=>{
      if(!a.to || a.to.indexOf(entry.id)<0) return;
      const q = a.qty||1;                 /* a single award row can be a batch of N identical boxes */
      const r = byBook[a.b] = byBook[a.b] || {tiers:{}, list:[], count:0};
      const tier = boxTier(box);
      r.tiers[tier] = (r.tiers[tier]||0)+q;
      r.count += q;
      r.list.push({box, b:a.b, c:a.c, tier, qty:q});
    });
  });
  const books = Object.keys(byBook).map(Number).sort((a,b)=>a-b);
  const sealedOf = b => b>entry.book && !decrypted;

  /* default open: the character's debut book if present, else the first
     visible book. Other books collapse to their tier mini-bar so the ledger
     stays a uniform, comparable stack as more books are mapped. */
  const visibleBooks = books.filter(b=>!sealedOf(b));
  const firstOpen = visibleBooks.indexOf(entry.book)>=0 ? entry.book : visibleBooks[0];
  const [open, setOpen] = useState({});
  if(!books.length) return null;
  const isOpen = b => (b in open) ? open[b] : (b===firstOpen);
  const toggle = b => setOpen(s=>({...s, [b]: !isOpen(b)}));

  const total = books.reduce((n,b)=>n+byBook[b].count,0);
  /* lifetime summary across what the reader can currently see */
  const lifeTiers = {}; let visTotal = 0;
  visibleBooks.forEach(b=>{ visTotal += byBook[b].count;
    for(const t in byBook[b].tiers) lifeTiers[t]=(lifeTiers[t]||0)+byBook[b].tiers[t]; });
  const sealedCount = books.length - visibleBooks.length;
  const span = visibleBooks.length>1
    ? `BOOKS ${visibleBooks[0]}–${visibleBooks[visibleBooks.length-1]}`
    : (visibleBooks.length ? `BOOK ${visibleBooks[0]}` : '');

  return (
    <div className="section" style={{paddingBottom:0}}>
      <div className="block-h">Loot box ledger · {total} logged</div>

      {visTotal>0 && (
        <div className="rec-sum">
          <div className="rec-sum-top">
            <div className="rec-sum-big">{visTotal}<small>BOX{visTotal===1?'':'ES'} LOGGED · {span}</small></div>
            {sealedCount>0 && <div className="rec-sum-meta">{sealedCount} later book{sealedCount===1?'':'s'} sealed · decrypt to reveal</div>}
          </div>
          <div className="tierbar">
            {BOX_TIERS.filter(t=>lifeTiers[t]).map(t=>(
              <span key={t} style={{width:(lifeTiers[t]/visTotal*100)+'%', background:BOX_TIER_COLOR[t]}}></span>
            ))}
          </div>
          <div className="tierkey">
            {BOX_TIERS.filter(t=>lifeTiers[t]).map(t=>(
              <b key={t}><i style={{background:BOX_TIER_COLOR[t]}}></i>{t} <em>· {lifeTiers[t]}</em></b>
            ))}
          </div>
        </div>
      )}

      <div className="ledger">
        {books.map(b=>{
          const sealed = sealedOf(b);
          const r = byBook[b];
          const meta = (D.BOOKS||[]).find(x=>x.n===b);
          const o = isOpen(b);
          return (
            <div className={'led-book'+(sealed?' sealed':'')} key={b}>
              <div className="led-head" onClick={()=>toggle(b)}>
                <span className="led-caret">{o?'▾':'▸'}</span>
                <span className="led-bn">BOOK {b}</span>
                {meta && <span className="led-bt">{meta.title}</span>}
                {!sealed && tierMiniBar(r.tiers)}
                <span className="led-tot"><b>{sealed?'▓▓':r.count}</b> box{r.count===1?'':'es'}{sealed?' · sealed':''}</span>
              </div>
              {o && (
                <div className="led-body">
                  <div className="led-tiers">
                    {BOX_TIERS.filter(t=>r.tiers[t]).map(t=>(
                      <span className="led-tier" key={t} style={{['--lc']:BOX_TIER_COLOR[t]}}>
                        <b>{sealed?'?':r.tiers[t]}×</b> {t}
                      </span>
                    ))}
                  </div>
                  {sealed ? (
                    <div className="led-collapsed-note">▓▓▓▓ contents sealed — raise clearance to reveal Book {b}</div>
                  ) : (
                    <div className="led-list">
                      {r.list.map((x,i)=>(
                        <span className="led-box" key={i} style={{['--lc']:BOX_TIER_COLOR[x.tier]}}
                          onClick={(ev)=>{ ev.stopPropagation(); go({view:'entry', id:x.box.id}); }}>
                          {x.box.name}{x.qty>1?<em style={{fontWeight:800,fontStyle:'normal',margin:'0 .1em 0 .4em',opacity:.9}}>×{x.qty}</em>:null}<i>C{x.c}</i>
                        </span>
                      ))}
                    </div>
                  )}
                </div>
              )}
            </div>
          );
        })}
      </div>
      <div className="led-note">Only drops whose recipient was broadcast are tallied — box types and unattributed drops live in the <a onClick={()=>go({view:'type', type:'lootbox'})}>Loot Box index</a>.</div>
    </div>
  );
}

function RedactionSpoiler({entry, decrypted, go}){
  const E = window.DCC.ENTRIES;
  return (
    <div className={"spoiler redact"+(decrypted?' is-on':'')}>
      <div className="sp-top">
        <div className="sp-warn"><span className="ic">{decrypted?'◈':'▓'}</span>{decrypted?'Decrypted · future-book teasers':'Future-book teasers · sealed'}</div>
      </div>
      <p className="sp-desc">Vague glimpses of what's still ahead for {entry.name}. Revealed with the clearance toggle above.</p>
      {entry.futureRefs.map((r,i)=>(
        <div className="xref" key={i}>
          <div className="rloc">{r.loc}</div>
          <div className="rtxt">
            <span className="inner" style={{filter: decrypted ? 'none' : 'blur(3px)'}}>
              {r.text}
              {r.link && E[r.link] && (
                <span className="rlink" style={{visibility: decrypted ? 'visible' : 'hidden'}} onClick={()=>go({view:'entry', id:r.link})}>→ see {E[r.link].name}</span>
              )}
            </span>
            <i className="bar" style={{transform: decrypted ? 'scaleX(0)' : 'scaleX(1)'}}></i>
          </div>
        </div>
      ))}
    </div>
  );
}

/* One occurrence in the occurrence ledger. Collapsed it shows a location + a
   one-line preview; clicking expands a full summary of THAT specific time the
   box was awarded (or the event occurred): who received it, what it held, and
   a jump to the chapter. This is the fix for "the page only describes the
   first award" — every recurrence now carries its own summary. */
function OccRow({entry, o, go, defaultOpen}){
  const E = window.DCC.ENTRIES;
  const [open, setOpen] = useState(!!defaultOpen);
  const people = (o.to||[]).map(id=>E[id]).filter(Boolean);
  const chTitle = (window.DCC.chapter(o.b, o.c)||{}).title || '';
  const fallback = people.length
    ? 'Awarded to '+people.map(p=>p.name).join(' & ')+(chTitle?' · '+chTitle:'')
    : (chTitle || 'Open this occurrence');
  const isBox = entry.type==='lootbox';
  return (
    <div className={'occ-ev'+(open?' open':'')}>
      <button className="occ-ev-head" onClick={()=>setOpen(v=>!v)}>
        <span className="occ-ev-loc">B{o.b}·C{o.c}</span>
        {o.qty>1 && <span className="occ-ev-qty">×{o.qty}</span>}
        <span className="occ-ev-prev">{open ? (chTitle || ('Chapter '+o.c)) : (o.note || fallback)}</span>
        <span className="occ-ev-caret">{open?'▾':'▸'}</span>
      </button>
      {open && (
        <div className="occ-ev-body">
          {o.note
            ? <p className="occ-ev-note">{linkify(o.note, entry, go)}</p>
            : <p className="occ-ev-note dim">No per-occurrence summary logged yet — jump to the chapter for the full scene.</p>}
          <div className="occ-ev-meta">
            {people.length>0 && (
              <div className="occ-ev-to">
                <span className="lab">{isBox?'Awarded to':'Involves'}</span>
                {people.map(p=>(
                  <span className="occ-person" key={p.id} onClick={()=>go({view:'entry', id:p.id})}>{p.name}</span>
                ))}
                {o.qty>1 && <span className="occ-ev-each">· {o.qty} each</span>}
              </div>
            )}
            <a className="occ-ev-goch" onClick={()=>go({view:'chapter', book:o.b, ch:o.c})}>
              Open B{o.b}·C{o.c}{chTitle?' — '+chTitle:''} ↗
            </a>
          </div>
        </div>
      )}
    </div>
  );
}

/* Compact snippet view for loot boxes & notifications: no art, just the
   basic record (rarity / type / contents) plus an OCCURRENCE INDEX — the
   count and chapter list that powers the "X total awarded" goal. */
function SnippetEntry({entry, go, prev, next}){
  const E = window.DCC.ENTRIES;
  const occ = (entry.awards && entry.awards.length) ? entry.awards
            : (entry.mentions && entry.mentions.length) ? entry.mentions.map(m=>({b:m.book||entry.book,c:m.ch,note:m.note}))
            : [{b:entry.book,c:entry.chapter}];
  const occTotal = occ.reduce((n,o)=>n+(o.qty||1),0);
  const isBox = entry.type==='lootbox';
  const occVerb = isBox ? 'awarded' : (entry.type==='spell' ? 'appears' : (entry.type==='event' ? 'occurs' : (entry.type==='lore' ? 'noted' : 'logged')));
  const occThing = isBox ? 'box is awarded' : (entry.type==='spell' ? 'spell/skill appears' : (entry.type==='event' ? 'event unfolds' : (entry.type==='lore' ? 'lore is recorded' : 'notification fires')));
  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:entry.book})}>Book {entry.book}</a><span className="sep">/</span>
        <a onClick={()=>go({view:'chapter', book:entry.book, ch:entry.chapter})}>Ch. {entry.chapter}</a><span className="sep">/</span>
        <span style={{color:'var(--ink)'}}>{entry.name}</span>
      </div>
      <div className="entry-hero">
        <div><TypeTag type={entry.type}/>{entry.kind && window.DCC.kindName[entry.kind] && <span className="snip-tier" style={{marginLeft:8}}>{window.DCC.kindName[entry.kind]}</span>}<h1 className="eh-name">{entry.name}</h1><div className="eh-sub">{entry.sub}</div></div>
        <div className="first-stamp">First logged<b>BOOK {entry.book} · CHAPTER {entry.chapter}</b></div>
      </div>
      <div className="snippet">
        <p className="desc">{linkify(entry.desc, entry, go)}</p>
        {entry.stats.length>0 && (
          <div className="stat-row">
            {entry.stats.map((s,i)=>(<span className="stat" key={i}>{s[0]}: <b>{s[1]}</b></span>))}
          </div>
        )}
        <div className="block-h">Occurrence index · {occVerb} {occTotal}×{occTotal!==occ.length?(' over '+occ.length+' logs'):''}</div>
        <div className="occ-summary">
          <span className="occ-count">{occTotal}</span>
          <div className="occ-sumtxt">
            <b>{occVerb} {occTotal} time{occTotal===1?'':'s'}</b>
            <span>Tap any log below for a summary of that {isBox?'drop':'occurrence'} — who got it and what it held.</span>
          </div>
        </div>
        <div className="occ-led">
          {occ.map((o,i)=><OccRow key={i} entry={entry} o={o} go={go} defaultOpen={i===0}/>)}
        </div>
        <div className="occ-note">Every chapter where this {occThing}, each with its own summary. The list grows automatically as more chapters and books are mapped — the basis for the “X total {occVerb}” index.</div>
        {entry.related && entry.related.length>0 && (
          <React.Fragment>
            <div className="block-h">{isBox?'Contents / related':'Related entries'}</div>
            <div style={{display:'flex',flexWrap:'wrap',gap:'10px'}}>
              {entry.related.map(id=> E[id] ? (
                <div key={id} className="mention" style={{cursor:'pointer',flex:'0 0 auto'}} onClick={()=>go({view:'entry', id})}>
                  <TypeTag type={E[id].type} outline/><span style={{fontFamily:'var(--f-gothic)',fontWeight:700,fontSize:'17px'}}>{E[id].name}</span>
                </div>
              ) : null)}
            </div>
          </React.Fragment>
        )}
      </div>
      <div className="entry-nav">
        {prev ? <a onClick={()=>go({view:'entry', id:prev.id})}><div className="lab">◂ Previous entry</div><div className="nm">{prev.name}</div></a> : <span style={{flex:'1 1 240px'}}/>}
        {next ? <a className="nx" onClick={()=>go({view:'entry', id:next.id})}><div className="lab">Next entry ▸</div><div className="nm">{next.name}</div></a> : <span style={{flex:'1 1 240px'}}/>}
      </div>
    </div></div>
  );
}

function EntryDetail({entry, go}){
  const [decrypted, setDecrypted] = window.useSpoilers();
  const D = window.DCC;
  const E = D.ENTRIES;
  const idx = D.ORDER.indexOf(entry.id);
  const prev = idx>0 ? E[D.ORDER[idx-1]] : null;
  const next = idx<D.ORDER.length-1 ? E[D.ORDER[idx+1]] : null;

  if(entry.type==='lootbox' || entry.type==='notification' || entry.type==='spell' || entry.type==='event' || entry.type==='lore')
    return <SnippetEntry entry={entry} go={go} prev={prev} next={next}/>;

  // Any element from a book later than the debut is a spoiler — if the entry
  // has one, surface the single clearance control.
  const hasSpoiler = entry.mentions.some(m=>(m.book||entry.book)>entry.book)
    || entry.timeline.some(n=>n.book>entry.book)
    || (entry.formLog||[]).some(r=>r.b>entry.book)
    || entry.futureRefs.length>0
    || D.allEntries().some(b=> b.type==='lootbox' && (b.awards||[]).some(a=> a.to && a.to.indexOf(entry.id)>=0 && a.b>entry.book));

  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:entry.book})}>Book {entry.book}</a><span className="sep">/</span>
          <a onClick={()=>go({view:'chapter', book:entry.book, ch:entry.chapter})}>Ch. {entry.chapter}</a><span className="sep">/</span>
          <span style={{color:'var(--ink)'}}>{entry.name}</span>
        </div>

        <div className="entry-hero">
          <div>
            <TypeTag type={entry.type}/>
            <h1 className="eh-name">{entry.name}</h1>
            <div className="eh-sub">{entry.sub}</div>
            <DupNameNote entry={entry}/>
          </div>
          <div className="first-stamp">First logged<b>BOOK {entry.book||1} · CHAPTER {entry.chapter}</b></div>
        </div>

        <div className="detail-grid">
          <ArtStack entry={entry}/>
          <div className="detail-info">
            <p className="desc">{linkify(entry.desc, entry, go)}</p>
            {entry.stats.length>0 && (
              <div className="stat-row">
                {entry.stats.map((s,i)=>(<span className="stat" key={i}>{s[0]}: <b>{s[1]}</b></span>))}
              </div>
            )}
          </div>
        </div>

        {hasSpoiler && <DecryptBar decrypted={decrypted} setDecrypted={setDecrypted} name={entry.name}/>}

        {entry.timeline.length>=2 && (
          <div className="section" style={{paddingBottom:0}}>
            <div className="block-h">Appearance map</div>
            <Timeline entry={entry} decrypted={decrypted}/>
          </div>
        )}

        <BoxLedger entry={entry} decrypted={decrypted} go={go}/>

        <MajorEvents entry={entry} decrypted={decrypted} go={go}/>

        <FormLog entry={entry} decrypted={decrypted}/>

        {entry.futureRefs.length>0 && (
          <RedactionSpoiler entry={entry} decrypted={decrypted} go={go}/>
        )}

        {entry.related && entry.related.length>0 && (
          <div className="section" style={{paddingBottom:0}}>
            <div className="block-h">Related entries</div>
            <div style={{display:'flex',flexWrap:'wrap',gap:'10px'}}>
              {entry.related.map(id=> E[id] ? (
                <div key={id} className="mention" style={{cursor:'pointer',flex:'0 0 auto'}} onClick={()=>go({view:'entry', id})}>
                  <TypeTag type={E[id].type} outline/><span style={{fontFamily:'var(--f-gothic)',fontWeight:700,fontSize:'17px'}}>{E[id].name}</span>
                </div>
              ) : null)}
            </div>
          </div>
        )}

        <div className="entry-nav">
          {prev ? <a onClick={()=>go({view:'entry', id:prev.id})}><div className="lab">◂ Previous entry</div><div className="nm">{prev.name}</div></a> : <span style={{flex:'1 1 240px'}}/>}
          {next ? <a className="nx" onClick={()=>go({view:'entry', id:next.id})}><div className="lab">Next entry ▸</div><div className="nm">{next.name}</div></a> : <span style={{flex:'1 1 240px'}}/>}
        </div>
      </div>
    </div>
  );
}

window.EntryDetail = EntryDetail;
window.TypeTag = TypeTag;
window.ArtThumb = ArtThumb;
window.useArtImage = useArtImage;
