// Hunters Association — Global Hub
// Tabs: Guilds · Requests · Dice Tools
// Codex lives in codex.jsx

const ASSOC_TABS = [
  { id: 'guilds',   label: '⚔ Guilds'        },
  { id: 'requests', label: '📋 Requests'      },
  { id: 'dice',     label: '🎲 Dice Tools'    },
  { id: 'codex',    label: '📖 Codex'         },
];

const RANK_ORDER = ['E','D','C','B','A','S'];
const RANK_THRESHOLDS_A = { E:10, D:50, C:250, B:1000, A:5000, S:15000 };
const RANK_COLOR = { E:'#6b6966', D:'#1e6b3c', C:'#1a5c7a', B:'#7c3aed', A:'#c9a227', S:'#c53030' };
const DIFF_COLOR = { Easy:'#1e6b3c', Medium:'#b45309', Hard:'#c53030', Legendary:'#7c3aed' };
const REQ_TYPES = ['Gathering','Hunting','Mining','Investigation','Crafting','Escort','Exploration','Defense','Other'];
const REQ_STATUS_COLOR = { open:'#1e6b3c', claimed:'#b45309', completed:'#4a4a52' };

// ── Association Root ──────────────────────────────────────────────────────────
const AssociationView = ({ user, setNav }) => {
  const [tab, setTab] = React.useState('guilds');
  // "Can act as DM" for these global views = admin OR DM/Co-DM of at least one
  // campaign. We load campaigns once for the check; server still enforces.
  const [campaigns, setCampaigns] = React.useState([]);
  React.useEffect(() => {
    api.campaigns.list().then(list => setCampaigns(list || [])).catch(err => console.error('[Association] campaigns load failed:', err));
  }, []);
  const isDMorAdmin = user.isAdmin || isAnyDm(campaigns);

  return (
    <div style={{ display:'flex', flexDirection:'column', height:'100vh', overflow:'hidden', fontFamily:"'Nunito',sans-serif" }}>
      {/* Header */}
      <div style={{ padding:'20px 32px 0', background:'linear-gradient(135deg,#1a1208 0%,#111116 60%)', borderBottom:'1px solid #2a2a32', flexShrink:0 }}>
        <div style={{ display:'flex', alignItems:'baseline', gap:14, marginBottom:14 }}>
          <h1 style={{ fontFamily:"'Cinzel',serif", fontSize:22, fontWeight:900, color:'#c9a227', margin:0 }}>Hunters Association</h1>
          <span style={{ fontSize:12, color:'#6b6966' }}>Gyren Headquarters · Vasuin</span>
        </div>
        <div style={{ display:'flex', gap:0, overflowX:'auto' }}>
          {ASSOC_TABS.map(t => (
            <button key={t.id}
              style={{ background:'none', border:'none', borderBottom: tab===t.id ? '2px solid #c9a227' : '2px solid transparent', color: tab===t.id ? '#e8e6e3' : '#6b6966', padding:'10px 18px', cursor:'pointer', fontSize:13, fontWeight:600, fontFamily:"'Nunito',sans-serif", whiteSpace:'nowrap', transition:'color 0.15s' }}
              onClick={() => setTab(t.id)}>{t.label}</button>
          ))}
        </div>
      </div>

      {/* Content */}
      <div style={{ flex:1, overflowY:'auto', padding:'28px 32px' }}>
        {tab === 'guilds'   && <GuildRegistry user={user} isDMorAdmin={isDMorAdmin} setNav={setNav} />}
        {tab === 'requests' && <RequestBoard user={user} isDMorAdmin={isDMorAdmin} />}
        {tab === 'dice'     && <DiceTools />}
        {tab === 'codex'    && <AssocCodex />}
      </div>
    </div>
  );
};

// ── Guild Registry ────────────────────────────────────────────────────────────
const GuildRegistry = ({ user, isDMorAdmin, setNav }) => {
  // Cross-campaign guild list: fan out api.guilds.get over visible campaigns,
  // collect non-404 results. Each campaign has at most one guild.
  const [guilds, setGuilds] = React.useState([]);
  const [campaigns, setCampaigns] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [filterRank, setFilterRank] = React.useState('all');
  const [search, setSearch] = React.useState('');
  const [editing, setEditing] = React.useState(null); // null | 'new' | guild id (which is also campaignId for our routes)
  const [editForm, setEditForm] = React.useState(null);

  const refresh = React.useCallback(async () => {
    setLoading(true);
    try {
      const camps = (await api.campaigns.list()) || [];
      setCampaigns(camps);
      const lists = await Promise.all(camps.map(c =>
        api.guilds.get(c.id).catch(err => err.status === 404 ? null : Promise.reject(err))
      ));
      setGuilds(lists.filter(Boolean));
    } catch (err) {
      console.error('[GuildRegistry] load failed:', err);
    } finally {
      setLoading(false);
    }
  }, []);

  React.useEffect(() => { refresh(); }, [refresh]);

  const visible = guilds.filter(g => {
    const rankOk = filterRank === 'all' || g.rank === filterRank;
    const searchOk = !search || g.name.toLowerCase().includes(search.toLowerCase()) || (g.baseCityName || '').toLowerCase().includes(search.toLowerCase());
    return rankOk && searchOk;
  });

  const startNew = () => {
    // Default campaignId to the first campaign without a guild yet.
    const firstFree = campaigns.find(c => !guilds.some(g => g.campaignId === c.id))?.id || '';
    setEditForm({ name:'', rank:'E', rankPoints:0, baseCityId:'', basePurchased:false, treasury:0, notes:'', campaignId: firstFree });
    setEditing('new');
  };

  const startEdit = (g) => { setEditForm({...g}); setEditing(g.id); };

  const saveGuild = async () => {
    try {
      if (editing === 'new') {
        if (!editForm.campaignId) { window.dialog.alert('Pick a campaign for the new guild.'); return; }
        await api.guilds.create(editForm.campaignId, {
          name: editForm.name, rank: editForm.rank, rankPoints: Number(editForm.rankPoints) || 0,
          baseCityId: editForm.baseCityId || null, basePurchased: !!editForm.basePurchased,
          treasury: Number(editForm.treasury) || 0, notes: editForm.notes || '',
        });
      } else {
        await api.guilds.update(editForm.campaignId, {
          name: editForm.name, rank: editForm.rank, rankPoints: Number(editForm.rankPoints) || 0,
          baseCityId: editForm.baseCityId || null, basePurchased: !!editForm.basePurchased,
          treasury: Number(editForm.treasury) || 0, notes: editForm.notes || '',
        });
      }
      await refresh();
    } catch (err) {
      console.error('[GuildRegistry] save failed:', err);
      window.dialog.alert('Failed to save guild: ' + (err.message || 'unknown error'));
    }
    setEditing(null); setEditForm(null);
  };

  const deleteGuild = async (g) => {
    if (!(await window.dialog.confirm({ title:'Delete guild?', message:`Delete guild "${g.name}"? Members will also be removed.`, danger:true, confirmLabel:'Delete' }))) return;
    try {
      await api.guilds.delete(g.campaignId);
      await refresh();
    } catch (err) {
      console.error('[GuildRegistry] delete failed:', err);
      window.dialog.alert('Failed to delete guild: ' + (err.message || 'unknown error'));
    }
  };

  return (
    <div style={{ maxWidth:1000 }}>
      {/* Toolbar */}
      <div style={{ display:'flex', gap:10, marginBottom:20, flexWrap:'wrap', alignItems:'center' }}>
        <input style={assocStyles.searchInput} placeholder="Search guilds…" value={search} onChange={e=>setSearch(e.target.value)} />
        <select style={assocStyles.select} value={filterRank} onChange={e=>setFilterRank(e.target.value)}>
          <option value="all">All Ranks</option>
          {RANK_ORDER.map(r => <option key={r} value={r}>Rank {r}</option>)}
        </select>
        <div style={{ flex:1 }}></div>
        {isDMorAdmin && <button style={assocStyles.primaryBtn} onClick={startNew}>+ Register Guild</button>}
      </div>

      {/* Stats row */}
      <div style={{ display:'grid', gridTemplateColumns:'repeat(6,1fr)', gap:8, marginBottom:24 }}>
        {RANK_ORDER.map(r => {
          const count = guilds.filter(g => g.rank === r).length;
          return (
            <div key={r} style={{ background:'#1c1c22', border:`1px solid ${RANK_COLOR[r]}44`, borderRadius:8, padding:'10px 12px', textAlign:'center' }}>
              <div style={{ fontFamily:"'Cinzel',serif", fontSize:20, fontWeight:900, color:RANK_COLOR[r] }}>Rank {r}</div>
              <div style={{ fontSize:11, color:'#6b6966', marginTop:2 }}>{count} guild{count!==1?'s':''}</div>
            </div>
          );
        })}
      </div>

      {/* Guild cards */}
      <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
        {loading && guilds.length === 0 && <div style={{color:'#6b6966', fontSize:13, padding:'20px 0', textAlign:'center'}}>Loading guilds…</div>}
        {visible.map(g => {
          const rankColor = RANK_COLOR[g.rank] || '#6b6966';
          const nextRank = RANK_ORDER[RANK_ORDER.indexOf(g.rank)+1];
          const nextThresh = nextRank ? RANK_THRESHOLDS_A[nextRank] : null;
          const prevThresh = RANK_THRESHOLDS_A[g.rank] || 0;
          const progress = nextThresh ? Math.min(1,(g.rankPoints-prevThresh)/(nextThresh-prevThresh)) : 1;
          return (
            <div key={g.id} style={{ background:'#1c1c22', border:'1px solid #2a2a32', borderRadius:10, padding:'18px 22px', display:'grid', gridTemplateColumns:'1fr auto', gap:16, alignItems:'start' }}>
              <div>
                <div style={{ display:'flex', gap:10, alignItems:'center', marginBottom:6 }}>
                  <div style={{ fontFamily:"'Cinzel',serif", fontSize:17, fontWeight:800, color:'#e8e6e3' }}>{g.name}</div>
                  <span style={{ fontSize:12, fontWeight:800, color:rankColor, background:`${rankColor}22`, border:`1px solid ${rankColor}44`, borderRadius:5, padding:'2px 9px' }}>Rank {g.rank}</span>
                  {g.campaignName && <span style={{ fontSize:11, color:'#6b6966', background:'#2a2a32', borderRadius:4, padding:'2px 7px' }}>{g.campaignName}</span>}
                </div>
                <div style={{ fontSize:12, color:'#9a9793', marginBottom:8 }}>Base: {g.baseCityName || (g.basePurchased ? '—' : 'No base')} · {g.members.length} members · {g.rankPoints.toLocaleString()} pts</div>
                {nextRank && (
                  <div style={{ marginBottom:8 }}>
                    <div style={{ height:4, background:'#2a2a32', borderRadius:2, overflow:'hidden', maxWidth:300 }}>
                      <div style={{ height:'100%', width:`${progress*100}%`, background:`linear-gradient(90deg,${rankColor}88,${rankColor})`, borderRadius:2 }}></div>
                    </div>
                    <div style={{ fontSize:10, color:'#4a4a52', marginTop:3 }}>{Math.round(progress*100)}% to Rank {nextRank}</div>
                  </div>
                )}
                <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
                  {g.members.slice(0,5).map(m => (
                    <span key={m.id} style={{ fontSize:11, color:'#9a9793', background:'#111116', borderRadius:4, padding:'2px 7px' }}>{m.name} <span style={{ color:'#6b6966' }}>({m.specLevel[0]})</span></span>
                  ))}
                  {g.members.length > 5 && <span style={{ fontSize:11, color:'#6b6966' }}>+{g.members.length-5} more</span>}
                </div>
              </div>
              <div style={{ display:'flex', gap:8, flexDirection:'column', alignItems:'flex-end' }}>
                {g.campaignId && <button style={assocStyles.outlineBtn} onClick={()=>setNav({view:'campaign',campaignId:g.campaignId,tab:'guild'})}>View Guild Tab →</button>}
                {isDMorAdmin && <button style={assocStyles.outlineBtn} onClick={()=>startEdit(g)}>✎ Edit</button>}
                {isDMorAdmin && <button style={{...assocStyles.outlineBtn,color:'#f87171',borderColor:'rgba(248,113,113,0.2)'}} onClick={()=>deleteGuild(g)}>Delete</button>}
              </div>
            </div>
          );
        })}
        {visible.length === 0 && <div style={{ color:'#4a4a52', textAlign:'center', padding:'40px 0' }}>No guilds match your filters.</div>}
      </div>

      {/* Edit/Create modal */}
      {editing && editForm && (
        <div style={assocStyles.modalOverlay} {...scrimDismiss(()=>{setEditing(null);setEditForm(null);})}>
          <div style={assocStyles.modal} onClick={e=>e.stopPropagation()}>
            <div style={{ fontFamily:"'Cinzel',serif", fontSize:16, fontWeight:800, color:'#c9a227', marginBottom:18 }}>
              {editing==='new' ? 'Register New Guild' : 'Edit Guild'}
            </div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginBottom:12 }}>
              <div><label style={assocStyles.label}>Guild Name<Req/></label><input style={assocStyles.input} value={editForm.name} onChange={e=>setEditForm(p=>({...p,name:e.target.value}))} placeholder="e.g. Ironveil Company"/></div>
              <div><label style={assocStyles.label}>Base City</label>
                <window.CityPicker
                  value={editForm.baseCityId || ''}
                  onChange={v => setEditForm(p => ({...p, baseCityId: v}))}
                  campaignId={editForm.campaignId || null}
                  placeholder="— None —"
                  style={assocStyles.input}
                />
              </div>
              <div><label style={assocStyles.label}>Current Rank</label>
                <select style={assocStyles.select2} value={editForm.rank} onChange={e=>setEditForm(p=>({...p,rank:e.target.value}))}>
                  {RANK_ORDER.map(r=><option key={r} value={r}>Rank {r}</option>)}
                </select>
              </div>
              <div><label style={assocStyles.label}>Rank Points</label><input type="number" style={assocStyles.input} value={editForm.rankPoints} onChange={e=>setEditForm(p=>({...p,rankPoints:Number(e.target.value)}))} /></div>
              <div style={{ gridColumn:'1/-1' }}><label style={assocStyles.label}>Campaign</label>
                <select style={assocStyles.select2} value={editForm.campaignId||''} onChange={e=>setEditForm(p=>({...p,campaignId:e.target.value}))} disabled={editing !== 'new'}>
                  <option value="">— pick a campaign —</option>
                  {campaigns.map(c=><option key={c.id} value={c.id} disabled={editing === 'new' && guilds.some(g=>g.campaignId===c.id)}>{c.name}{guilds.some(g=>g.campaignId===c.id)?' (already has guild)':''}</option>)}
                </select>
              </div>
              <div style={{ gridColumn:'1/-1' }}><label style={assocStyles.label}>Notes</label><textarea style={assocStyles.textarea} rows={3} value={editForm.notes} onChange={e=>setEditForm(p=>({...p,notes:e.target.value}))}/></div>
            </div>
            <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
              <button style={assocStyles.outlineBtn} onClick={()=>{setEditing(null);setEditForm(null);}}>Cancel</button>
              <button style={assocStyles.primaryBtn} onClick={saveGuild} disabled={!editForm.name.trim()}>{editing==='new'?'Register':'Save Changes'}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ── Request Board ─────────────────────────────────────────────────────────────
// API-augmented to expose legacy field names (claimedBy, claimedByGuildName resolved server-side).
const requestFromApi = (q) => q && ({
  ...q,
  claimedBy: q.claimedByGuildId || null,
  claimedByGuildName: q.claimedByGuildName || null,
});

const RequestBoard = ({ user, isDMorAdmin }) => {
  const [requests, setRequests] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [myGuild, setMyGuild] = React.useState(null);
  const [filterStatus, setFilterStatus] = React.useState('all');
  const [filterType, setFilterType] = React.useState('all');
  const [filterRank, setFilterRank] = React.useState('all');
  const [editing, setEditing] = React.useState(null);
  const [editForm, setEditForm] = React.useState(null);
  const [expandedId, setExpandedId] = React.useState(null);

  const refresh = React.useCallback(async () => {
    setLoading(true);
    try {
      const reqs = (await api.requests.list()) || [];
      setRequests(reqs.map(requestFromApi));
      // Find this user's guild across visible campaigns (any guild they're a member of)
      const camps = (await api.campaigns.list()) || [];
      const guilds = await Promise.all(camps.map(c =>
        api.guilds.get(c.id).catch(err => err.status === 404 ? null : Promise.reject(err))
      ));
      const mine = guilds.filter(Boolean).find(g => (g.members || []).some(m => m.playerId === user.id));
      setMyGuild(mine || null);
    } catch (err) {
      console.error('[RequestBoard] load failed:', err);
    } finally {
      setLoading(false);
    }
  }, [user.id]);

  React.useEffect(() => { refresh(); }, [refresh]);

  const visible = requests.filter(r => {
    const s = filterStatus==='all' || r.status===filterStatus;
    const t = filterType==='all' || r.type===filterType;
    const rk = filterRank==='all' || r.minRank===filterRank;
    return s && t && rk;
  });

  const blankForm = { title:'', type:'Hunting', minRank:'D', reward:0, currency:'gp', description:'', giver:'', status:'open', claimedBy:null, campaignId:null, date: new Date().toISOString().slice(0,10), difficulty:'Medium', priority:'medium', column:'unassigned' };

  // Save: campaign-scoped uses /api/campaigns/{id}/quests; global (campaignId=null) uses /api/requests.
  const saveReq = async () => {
    const payload = {
      title: editForm.title, priority: editForm.priority || 'medium', description: editForm.description,
      giver: editForm.giver, reward: Number(editForm.reward) || 0, currency: editForm.currency || 'gp',
      type: editForm.type, minRank: editForm.minRank, difficulty: editForm.difficulty,
      column: editForm.column || (editForm.status === 'completed' ? 'completed' : editForm.claimedBy ? 'guild' : 'unassigned'),
      date: editForm.date,
      claimedByGuildId: editForm.claimedBy || null,
    };
    try {
      if (editing === 'new') {
        if (editForm.campaignId) {
          await api.campaigns.quests.create(editForm.campaignId, payload);
        } else {
          await api.requests.create({ ...payload, campaignId: null });
        }
      } else {
        const cid = editForm.campaignId;
        if (!cid) { window.dialog.alert('Editing global-board requests via this form is not supported yet.'); return; }
        await api.campaigns.quests.update(cid, editing, payload);
      }
      await refresh();
    } catch (err) {
      console.error('[RequestBoard] save failed:', err);
      window.dialog.alert('Failed to save request: ' + (err.message || 'unknown error'));
    }
    setEditing(null); setEditForm(null);
  };

  const claimReq = async (req) => {
    if (!myGuild) return;
    const cid = req.campaignId;
    if (!cid) { window.dialog.alert('Claiming global-board requests is not yet supported.'); return; }
    try {
      await api.campaigns.quests.update(cid, req.id, { status:'claimed', column:'guild', claimedByGuildId: myGuild.id });
      await refresh();
    } catch (err) { console.error('[RequestBoard] claim failed:', err); window.dialog.alert('Failed: ' + (err.message || 'unknown')); }
  };

  const completeReq = async (req) => {
    const cid = req.campaignId;
    if (!cid) { window.dialog.alert('Completing global-board requests is not yet supported.'); return; }
    try {
      await api.campaigns.quests.update(cid, req.id, { status:'completed', column:'completed' });
      await refresh();
    } catch (err) { console.error('[RequestBoard] complete failed:', err); window.dialog.alert('Failed: ' + (err.message || 'unknown')); }
  };

  const deleteReq = async (req) => {
    if (!(await window.dialog.confirm({ title:'Delete request?', message:`Delete "${req.title}"?`, danger:true, confirmLabel:'Delete' }))) return;
    const cid = req.campaignId;
    if (!cid) { window.dialog.alert('Deleting global-board requests is not yet supported.'); return; }
    try {
      await api.campaigns.quests.delete(cid, req.id);
      await refresh();
    } catch (err) { console.error('[RequestBoard] delete failed:', err); window.dialog.alert('Failed: ' + (err.message || 'unknown')); }
  };

  return (
    <div style={{ maxWidth:900 }}>
      {/* Toolbar */}
      <div style={{ display:'flex', gap:8, marginBottom:18, flexWrap:'wrap', alignItems:'center' }}>
        <select style={assocStyles.select} value={filterStatus} onChange={e=>setFilterStatus(e.target.value)}>
          <option value="all">All Statuses</option>
          <option value="open">Open</option>
          <option value="claimed">Claimed</option>
          <option value="completed">Completed</option>
        </select>
        <select style={assocStyles.select} value={filterType} onChange={e=>setFilterType(e.target.value)}>
          <option value="all">All Types</option>
          {REQ_TYPES.map(t=><option key={t} value={t}>{t}</option>)}
        </select>
        <select style={assocStyles.select} value={filterRank} onChange={e=>setFilterRank(e.target.value)}>
          <option value="all">All Ranks</option>
          {RANK_ORDER.map(r=><option key={r} value={r}>Min Rank {r}</option>)}
        </select>
        <div style={{flex:1}}></div>
        <div style={{ fontSize:12, color:'#6b6966' }}>{visible.length} request{visible.length!==1?'s':''}</div>
        {isDMorAdmin && <button style={assocStyles.primaryBtn} onClick={()=>{setEditForm({...blankForm});setEditing('new');}}>+ Post Request</button>}
      </div>

      <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
        {visible.map(req => {
          const rankColor = RANK_COLOR[req.minRank] || '#6b6966';
          const diffColor = DIFF_COLOR[req.difficulty] || '#6b6966';
          const statusColor = REQ_STATUS_COLOR[req.status] || '#6b6966';
          const isExpanded = expandedId === req.id;
          // claimingGuild name comes pre-resolved from the API DTO.
          const claimingGuildName = req.claimedByGuildName;
          const canClaim = req.status==='open' && myGuild && RANK_ORDER.indexOf(myGuild.rank)>=RANK_ORDER.indexOf(req.minRank);
          const isClaimer = req.claimedBy && myGuild && req.claimedBy===myGuild.id;
          return (
            <div key={req.id} style={{ background:'#1c1c22', border:`1px solid ${req.status==='completed'?'#2a2a32':req.status==='claimed'?'#b4530933':'#2a2a32'}`, borderRadius:9, overflow:'hidden' }}>
              <div style={{ padding:'14px 18px', cursor:'pointer', display:'flex', gap:12, alignItems:'center' }} onClick={()=>setExpandedId(isExpanded?null:req.id)}>
                <div style={{ flexShrink:0, width:36, height:36, borderRadius:7, background:`${rankColor}20`, border:`1px solid ${rankColor}44`, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:"'Cinzel',serif", fontSize:13, fontWeight:900, color:rankColor }}>
                  {req.minRank}
                </div>
                <div style={{ flex:1 }}>
                  <div style={{ display:'flex', gap:8, alignItems:'center', marginBottom:3, flexWrap:'wrap' }}>
                    <span style={{ fontSize:14, fontWeight:700, color: req.status==='completed'?'#6b6966':'#e8e6e3' }}>{req.title}</span>
                    <span style={{ fontSize:10, fontWeight:700, color:diffColor, background:`${diffColor}18`, border:`1px solid ${diffColor}33`, borderRadius:4, padding:'1px 7px', textTransform:'uppercase', letterSpacing:'0.06em' }}>{req.difficulty}</span>
                    <span style={{ fontSize:10, color:'#6b6966', background:'#2a2a32', borderRadius:4, padding:'1px 7px' }}>{req.type}</span>
                  </div>
                  <div style={{ fontSize:11, color:'#6b6966' }}>
                    {req.giver} · {req.date}
                    {req.status==='claimed' && claimingGuildName && <span style={{ color:'#b45309', marginLeft:8 }}>· Claimed by {claimingGuildName}</span>}
                  </div>
                </div>
                <div style={{ textAlign:'right', flexShrink:0 }}>
                  <div style={{ fontSize:15, fontWeight:800, color:'#c9a227' }}>{req.reward.toLocaleString()} {req.currency}</div>
                  <div style={{ fontSize:10, fontWeight:700, color:statusColor, textTransform:'uppercase', letterSpacing:'0.08em', marginTop:2 }}>● {req.status}</div>
                </div>
              </div>

              {isExpanded && (
                <div style={{ padding:'0 18px 14px', borderTop:'1px solid #2a2a32' }}>
                  <p style={{ fontSize:13, color:'#c5c3c0', lineHeight:1.7, margin:'12px 0' }}>{req.description}</p>
                  <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
                    {canClaim && <button style={assocStyles.primaryBtn} onClick={()=>claimReq(req)}>Claim Request</button>}
                    {isClaimer && req.status==='claimed' && <button style={{...assocStyles.primaryBtn,background:'rgba(30,107,60,0.2)',borderColor:'#1e6b3c'}} onClick={()=>completeReq(req)}>Mark Completed</button>}
                    {isDMorAdmin && <button style={assocStyles.outlineBtn} onClick={()=>{setEditForm({...req});setEditing(req.id);}}>✎ Edit</button>}
                    {isDMorAdmin && <button style={{...assocStyles.outlineBtn,color:'#f87171',borderColor:'rgba(248,113,113,0.2)'}} onClick={()=>deleteReq(req)}>Delete</button>}
                  </div>
                </div>
              )}
            </div>
          );
        })}
        {visible.length===0 && <div style={{ color:'#4a4a52', textAlign:'center', padding:'40px 0' }}>No requests match the current filters.</div>}
      </div>

      {/* Edit modal */}
      {editing && editForm && (
        <div style={assocStyles.modalOverlay} {...scrimDismiss(()=>{setEditing(null);setEditForm(null);})}>
          <div style={{ ...assocStyles.modal, maxWidth:560 }} onClick={e=>e.stopPropagation()}>
            <div style={{ fontFamily:"'Cinzel',serif", fontSize:15, fontWeight:800, color:'#c9a227', marginBottom:16 }}>{editing==='new'?'Post New Request':'Edit Request'}</div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:10 }}>
              <div style={{gridColumn:'1/-1'}}><label style={assocStyles.label}>Title<Req/></label><input style={assocStyles.input} value={editForm.title} onChange={e=>setEditForm(p=>({...p,title:e.target.value}))} placeholder="Request title"/></div>
              <div><label style={assocStyles.label}>Type</label><select style={assocStyles.select2} value={editForm.type} onChange={e=>setEditForm(p=>({...p,type:e.target.value}))}>{REQ_TYPES.map(t=><option key={t} value={t}>{t}</option>)}</select></div>
              <div><label style={assocStyles.label}>Min Rank</label><select style={assocStyles.select2} value={editForm.minRank} onChange={e=>setEditForm(p=>({...p,minRank:e.target.value}))}>{RANK_ORDER.map(r=><option key={r} value={r}>Rank {r}</option>)}</select></div>
              <div><label style={assocStyles.label}>Reward (gp)</label><input type="number" style={assocStyles.input} value={editForm.reward} onChange={e=>setEditForm(p=>({...p,reward:Number(e.target.value)}))}/></div>
              <div><label style={assocStyles.label}>Difficulty</label><select style={assocStyles.select2} value={editForm.difficulty} onChange={e=>setEditForm(p=>({...p,difficulty:e.target.value}))}>{['Easy','Medium','Hard','Legendary'].map(d=><option key={d} value={d}>{d}</option>)}</select></div>
              <div><label style={assocStyles.label}>Quest Giver</label><input style={assocStyles.input} value={editForm.giver} onChange={e=>setEditForm(p=>({...p,giver:e.target.value}))}/></div>
              <div><label style={assocStyles.label}>Status</label><select style={assocStyles.select2} value={editForm.status} onChange={e=>setEditForm(p=>({...p,status:e.target.value}))}><option value="open">Open</option><option value="claimed">Claimed</option><option value="completed">Completed</option></select></div>
              <div style={{gridColumn:'1/-1'}}><label style={assocStyles.label}>Description</label><textarea style={assocStyles.textarea} rows={4} value={editForm.description} onChange={e=>setEditForm(p=>({...p,description:e.target.value}))}/></div>
            </div>
            <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
              <button style={assocStyles.outlineBtn} onClick={()=>{setEditing(null);setEditForm(null);}}>Cancel</button>
              <button style={assocStyles.primaryBtn} onClick={saveReq} disabled={!editForm.title.trim()}>{editing==='new'?'Post':'Save'}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ── Dice Tools ────────────────────────────────────────────────────────────────
const DiceTools = () => {
  const [alchLevel, setAlchLevel] = React.useState('Apprentice');
  const [alchRolls, setAlchRolls] = React.useState([]);
  const [herbLevel, setHerbLevel] = React.useState('Apprentice');
  const [herbPoints, setHerbPoints] = React.useState(null);
  const [herbBrews, setHerbBrews] = React.useState([]);
  const [diceLog, setDiceLog] = React.useState([]);

  const ALCH_EFFECTS = [
    'Comprehension — Understand all spoken languages for 1 minute',
    'Rest — Cannot be magically slept for 8h; stay awake during long rest',
    'Climbing — Gain climbing speed equal to walking speed for 1 min',
    'Resistance — Gain resistance to a random elemental type for 1 min',
    'Friendship — Advantage on Animal Handling & Persuasion for 1 min',
    'Growth — Enlarge/Reduce effect for 1 minute (your choice)',
    'Spell Power — Next damage spell (4th or lower) deals max damage',
    'Invulnerability — Resistant to all damage types for 5 rounds',
    'Speed — Haste for 5 rounds',
    'Flying — Fly speed = walk speed + hover for 1 minute',
    'Strength — Strength becomes 30 for 5 rounds',
    'Size — Become Huge, triple melee damage for 5 rounds',
  ];

  const ALCH_DICE = { Apprentice:6, Journeyman:8, Master:10, Grandmaster:12 };
  const HERB_PROF = { Apprentice:1, Journeyman:2, Master:3, Grandmaster:4 };

  const HERB_BREWS = [
    { pts:1, name:'Minor Healing Brew', effect:'Restore 1d4 HP to a willing creature within 30 ft' },
    { pts:2, name:'Healing Brew', effect:'Restore 2d4 HP to a willing creature within 30 ft' },
    { pts:3, name:'Greater Healing Brew', effect:'Restore 3d4+3 HP to a willing creature within 30 ft' },
    { pts:4, name:'Superior Healing Brew', effect:'Restore 4d4+4 HP to a willing creature within 30 ft' },
    { pts:5, name:'Supreme Healing Brew', effect:'Restore 5d4+10 HP — also removes one condition' },
    { pts:2, name:'Spell Slot Brew (1st)', effect:'Recover one 1st-level spell slot' },
    { pts:3, name:'Spell Slot Brew (2nd)', effect:'Recover one 2nd-level spell slot' },
    { pts:4, name:'Spell Slot Brew (3rd)', effect:'Recover one 3rd-level spell slot' },
  ];

  const rollAlch = () => {
    const sides = ALCH_DICE[alchLevel];
    const prof = HERB_PROF[alchLevel];
    const rolls = Array.from({length:prof}, () => {
      const r = Math.floor(Math.random()*sides)+1;
      return { roll: r, effect: ALCH_EFFECTS[r-1] };
    });
    setAlchRolls(rolls);
    setDiceLog(prev => [{ time: new Date().toLocaleTimeString(), label:`Alch (${alchLevel}) — d${sides}×${prof}`, results: rolls.map(r=>`${r.roll}: ${r.effect.split('—')[0].trim()}`) }, ...prev].slice(0,20));
  };

  const rollHerbPoints = () => {
    const prof = HERB_PROF[herbLevel];
    let total = 0;
    const dieRolls = [];
    for (let i=0;i<prof;i++) { const r=Math.floor(Math.random()*4)+1; total+=r; dieRolls.push(r); }
    const pts = total + prof*4;
    setHerbPoints(pts);
    setHerbBrews([]);
    setDiceLog(prev=>[{time:new Date().toLocaleTimeString(),label:`Herb Points (${herbLevel})`,results:[`Rolled ${dieRolls.join('+')} + ${prof*4} = ${pts} brew points`]},...prev].slice(0,20));
  };

  const rollD = (sides) => {
    const r = Math.floor(Math.random()*sides)+1;
    setDiceLog(prev=>[{time:new Date().toLocaleTimeString(),label:`d${sides}`,results:[`Rolled: ${r}`]},...prev].slice(0,20));
  };

  return (
    <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:24, maxWidth:900 }}>
      {/* Alchemist Roller */}
      <div style={{ display:'flex', flexDirection:'column', gap:16 }}>
        <div style={{ background:'#1c1c22', border:'1px solid #2a2a32', borderRadius:10, padding:'20px 22px' }}>
          <div style={{ fontFamily:"'Cinzel',serif", fontSize:15, fontWeight:700, color:'#c9a227', marginBottom:14 }}>⚗ Alchemist Potions</div>
          <div style={{ fontSize:12, color:'#9a9793', marginBottom:14, lineHeight:1.6 }}>
            Roll a d6→d12 based on level to determine random potion effects. Roll count equals your profession modifier.
          </div>
          <div style={{ display:'flex', gap:8, alignItems:'center', marginBottom:14 }}>
            <label style={assocStyles.label}>Level</label>
            <select style={assocStyles.select2} value={alchLevel} onChange={e=>{setAlchLevel(e.target.value);setAlchRolls([]);}}>
              {['Apprentice','Journeyman','Master','Grandmaster'].map(l=><option key={l} value={l}>{l} (d{ALCH_DICE[l]})</option>)}
            </select>
          </div>
          <button style={{...assocStyles.primaryBtn, width:'100%', justifyContent:'center'}} onClick={rollAlch}>
            🎲 Roll {HERB_PROF[alchLevel]}×d{ALCH_DICE[alchLevel]}
          </button>
          {alchRolls.length>0 && (
            <div style={{ marginTop:14, display:'flex', flexDirection:'column', gap:8 }}>
              {alchRolls.map((r,i) => (
                <div key={i} style={{ background:'#111116', borderRadius:8, padding:'10px 12px', borderLeft:`3px solid #7c3aed` }}>
                  <div style={{ display:'flex', gap:8, alignItems:'center', marginBottom:4 }}>
                    <span style={{ fontFamily:"'Cinzel',serif", fontSize:18, fontWeight:900, color:'#7c3aed', minWidth:28 }}>{r.roll}</span>
                    <span style={{ fontSize:13, fontWeight:700, color:'#e8e6e3' }}>{r.effect.split('—')[0].trim()}</span>
                  </div>
                  <div style={{ fontSize:11, color:'#9a9793' }}>{r.effect.split('—').slice(1).join('—').trim()}</div>
                </div>
              ))}
            </div>
          )}
        </div>

        {/* Herbalist Brew Planner */}
        <div style={{ background:'#1c1c22', border:'1px solid #2a2a32', borderRadius:10, padding:'20px 22px' }}>
          <div style={{ fontFamily:"'Cinzel',serif", fontSize:15, fontWeight:700, color:'#1e6b3c', marginBottom:14 }}>🌿 Herbalist Brew Planner</div>
          <div style={{ display:'flex', gap:8, alignItems:'center', marginBottom:14 }}>
            <label style={assocStyles.label}>Level</label>
            <select style={assocStyles.select2} value={herbLevel} onChange={e=>{setHerbLevel(e.target.value);setHerbPoints(null);setHerbBrews([]);}}>
              {['Apprentice','Journeyman','Master','Grandmaster'].map(l=><option key={l} value={l}>{l} (+{HERB_PROF[l]})</option>)}
            </select>
          </div>
          <button style={{...assocStyles.primaryBtn,background:'rgba(30,107,60,0.2)',borderColor:'#1e6b3c',color:'#4ade80', width:'100%',justifyContent:'center'}} onClick={rollHerbPoints}>
            🎲 Roll Daily Brew Points
          </button>
          {herbPoints !== null && (
            <div style={{ marginTop:14 }}>
              <div style={{ textAlign:'center', fontSize:32, fontWeight:900, fontFamily:"'Cinzel',serif", color:'#4ade80', marginBottom:12 }}>{herbPoints} pts</div>
              <div style={{ fontSize:11, color:'#6b6966', marginBottom:10 }}>Available brews (click to plan):</div>
              <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                {HERB_BREWS.filter(b=>b.pts<=herbPoints).map((b,i) => {
                  const count = herbBrews.filter(x=>x===b.name).length;
                  const spent = herbBrews.reduce((acc,name) => { const brew=HERB_BREWS.find(x=>x.name===name); return acc+(brew?.pts||0); },0);
                  const canAdd = spent+b.pts<=herbPoints;
                  return (
                    <div key={i} style={{ display:'flex', alignItems:'center', gap:8, background:'#111116', borderRadius:7, padding:'8px 10px' }}>
                      <span style={{ fontSize:11, fontWeight:800, color:'#4ade80', minWidth:20 }}>{b.pts}p</span>
                      <div style={{ flex:1 }}>
                        <div style={{ fontSize:12, fontWeight:700, color:'#e8e6e3' }}>{b.name}</div>
                        <div style={{ fontSize:10, color:'#9a9793' }}>{b.effect}</div>
                      </div>
                      <div style={{ display:'flex', gap:4, alignItems:'center', flexShrink:0 }}>
                        <button style={{ background:'none',border:'1px solid #3a3a42',color:'#6b6966',borderRadius:4,width:22,height:22,cursor:'pointer',fontSize:13,lineHeight:1 }} onClick={()=>setHerbBrews(prev=>prev.filter((x,idx)=>!(x===b.name&&idx===prev.lastIndexOf(b.name))))}>−</button>
                        <span style={{ fontSize:13,fontWeight:700,color:'#e8e6e3',minWidth:16,textAlign:'center' }}>{count}</span>
                        <button style={{ background: canAdd?'rgba(30,107,60,0.2)':'none',border:`1px solid ${canAdd?'#1e6b3c':'#2a2a32'}`,color:canAdd?'#4ade80':'#3a3a42',borderRadius:4,width:22,height:22,cursor:canAdd?'pointer':'default',fontSize:13,lineHeight:1 }} onClick={()=>canAdd&&setHerbBrews(prev=>[...prev,b.name])}>+</button>
                      </div>
                    </div>
                  );
                })}
              </div>
              {herbBrews.length>0 && (
                <div style={{ marginTop:10, padding:'8px 12px', background:'rgba(30,107,60,0.08)', border:'1px solid #1e6b3c44', borderRadius:7 }}>
                  <div style={{ fontSize:10,fontWeight:800,color:'#4ade80',marginBottom:4 }}>TODAY'S PLAN</div>
                  {[...new Set(herbBrews)].map(name => {
                    const count=herbBrews.filter(x=>x===name).length;
                    return <div key={name} style={{fontSize:12,color:'#c5c3c0'}}>×{count} {name}</div>;
                  })}
                  <div style={{ fontSize:11,color:'#4ade80',marginTop:4 }}>
                    {herbBrews.reduce((a,n)=>a+(HERB_BREWS.find(x=>x.name===n)?.pts||0),0)} / {herbPoints} pts used
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
      </div>

      {/* Quick Dice + Log */}
      <div style={{ display:'flex', flexDirection:'column', gap:16 }}>
        <div style={{ background:'#1c1c22', border:'1px solid #2a2a32', borderRadius:10, padding:'20px 22px' }}>
          <div style={{ fontFamily:"'Cinzel',serif", fontSize:15, fontWeight:700, color:'#e8e6e3', marginBottom:14 }}>🎲 Quick Dice</div>
          <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:8 }}>
            {[4,6,8,10,12,20,100].map(d => (
              <button key={d} style={{ background:'#111116',border:'1px solid #3a3a42',color:'#c9a227',borderRadius:8,padding:'12px 8px',cursor:'pointer',fontSize:14,fontWeight:800,fontFamily:"'Cinzel',serif",transition:'background 0.12s' }}
                onClick={()=>rollD(d)}>d{d}</button>
            ))}
            <button style={{ background:'#111116',border:'1px solid #3a3a42',color:'#9a9793',borderRadius:8,padding:'12px 8px',cursor:'pointer',fontSize:12,fontWeight:700 }}
              onClick={()=>setDiceLog([])}>Clear</button>
          </div>
        </div>

        <div style={{ background:'#1c1c22', border:'1px solid #2a2a32', borderRadius:10, padding:'20px 22px', flex:1 }}>
          <div style={{ fontFamily:"'Cinzel',serif", fontSize:13, fontWeight:700, color:'#6b6966', marginBottom:12 }}>Roll Log</div>
          {diceLog.length===0 && <div style={{ fontSize:12,color:'#3a3a42',textAlign:'center',padding:'20px 0' }}>No rolls yet.</div>}
          <div style={{ display:'flex', flexDirection:'column', gap:6, maxHeight:400, overflowY:'auto' }}>
            {diceLog.map((entry,i) => (
              <div key={i} style={{ background:'#111116', borderRadius:6, padding:'8px 10px' }}>
                <div style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
                  <span style={{ fontSize:11,fontWeight:700,color:'#9a9793' }}>{entry.label}</span>
                  <span style={{ fontSize:10,color:'#4a4a52' }}>{entry.time}</span>
                </div>
                {entry.results.map((r,j) => <div key={j} style={{ fontSize:12,color:'#c5c3c0' }}>{r}</div>)}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

// ── Codex ─────────────────────────────────────────────────────────────────────
const CODEX_SECTIONS = [
  {
    id: 'intro', title: 'Introduction', icon: '🌍',
    entries: [
      { title: 'Kamia — The Plane of Hunters', body: 'Kamia is a vast and diverse world filled with untold adventures and mysteries. It is a continent with a rich history and diverse cultures, where the line between magic and technology is blurred. The power of nature is balanced with the might of civilization, where ancient ruins and forgotten dungeons lie hidden amidst towering cities and lush forests.\n\nThe denizens of Kamia face numerous dangers: fierce beasts that roam the wilds, cunning bandits, and marauding monsters. To meet these challenges, a group of hunters banded together to form the Hunters Association, unified from its central headquarters in Gyren.' },
      { title: 'The Continents', body: 'Vasuin — The largest explored continent. Cities scattered far and wide, connected by many roads. Many races live here together in relative peace. Home of Gyren, the largest city and HQ of the Hunters Association.\n\nAeropera — A smaller continent to the north of Vasuin. The most recently explored continent, where only wildlife was found.\n\nOpane — A continent to the east. Fairly simple with not much threatening wildlife. Where most hunters move after retiring.\n\nUnexplored continents also exist, awaiting pioneer guilds.' },
    ]
  },
  {
    id: 'guilds', title: 'Guild System', icon: '⚔',
    entries: [
      { title: 'What are Guilds?', body: 'Guilds are groups of hunters registered under the Hunters Association. Before Guilds existed, hunters fought in unorganized bands. The Hunters Association was formed in Gyren to unify and centralize hunter efforts.\n\nGuilds choose a base of operations (usually near a city). The Association builds a branch office in the nearest city for information relaying. Guild leaders pick up requests from branch offices, paying a fee to secure exclusive claim on the reward. An Association specialist accompanies teams to report status.' },
      { title: 'Requests', body: 'A request to the Association can be anything. Not all Guilds can pick up any request — simpler gathering requests go to low-ranking Guilds while difficult hunting and investigation requests require higher ranks.\n\nTypical requests include: Gathering herbs · Mining ores · Hunting wildlife for materials · Hunting wildlife during outbreaks · Bandit hunting · Slaying dangerous large wildlife · Defending a city · Investigation of events · Political assistance · Rescue missions · Cave/dungeon exploration · Crafting requested items.' },
      { title: 'Request Completion', body: 'After a team completes a mission they report back to the association or quest giver, then claim their reward. The team receives the posted reward, and the same amount is deposited into the Guild\'s coffers for upgrading and upkeep.\n\nWildlife corpses and materials found can be taken by the team to be dismantled. Materials go to Guild storage. Completing requests earns credits that can be used to request equipment from Guild Craftsmen.' },
      { title: 'Guild Leadership', body: 'A Guild needs leaders — usually the group that registers it. A Guild also appoints a spokesperson who functions as the voice of the Guild to the outside world. The spokesperson leads discussions with other Guild leaders but has no more authority or privileges.\n\nAll specialized members hired from the Association are sworn loyal to the Guild. Laying them off is out of the question once hired.' },
      { title: 'Guild Ranks', body: 'Rank E — 10+ total points\nRank D — 50+ total points\nRank C — 250+ total points\nRank B — 1,000+ total points\nRank A — 5,000+ total points\nRank S — 15,000+ total points\n\nRank E: Base of operations, max 10 members.\nRank D: Basic facilities, Apprentice specialists, team leaders, max 20.\nRank C: Improved facilities, Expeditions, Journeyman specialists, max 40.\nRank B: Master specialists, Commanders, Squads, Mining operations, max 80.\nRank A: Advanced facilities, Grandmaster specialists, Additional base, Large explorations, max 160.\nRank S: Pioneer a new city, max 320 members.' },
    ]
  },
  {
    id: 'facilities', title: 'Facilities', icon: '🏛',
    entries: [
      { title: 'Basic Facilities', body: 'Reception — Main hall. Desk, chairs, visitor log. Required before any other facility.\n\nCrafting Facility — Large hall with anvils, workbenches, forges, tanning stations. Up to 4 craftsmen simultaneously. Fits Blacksmiths, Leatherworkers, Stonemasons, Woodworkers.\n\nButchery Facility — Open well-kept room with reinforced table, ceiling hooks, knife sets, ice bin for preservation. Up to 4 butchers.\n\nCook\'s Kitchen — Large enough for 3 cooks. Sink, oven, furnace, cabinets, preservation bin. Merchant can supply basic ingredients.\n\nSleeping Quarters — Houses members without their own home in the city. Required to employ merchants, scouts, and recruiters.' },
      { title: 'Improved Facilities', body: 'Improved Crafting — Larger hall, more specialized tools. Up to 10 craftsmen. Can now work on carts.\n\nImproved Butchery — Expanded room + large built-in bone saw. Up to 8 butchers.\n\nImproved Sleeping Quarters — More rooms, temporary guest rooms with privacy and lockable chests. Required to send out guild-assigned parties.\n\nMarket Stand — Portable stand for selling surplus goods at Gyren market. Not viable in other cities.\n\nApothecary — Complex facility near a library. Glass vials, containers, burners for potions and poisons.' },
      { title: 'Advanced Facilities', body: 'Advanced Crafting — Top-tier tools, any crafting type imaginable. Up to 20 craftsmen. Can work on large boats and siege weaponry.\n\nAdvanced Butchery — Additional room for gargantuan creatures. Up to 16 butchers. Includes a pocket-dimension arctic storage room (connects with Restaurant\'s Kitchen if both are built).\n\nAdvanced Sleeping Quarters — Small individual rooms + communal squad rooms. Required to have Squads in your Guild.\n\nAdvanced Market Stand — Packs into a portable cart via command word. Pocket dimension stores goods. Can be taken to any city.\n\nAdvanced Apothecary — Even more complex. Can create liquid medicines, tablets, drops, and creams.\n\nRestaurant\'s Kitchen — 12 cooks simultaneously. Connects to arctic pocket-dimension storage.\n\nDining Hall — Seats 80+ for dinner service. Menu ordering or full feasts. Doubles as presentation/meeting space.\n\nGrand Library — Managed by a hired librarian. Members can request books on any subject. Can be opened to the public for renown.' },
    ]
  },
  {
    id: 'specs', title: 'Specializations', icon: '⚒',
    entries: [
      { title: 'About Specializations', body: 'As a member or leader of a guild, you may specialize in a line of work. Specializations let you perform additional actions during roleplay and grant advantages or bonuses on certain checks. They can also be performed as downtime activities.\n\nYour specialization should align with your backstory or character\'s interests. Guild Leaders may pick up to two specializations but cannot actively perform both on the same day.\n\nAll specializations start at Apprentice and progress through the campaign based on your efforts.\n\nCommon features for all specializations:\n• Apprentice+: +profession modifier +2 bonus to specialized tool checks\n• Journeyman+: Improved results based on profession modifier\n• Master+: Half the usual time for specialized actions\n• Grandmaster+: Advantage on all related skill checks' },
      { title: 'Blacksmith', body: 'Required: Smith\'s tools\n\nApprentice+: Spend 10 min to improve a metal melee weapon\'s damage by your profession modifier. Up to [mod] weapons, lasts 1 day.\nJourneyman+: Once/day — 10 min during short rest to optimize [half mod rounded up] metal melee weapons; deal extra 1d4 damage per proficiency mod above 1. Lasts 1 day.\nMaster+: Spend 10 min to improve AC of armor you made by [half mod rounded down]. 1 item, lasts 1 day.\nGrandmaster+: Cast Create Construct (metal only) without a spell slot, twice/day.' },
      { title: 'Leatherworker / Tailor', body: 'Required: Leatherworker\'s tools (or Weaver\'s tools for Tailor)\n\nApprentice+: Improve leather/cloth boots by +5ft walking speed (max 30ft) for [mod] pairs, lasts 1 day.\nJourneyman+: Once/day — calibrate a bow/crossbow; next 3 attacks gain +[mod] to attack roll.\nMaster+: Improve AC of armor you made by [half mod down]. 1 item, lasts 1 day.\nGrandmaster+: Improve up to 4 armors you made — wearers gain Pass Without Trace, lose disadvantage on stealth. On detection: Longstrider for 1 min.' },
      { title: 'Woodworker', body: 'Required: Carpenter\'s tools\n\nApprentice+: Improve wood melee or ranged weapon damage by [mod]. Up to [mod] weapons, lasts 1 day.\nJourneyman+: Once/day — craft 3 knockback arrowheads. Targets hit must make DC 15 DEX save or be pushed 5ft and knocked prone. Expire end of day.\nMaster+: Improve AC of wood shield by [half mod down]. 1 item, lasts 1 day.\nGrandmaster+: Control a pocket dimension containing one wooden creation. Summon it as an action at any time.' },
      { title: 'Stonemason', body: 'Required: Mason\'s tools\n\nApprentice+: Cast Mending on stone objects (not limited to small objects). If already known, target a 10ft cube.\nJourneyman+: Cast Mold Earth on stone. If already known, increase area to 10ft cube. Excavating stone takes 10 min per 5ft and isn\'t instantaneous.\nMaster+: Cast Stone Shape without spell slot [half mod down] times/day.\nGrandmaster+: Cast Wall of Stone without spell slot twice/day. Can cast as reaction to a ranged attack to provide cover.' },
      { title: 'Alchemist', body: 'Required: Alchemist\'s tools\n\nApprentice+: Craft [mod] random potions at start of day. Use the Alchemist table (d6 at Apprentice, scaling up). Potions spoil next day.\nJourneyman+: Identify any potion by smell and movement. Learn some ingredients.\nMaster+: Share a potion\'s effects to a willing creature within 30ft as an action. Duration is halved. Cannot be used on healing/mana potions.\nGrandmaster+: Once/day, create a potion with effects of a 7th or lower level spell (target: self). Expires next day.' },
      { title: 'Herbalist', body: 'Required: Herbalist\'s tools\n\nApprentice+: Each morning, gain a brew point pool = (1d4 + mod) × 4, rolling [mod] d4s. Use points to craft healing brews from the Herbalist table. Brews spoil next day.\nJourneyman+: Spend 10 min during short rest to apply a healing brew to any willing creature within 30ft. Effect is halved.\nMaster+: Once/week — cast Awaken on plants without spell slot. Plant is friendly.\nGrandmaster+: Once/week — brew a magical panacea. Target regains all HP, loses all negative conditions, benefits from a long rest, and recovers all dawn-reset features. If prone, springs up immediately.' },
      { title: 'Beastmaster', body: 'Required: Animal Handling\n\nApprentice+: Spend 10 min during short rest to heal a friendly beast [mod]d8 HP.\nJourneyman+: Encourage beasts within 30ft — they gain [mod]d8 temp HP, double pull weight, double travel distance. Lasts 1 day.\nMaster+: Once/week — cast Awaken on beasts without spell slot. Beast is friendly.\nGrandmaster+: Once/day — request help from a legendary beast you\'re friendly with. Appears at your side, acts on your initiative, follows commands. Returns when dismissed.' },
      { title: 'Enchanter', body: 'Required: Tinkerer\'s tools\n\nApprentice+: Imbue mundane items with a selection of effects for 1 day (see Enchanter table).\nJourneyman+: Passively detect the presence of magic items at a glance. Cast Detect Magic without spell slot [mod] times/day.\nMaster+: Cast Animate Objects without spell slot [half mod down] times/day.\nGrandmaster+: Once/week — embed one magical item\'s effects onto another magical item (including attunements, limitations, and curses). An item cannot contain more than 2 embedded items.' },
      { title: 'Butcher', body: 'Required: Cook\'s tools\n\nApprentice+: When failing a harvesting check, reroll it. Use [mod] times/day.\nJourneyman+: Spend 10 min to automatically succeed on a harvesting check. [Half mod up] times/day.\nMaster+: On harvesting, get extra harvesting checks equal to [half mod down].\nGrandmaster+: Once/day — forgo all harvesting checks on a target and succeed at all of them, gaining maximum materials. Takes 1 hour.' },
      { title: 'Cook', body: 'Required: Cook\'s tools\n\nApprentice+: Meals grant temporary HP = [mod]d4 + [mod] to all allies.\nJourneyman+: Make [mod]d4 treats per meal. Eaten as bonus action for same temp HP. Expire end of day.\nMaster+: Meals boost maximum HP instead of granting temp HP. Treats still grant temp HP.\nGrandmaster+: Spend 1 min to conjure a pocket kitchen with basic functionality. Stored items persist between uses.' },
      { title: 'Scout', body: 'Required: Cartographer\'s tools\n\nApprentice+: Scout ahead of the party once/day for information on a request.\nJourneyman+: Cast Detect Traps without spell slot [mod] times/day.\nMaster+: Cast Clairvoyance without spell slot [half mod down] times/day.\nGrandmaster+: Once/day — cast Invisibility without spell slot targeting all allies.' },
      { title: 'Recruiter', body: 'Required: Calligrapher\'s tools\n\nApprentice+: When failing a persuasion check to recruit, reroll it. [Mod] times/day.\nJourneyman+: Investigation checks below 10 can be taken as 10 + modifiers. [Half mod down] times/day.\nMaster+: Spend 10 min to give a compelling speech in a town/village, increasing chance of recruits appearing. Once/day/location.\nGrandmaster+: Immune to charmed condition. Can recognize when others are charmed. Can snap others out of charmed as bonus action. Gain Cutting Words cantrip (or increase targets to 2).' },
      { title: 'Receptionist', body: 'Required: Calligrapher\'s tools\n\nApprentice+: Communicate without speech with creatures you don\'t share a language with.\nJourneyman+: Investigation checks below 10 can be taken as 10 + modifiers. [Half mod down] times/day.\nMaster+: Gain proficiency in all languages (excluding class-based ones like Thieves\' Cant).\nGrandmaster+: Always maintain Telepathic Bond with any Guild Leader of your guild, ignoring initial range requirement.' },
      { title: 'Trader', body: 'Required: Calligrapher\'s tools\n\nApprentice+: Advantage on saves to resist distortion of goods\' value.\nJourneyman+: Spend 10 min to conjure 2 horses and a cart (like Find Steed). Dismissed or destroyed — cannot be called for 1 week.\nMaster+: Cast Teleport without spell slot [half mod down] times/day. Limited to familiar towns on your current continent. Bring up to 16 willing allies.\nGrandmaster+: Once/week — spend 10 min to contact an entity from another plane for trade. Entities believe you are from their realm.' },
      { title: 'Fisher', body: 'Required: Survival\n\nApprentice+: Conjure a Pole of Angling and Hook of Fisher\'s Delight. Dismiss as action.\nJourneyman+: Cast Shape Water. If already known, maintain up to 4 non-instantaneous effects simultaneously; water flow increases to 10ft.\nMaster+: Spend 10 min to conjure a small rowboat in nearby water. Dismiss as action. Destroyed — cannot call for 1 week.\nGrandmaster+: Breathe underwater, gain swim speed = 2× walking speed. Allies within 120ft share this. Telepathically communicate with fish.' },
    ]
  },
  {
    id: 'crafting', title: 'Crafting & Cooking', icon: '🔨',
    entries: [
      { title: 'Collecting Materials', body: 'Materials come from three sources:\n\nFauna — Creatures and wildlife. Materials are harvested through dismantling. A Butcher specialist improves harvesting outcomes significantly.\n\nFlora — Plants, herbs, and fungi. Requires harvesting checks based on the plant type.\n\nHarvesting Flora — Spend time in the field or maintain a private herbalist garden (Herbalist specialization). Success depends on the region and season.\n\nAll gathered materials go into the Guild\'s storage and can be used by craftsmen or traded using credits.' },
      { title: 'Crafting', body: 'Crafting in Kamia uses harvested materials combined with artisan skills. The quality of crafted items depends on:\n\n• The level of the craftsman (Apprentice through Grandmaster)\n• The quality of materials used\n• The facility available (Basic → Improved → Advanced)\n• Special or magical materials may imbue crafted items with unique properties\n\nCredits from completing requests can be exchanged for items crafted by Guild artisans. Custom requests may take extended time depending on base materials.' },
      { title: 'Cooking', body: 'The Cook specialization allows preparing meals during downtime, short rests, and long rests.\n\nMeal effects depend on your Cook level:\n• Apprentice: Allies gain temp HP = [mod]d4 + [mod]\n• Journeyman: Also craft [mod]d4 treats (bonus action to consume for same temp HP)\n• Master: Meals boost max HP instead of granting temp HP\n• Grandmaster: Pocket kitchen — conjure a full kitchen in 1 minute\n\nIngredients affect meal power. Higher quality ingredients from harvested fauna and flora produce superior effects. A Cook\'s Kitchen is required for full functionality; the Restaurant\'s Kitchen allows 12 cooks to prepare large feasts.' },
    ]
  },
  {
    id: 'character', title: 'Character Creation', icon: '🧝',
    entries: [
      { title: 'Creating a Character in Kamia', body: 'Characters in Kamia follow the standard D&D 5.5e rules with the following Kamia-specific additions:\n\n• Every character may pick one Specialization at the start, beginning at Apprentice level\n• Characters are encouraged to have a role within the Guild (even if they are primarily a hunter)\n• Ability scores and class choices follow standard 5.5e rules' },
      { title: 'Ability Score Priorities', body: 'The following characteristics are recommended for a balanced party:\n\n• At least one character with high Strength or Constitution for frontline roles\n• At least one character with high Intelligence or Wisdom for investigation and knowledge checks\n• At least one character with high Charisma for the Guild Spokesperson role\n• Dexterity is broadly useful for scouts and ranged hunters\n\nThe DM sets DC values for team assignments based on the composition and skill of the team.' },
      { title: 'Glossary', body: 'Association — The Hunters Association, the central governing body of guilds.\nBase of operations — The Guild\'s home building and facilities.\nBranch office — Association office near a Guild\'s base city, used to relay requests.\nCredits — Internal Guild currency earned by completing requests, used to request crafted gear.\nGuild coffers — Treasury of the Guild, funded by request rewards.\nGuildmaster — The leading figure(s) of a Guild.\nRequest — A mission posted to the Association board.\nSpecialization — A craft or skill role a character commits to.\nTeam — A self-steering group of up to 5 hunters led by a team leader.\nSquad — A group of up to 40 hunters led by a Commander (requires Rank B+).' },
    ]
  },
];

const AssocCodex = () => {
  const [activeSection, setActiveSection] = React.useState('intro');
  const [activeEntry, setActiveEntry] = React.useState(null);
  const [search, setSearch] = React.useState('');

  const section = CODEX_SECTIONS.find(s=>s.id===activeSection);
  const filteredEntries = search
    ? CODEX_SECTIONS.flatMap(s=>s.entries.map(e=>({...e,sectionId:s.id,sectionTitle:s.title}))).filter(e=>e.title.toLowerCase().includes(search.toLowerCase())||e.body.toLowerCase().includes(search.toLowerCase()))
    : null;

  return (
    <div style={{ display:'grid', gridTemplateColumns:'200px 1fr', gap:0, maxWidth:1000, border:'1px solid #2a2a32', borderRadius:10, overflow:'hidden', height:'70vh' }}>
      {/* Sidebar */}
      <div style={{ background:'#13131a', borderRight:'1px solid #2a2a32', display:'flex', flexDirection:'column', overflow:'hidden' }}>
        <div style={{ padding:'10px 12px', borderBottom:'1px solid #2a2a32' }}>
          <input style={{ ...assocStyles.searchInput, width:'100%', fontSize:12, padding:'5px 8px' }} placeholder="Search codex…" value={search} onChange={e=>{setSearch(e.target.value);setActiveEntry(null);}} />
        </div>
        <div style={{ overflowY:'auto', flex:1, padding:'8px 6px' }}>
          {!search && CODEX_SECTIONS.map(s=>(
            <button key={s.id} style={{ display:'flex', alignItems:'center', gap:8, width:'100%', background: activeSection===s.id?'rgba(201,162,39,0.1)':'none', border:'none', borderLeft: activeSection===s.id?'2px solid #c9a227':'2px solid transparent', color: activeSection===s.id?'#e8e6e3':'#9a9793', padding:'8px 10px', cursor:'pointer', textAlign:'left', fontFamily:"'Nunito',sans-serif", fontSize:13, fontWeight:600, borderRadius:4, marginBottom:2 }}
              onClick={()=>{setActiveSection(s.id);setActiveEntry(null);}}>
              <span>{s.icon}</span>{s.title}
            </button>
          ))}
        </div>
      </div>

      {/* Content */}
      <div style={{ display:'flex', flexDirection:'column', overflow:'hidden' }}>
        {search ? (
          <div style={{ overflowY:'auto', flex:1, padding:'20px 24px' }}>
            <div style={{ fontSize:11,color:'#6b6966',marginBottom:12 }}>{filteredEntries.length} results for "{search}"</div>
            {filteredEntries.map((e,i)=>(
              <div key={i} style={{ marginBottom:20 }}>
                <div style={{ fontSize:10,color:'#c9a227',textTransform:'uppercase',letterSpacing:'0.1em',marginBottom:4 }}>{e.sectionTitle}</div>
                <div style={{ fontSize:15,fontFamily:"'Cinzel',serif",fontWeight:700,color:'#e8e6e3',marginBottom:8 }}>{e.title}</div>
                <p style={{ fontSize:13,color:'#c5c3c0',lineHeight:1.75,whiteSpace:'pre-line',margin:0 }}>{e.body}</p>
              </div>
            ))}
            {filteredEntries.length===0&&<div style={{color:'#4a4a52',textAlign:'center',padding:'40px 0'}}>No results found.</div>}
          </div>
        ) : activeEntry === null ? (
          <div style={{ overflowY:'auto', flex:1, padding:'20px 24px' }}>
            <div style={{ fontFamily:"'Cinzel',serif", fontSize:20, fontWeight:800, color:'#c9a227', marginBottom:4 }}>{section?.icon} {section?.title}</div>
            <div style={{ height:1, background:'#2a2a32', marginBottom:16 }}></div>
            <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
              {section?.entries.map((e,i)=>(
                <button key={i} style={{ background:'#111116', border:'1px solid #2a2a32', borderRadius:8, padding:'14px 16px', cursor:'pointer', textAlign:'left', fontFamily:"'Nunito',sans-serif", transition:'border-color 0.15s' }}
                  onClick={()=>setActiveEntry(i)}>
                  <div style={{ fontSize:14,fontWeight:700,color:'#e8e6e3',marginBottom:4 }}>{e.title}</div>
                  <div style={{ fontSize:12,color:'#6b6966',lineHeight:1.5 }}>{e.body.split('\n')[0].slice(0,120)}…</div>
                </button>
              ))}
            </div>
          </div>
        ) : (
          <div style={{ overflowY:'auto', flex:1, padding:'20px 24px' }}>
            <button style={{ background:'none',border:'none',color:'#c9a227',cursor:'pointer',fontSize:12,fontWeight:700,fontFamily:"'Nunito',sans-serif",marginBottom:14,padding:0 }}
              onClick={()=>setActiveEntry(null)}>← Back to {section?.title}</button>
            <div style={{ fontFamily:"'Cinzel',serif", fontSize:20, fontWeight:800, color:'#e8e6e3', marginBottom:16 }}>{section?.entries[activeEntry]?.title}</div>
            <p style={{ fontSize:14, color:'#c5c3c0', lineHeight:1.85, whiteSpace:'pre-line', margin:0 }}>{section?.entries[activeEntry]?.body}</p>
          </div>
        )}
      </div>
    </div>
  );
};

// ── Styles ────────────────────────────────────────────────────────────────────
const assocStyles = {
  searchInput: { background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 12px', fontSize:13, outline:'none', fontFamily:"'Nunito',sans-serif", minWidth:200 },
  select: { background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 10px', fontSize:12, cursor:'pointer', fontFamily:"'Nunito',sans-serif" },
  select2: { width:'100%', background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 10px', fontSize:13, cursor:'pointer', fontFamily:"'Nunito',sans-serif" },
  primaryBtn: { background:'rgba(201,162,39,0.15)', border:'1px solid #c9a227', color:'#c9a227', borderRadius:6, padding:'7px 16px', cursor:'pointer', fontSize:13, fontWeight:700, fontFamily:"'Nunito',sans-serif", display:'flex', alignItems:'center', gap:6 },
  outlineBtn: { background:'none', border:'1px solid #3a3a42', color:'#9a9793', borderRadius:6, padding:'6px 12px', cursor:'pointer', fontSize:12, fontWeight:600, fontFamily:"'Nunito',sans-serif" },
  label: { fontSize:10, fontWeight:800, color:'#6b6966', textTransform:'uppercase', letterSpacing:'0.08em', display:'block', marginBottom:4 },
  input: { width:'100%', background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 10px', fontSize:13, outline:'none', fontFamily:"'Nunito',sans-serif", boxSizing:'border-box' },
  textarea: { width:'100%', background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'8px 10px', fontSize:13, outline:'none', fontFamily:"'Nunito',sans-serif", resize:'vertical', boxSizing:'border-box', lineHeight:1.5 },
  modalOverlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.7)', zIndex:1000, display:'flex', alignItems:'center', justifyContent:'center', backdropFilter:'blur(4px)' },
  modal: { background:'#1c1c22', border:'1px solid #3a3a42', borderRadius:12, padding:'24px 28px', width:'100%', maxWidth:500, maxHeight:'85vh', overflowY:'auto', boxShadow:'0 20px 60px rgba(0,0,0,0.8)' },
};

Object.assign(window, { AssociationView, assocStyles });
