// app.jsx — Tally views. Depends on core.jsx (loaded first; globals on window).
const { useState, useEffect, useMemo, useRef } = React;
const {
  C, uid, r2, equalSplit, fixRound, CATS, catEmoji, catName, load, materializeDue, splitDeltas, isMonetaryEdit,
  involvedOf, requiredApprovers, approvalOf, effectiveEntry, editDiffs, baseEntries,
  netBalances, simplify, globalPairwise, consolidated, pendingForMe, relDate, describeEntry, describeEdit,
  Avatar, AvatarStack, Btn, Sheet, Input, Label, chipStyle, Empty, PageHead, ApprovalPill, Arrow, Mark,
} = window;

const ME = 'me';
const memOf = (g, id) => g.members.find(m => m.id === id) || { id, name: (typeof id === 'string' && id.length > 12) ? ('Member ' + id.slice(0, 4)) : (id === 'me' ? 'You' : (id || '?')) };
// add an expense from editor output, materializing a recurring rule if requested
function addExpenseFrom(dispatch, gid, d) {
  const { repeat, ...exp } = d;
  if (repeat && repeat !== 'none') {
    const rid = uid();
    dispatch('ExpenseAdded', gid, { expenseId: uid(), recurringId: rid, ...exp });
    dispatch('RecurringAdded', gid, { recurringId: rid, desc: exp.desc, amount: exp.amount, paidBy: exp.paidBy, split: exp.split, category: exp.category, note: exp.note, items: exp.items, frequency: repeat, anchorTs: Date.now() });
  } else {
    dispatch('ExpenseAdded', gid, { expenseId: uid(), ...exp });
  }
}
// active recurring rule + next occurrence date for an expense, or null
function recurringInfo(group, eff) {
  if (!eff || !eff.recurringId || !group.recurring) return null;
  const rule = group.recurring.find(r => r.id === eff.recurringId);
  if (!rule || !rule.active) return null;
  const occs = baseEntries(group).filter(e => e.type === 'expense' && effectiveEntry(group, e).recurringId === eff.recurringId).map(e => e.date);
  const last = occs.length ? Math.max(...occs) : rule.anchorTs;
  return { freq: rule.frequency, next: window.recurStep(last, rule.frequency), rule };
}
const shortDate = ts => new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: new Date(ts).getFullYear() === new Date().getFullYear() ? undefined : 'numeric' });

// reusable per-person shares pills for an (effective) expense
function SharePills({ group, eff, cur, indent = 0 }) {
  const payerId = eff.paidBy;
  const owers = Object.keys(eff.split || {}).filter(id => id !== payerId);
  return (
    <div style={{ marginTop: 10, paddingLeft: indent, display: 'flex', gap: 14 }}>
      <div style={{ minWidth: 0, flexShrink: 0 }}>
        <div style={{ fontSize: 10.5, fontWeight: 800, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 5 }}>Paid by</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Avatar member={memOf(group, payerId)} size={18} />
          <span style={{ fontSize: 12.5, fontWeight: 700, color: C.ink }}>{memOf(group, payerId).name}</span>
        </div>
        <div style={{ fontSize: 13, fontWeight: 800, color: C.green, marginTop: 3 }}>{cur}{eff.amount.toFixed(2)}</div>
      </div>
      <div style={{ width: 1, background: C.line, alignSelf: 'stretch' }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 10.5, fontWeight: 800, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 5 }}>Owes</div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px 6px' }}>
          {owers.length === 0 ? <span style={{ fontSize: 12, color: C.faint }}>just {memOf(group, payerId).name}</span> : owers.map(id => (
            <span key={id} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: C.soft, borderRadius: 999, padding: '3px 9px 3px 4px' }}>
              <Avatar member={memOf(group, id)} size={16} />
              <span style={{ fontSize: 12, fontWeight: 600, color: C.ink }}>{memOf(group, id).name}</span>
              <span style={{ fontSize: 12, fontWeight: 800, color: C.ink }}>{cur}{Number(eff.split[id] || 0).toFixed(2)}</span>
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

// trust badge — reflects whether the entry's signer has a verified key
function TrustDot({ verified, size = 16 }) {
  if (verified) return (
    <span title="Signed by a verified member" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: size, height: size, borderRadius: '50%', background: `${C.green}1a`, flexShrink: 0 }}>
      <svg width={size * 0.62} height={size * 0.62} viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="3.2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
    </span>
  );
  return (
    <span title="Signer's key isn't verified yet" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: size, height: size, borderRadius: '50%', background: `${C.orange}1f`, flexShrink: 0 }}>
      <svg width={size * 0.58} height={size * 0.58} viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.orangeDk} strokeWidth="2.6" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1.1" fill={C.orangeDk} /></svg>
    </span>
  );
}

// deterministic safety code (Signal-style) so a member's code is stable across views
const SAFE_WORDS = ['anchor', 'maple', 'cobalt', 'otter', 'velvet', 'ember', 'harbor', 'cedar', 'quartz', 'meadow', 'falcon', 'indigo', 'pebble', 'willow', 'garnet', 'sable'];
function pubKeyHex(seed) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
  let out = '';
  for (let i = 0; i < 16; i++) { h ^= h << 13; h >>>= 0; h ^= h >>> 17; h ^= h << 5; h >>>= 0; out += (h & 0xff).toString(16).padStart(2, '0'); }
  return (out.match(/.{1,4}/g) || []).join(' ');
}
function safetyCode(seedStr) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < seedStr.length; i++) { h ^= seedStr.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
  const out = [];
  for (let i = 0; i < 6; i++) {
    h ^= h << 13; h >>>= 0; h ^= h >>> 17; h ^= h << 5; h >>>= 0;
    const w = SAFE_WORDS[(h >>> 3) % SAFE_WORDS.length];
    const n = (h & 0xff).toString(16).padStart(2, '0');
    out.push([n, w]);
  }
  return out;
}

// home approval tile — your confirmation checkbox + the value that matters to you
function RecentRow({ group, E, cur, onOpen, onToggle }) {
  const eff = effectiveEntry(group, E);
  const settle = E.type === 'settle';
  const mineReq = requiredApprovers(E, group).includes(ME);
  const confirmed = !!(E.approvals || {})[ME];
  const disputed = E.dispute && E.dispute.status === 'open';
  const sl = stakeLine(group, E, cur);
  const title = settle ? `${memOf(group, eff.from).name} → ${memOf(group, eff.to).name}` : `${catEmoji(eff.category)} ${eff.desc || 'Untitled'}`;
  const accent = disputed ? C.red : (mineReq && !confirmed) ? C.orange : C.line;
  return (
    <div onClick={onOpen} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '11px 13px', border: `1px solid ${C.line}`, borderLeft: `3px solid ${accent}`, borderRadius: 14, cursor: 'pointer', background: C.bg }}>
      {mineReq && !confirmed
        ? <button onClick={e => { e.stopPropagation(); onToggle(); }} aria-label="Confirm your share" style={{ flexShrink: 0, width: 30, height: 30, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <span style={{ display: 'block', width: 24, height: 24, borderRadius: '50%', border: `2px solid ${C.orange}` }} />
          </button>
        : <span style={{ flexShrink: 0, width: 30, height: 30 }} />}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ fontSize: 14.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</span>
          {disputed && <span style={{ flexShrink: 0, display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, fontWeight: 800, color: C.red, background: `${C.red}14`, padding: '1px 6px 1px 4px', borderRadius: 5 }}><svg width="9" height="9" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.red} strokeWidth="2.6" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1.1" fill={C.red} /></svg>disputed</span>}
        </div>
        <div style={{ fontSize: 11.5, color: C.faint, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{group.emoji} {group.name} · {shortDate(E.date)}</div>
      </div>
      <div style={{ flexShrink: 0, textAlign: 'right' }}>
        {sl
          ? <div style={{ fontSize: 14.5, fontWeight: 800, color: sl[1] }}>{sl[0].charAt(0).toUpperCase() + sl[0].slice(1)}</div>
          : <div style={{ fontSize: 12.5, color: C.faint }}>{memOf(group, settle ? eff.from : eff.paidBy).name} paid {cur}{eff.amount.toFixed(2)}</div>}
        {mineReq && !confirmed && <div style={{ fontSize: 10.5, fontWeight: 700, color: C.orangeDk, marginTop: 2 }}>awaiting you</div>}
      </div>
    </div>
  );
}

// standardized expense row/card — matches the Totals "who paid & shares" presentation
function ExpenseCard({ group, E, cur, onClick, showGroup, hideWaiting, stakeOnly }) {
  const eff = effectiveEntry(group, E);
  const settle = E.type === 'settle';
  const sl = stakeOnly ? stakeLine(group, E, cur) : null;
  const a = approvalOf(E, group);
  const ap = E.approvals || {};
  const pendingIds = a.req.filter(id => !ap[id]);
  const disputed = E.dispute && E.dispute.status === 'open';
  const accent = eff.voided ? C.faint : disputed ? C.red : a.done ? C.green : C.orange;
  const payer = settle ? memOf(group, eff.from) : memOf(group, eff.paidBy);
  const rec = recurringInfo(group, eff);
  return (
    <div onClick={onClick} style={{ border: `1px solid ${C.line}`, borderLeft: `3px solid ${accent}`, borderRadius: 14, padding: '11px 13px', cursor: 'pointer', opacity: eff.voided ? 0.55 : 1 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ width: 7, height: 7, borderRadius: '50%', background: accent, flexShrink: 0 }} />
        <span style={{ flex: 1, minWidth: 0, fontSize: 14.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textDecoration: eff.voided ? 'line-through' : 'none' }}>{settle ? `${payer.name} → ${memOf(group, eff.to).name}` : `${catEmoji(eff.category)} ${eff.desc || 'Untitled'}`}</span>
        {showGroup && <span style={{ fontSize: 11.5, color: C.faint, flexShrink: 0 }}>· {group.emoji}</span>}
        {eff.amended && <span style={{ fontSize: 10, fontWeight: 700, color: C.blue, background: `${C.blue}12`, padding: '1px 5px', borderRadius: 5, flexShrink: 0 }}>edited</span>}
        {disputed && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, fontWeight: 800, color: C.red, background: `${C.red}14`, padding: '1px 6px 1px 4px', borderRadius: 5, flexShrink: 0 }}><svg width="9" height="9" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.red} strokeWidth="2.6" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1.1" fill={C.red} /></svg>disputed</span>}
        <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 6, flexShrink: 0, whiteSpace: 'nowrap' }}>
          <Avatar member={payer} size={18} />
          <span style={{ fontSize: 13, fontWeight: 800, color: C.green }}>{payer.name} paid {cur}{eff.amount.toFixed(2)}</span>
        </span>
        {E.by !== ME && <TrustDot verified={!!memOf(group, E.by).verified} />}
      </div>
      <div style={{ marginLeft: 15, marginTop: 3, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <span style={{ fontSize: 11.5, color: C.faint }}>{shortDate(E.date)}</span>
        {rec && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10.5, fontWeight: 700, color: C.blue, background: `${C.blue}10`, padding: '1px 7px 1px 5px', borderRadius: 999 }}><svg width="11" height="11" viewBox="0 0 24 24"><path d="M4 7h11m0 0l-3-3m3 3l-3 3M20 17H9m0 0l3-3m-3 3l3 3" stroke={C.blue} strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>{rec.freq} · next {shortDate(rec.next)}</span>}
      </div>
      {!hideWaiting && !eff.voided && !a.done && (
        <div style={{ marginTop: 8, marginLeft: 15, display: 'flex', alignItems: 'center', gap: 4 }}>
          <svg width="11" height="11" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke={C.orange} strokeWidth="2.2" fill="none" /><path d="M12 7v5l3 2" stroke={C.orange} strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
          <span style={{ fontSize: 10.5, fontWeight: 700, color: C.orangeDk }}>waiting</span>
          <span style={{ display: 'inline-flex' }}>{pendingIds.slice(0, 3).map((id, i) => <span key={id} style={{ marginLeft: i ? -5 : 0, borderRadius: '50%', boxShadow: '0 0 0 1.5px #fff', display: 'inline-flex' }}><Avatar member={memOf(group, id)} size={16} /></span>)}</span>
        </div>
      )}
      {stakeOnly && sl && (
        <div style={{ marginLeft: 15, marginTop: 6 }}>
          <span style={{ fontSize: 14.5, fontWeight: 800, color: sl[1] }}>{sl[0].charAt(0).toUpperCase() + sl[0].slice(1)}</span>
        </div>
      )}
      {!settle && !stakeOnly && <SharePills group={group} eff={eff} cur={cur} indent={15} />}
    </div>
  );
}

// ── App shell + routing ───────────────────────────────────────
function App() {
  const [events, setEvents] = useState(() => { const ev = load(); const due = materializeDue(ev, Date.now()); return due.length ? [...ev, ...due] : ev; });
  const [view, setView] = useState({ name: 'home' });
  useEffect(() => { localStorage.setItem(window.EKEY, JSON.stringify({ events })); }, [events]);
  const db = useMemo(() => window.project(events), [events]);

  // ── sync wiring (T2) ──────────────────────────────────────────
  // Inbound merge: peer events arrive via the channel and are appended to the
  // SAME log, deduped by id so project() stays the single source of truth.
  const eventIdsRef = useRef(null);
  if (eventIdsRef.current === null) eventIdsRef.current = new Set(events.map(e => e.id));
  const eventsRef = useRef(events);
  useEffect(() => { eventsRef.current = events; }, [events]);
  useEffect(() => {
    const sync = window.TallyGroupSync;
    if (!window.TALLY_SYNC_ENABLED || !sync) return;
    sync.attach({
      hasEvent: (id) => eventIdsRef.current.has(id),
      getEvents: () => eventsRef.current,
      addEvent: (tallyEvent) => setEvents(es => {
        if (eventIdsRef.current.has(tallyEvent.id)) return es;   // idempotent
        eventIdsRef.current.add(tallyEvent.id);
        return [...es, tallyEvent];
      }),
    });
    // DEV: inject local-only test data (fake same-named members + expenses) so the
    // disambiguation / remove UI can be exercised without creating real accounts.
    // Usage from the browser console: TALLY_seedTestData()  → reload not needed.
    // Local only — never synced, never hits the backend.
    window.TALLY_seedTestData = () => {
      const now = Date.now();
      const mk = (type, streamId, data, actor, ts) => ({ id: uid(), seq: 0, ts: ts || now, actor: actor || 'me', type, streamId, data: data || {} });
      const batch = [];
      // Group 1: two "Alex" + one "Sam", with expenses
      const g1 = 'test_' + uid();
      const alex1 = uid(), alex2 = uid(), sam = uid();
      batch.push(mk('GroupCreated', g1, { name: 'Test · Same Names', emoji: '🧪' }, 'me', now - 5000));
      batch.push(mk('MemberAdded', g1, { memberId: alex1, name: 'Alex', email: 'alex.rivera@gmail.com' }, 'me', now - 4900));
      batch.push(mk('MemberAdded', g1, { memberId: alex2, name: 'Alex', email: 'alex.tan@outlook.com' }, 'me', now - 4800));
      batch.push(mk('MemberAdded', g1, { memberId: sam, name: 'Sam', email: 'sam@work.io' }, 'me', now - 4700));
      batch.push(mk('ExpenseAdded', g1, { expenseId: uid(), desc: 'Dinner', amount: 120, paidBy: alex1, split: { me: 30, [alex1]: 30, [alex2]: 30, [sam]: 30 }, category: 'food' }, 'me', now - 4000));
      batch.push(mk('ExpenseAdded', g1, { expenseId: uid(), desc: 'Taxi', amount: 40, paidBy: sam, split: { me: 20, [sam]: 20 }, category: 'transport' }, 'me', now - 3000));
      // Group 2: a couple of removable members (no expenses) to test remove
      const g2 = 'test_' + uid();
      const jo = uid(), pat = uid();
      batch.push(mk('GroupCreated', g2, { name: 'Test · Removable', emoji: '🧹' }, 'me', now - 2000));
      batch.push(mk('MemberAdded', g2, { memberId: jo, name: 'Jo', email: 'jo@x.com' }, 'me', now - 1900));
      batch.push(mk('MemberAdded', g2, { memberId: pat, name: 'Pat', email: 'pat@x.com' }, 'me', now - 1800));
      batch.push(mk('ExpenseAdded', g2, { expenseId: uid(), desc: 'Snacks', amount: 10, paidBy: 'me', split: { me: 5, [jo]: 5 }, category: 'food' }, 'me', now - 1000));
      // Jo has an expense (not removable); Pat has none (removable). Add a 3rd with none.
      const dana = uid();
      batch.push(mk('MemberAdded', g2, { memberId: dana, name: 'Dana', email: 'dana@x.com' }, 'me', now - 900));
      batch.forEach(ev => eventIdsRef.current.add(ev.id));
      setEvents(es => [...es, ...batch]);
      console.log('[Tally] Injected local test data: 2 groups, same-named members, removable members. Local only — not synced.');
      return 'ok';
    };
    return () => sync.closeAll && sync.closeAll();
  }, []);

  // the UI never mutates state — it appends an event to the log
  const dispatch = (type, streamId, data, actor = ME) => {
    // when synced, the local account authors under its pubkey so peers attribute
    // the event correctly; offline it stays 'me'. (project() renders both as You.)
    const sync = window.TALLY_SYNC_ENABLED ? window.TallyGroupSync : null;
    const loggedIn = !!(window.TallySync && window.TallySync.myAuthorId);
    // author under pubkey if this group is synced OR is being created while synced
    const willSync = sync && loggedIn && (sync.isSynced(streamId) || type === 'GroupCreated');
    const selfActor = (willSync && actor === ME) ? window.TallySync.myAuthorId : actor;
    const ev = { id: uid(), seq: events.length + 1, ts: Date.now(), actor: selfActor, type, streamId, data: data || {} };
    eventIdsRef.current.add(ev.id);
    setEvents(es => [...es, ev]);
    if (sync) {
      // a new synced group must be registered server-side before its events flow
      if (type === 'GroupCreated') { sync.enableForGroup(streamId).then(() => sync.onLocalAppend(ev)).catch(e => console.warn('enableForGroup', e)); }
      else sync.onLocalAppend(ev);
    }
  };
  const go = v => setView(v);
  const home = () => setView({ name: 'home' });
  const group = view.name === 'group' ? db.groups.find(g => g.id === view.id) : null;
  const expGroup = view.name === 'expense' ? db.groups.find(g => g.id === view.gid) : null;
  const TABS = ['home', 'expenses', 'totals', 'people', 'activity'];
  const showTabs = TABS.includes(view.name);

  return (
    <div style={{ maxWidth: 460, margin: '0 auto', minHeight: '100vh', background: C.bg, position: 'relative', boxShadow: '0 0 60px rgba(0,0,0,0.07)' }}>
      <div style={{ paddingBottom: showTabs ? 'calc(64px + env(safe-area-inset-bottom))' : 0 }}>
        {view.name === 'home' && <Home state={db} dispatch={dispatch} go={go} />}
        {view.name === 'group' && group && <GroupView group={group} cur={db.currency} dispatch={dispatch} go={go} back={home} state={db} />}
        {(view.name === 'activity' || view.name === 'ledger') && <ActivityHub state={db} events={events} cur={db.currency} go={go} back={home} rootTab={showTabs} />}
        {view.name === 'people' && <PeoplePage state={db} cur={db.currency} go={go} back={home} rootTab={showTabs} />}
        {view.name === 'pending' && <PendingConfirmations state={db} cur={db.currency} go={go} back={home} dispatch={dispatch} />}
        {view.name === 'payments' && <PaymentsPage state={db} cur={db.currency} go={go} back={home} />}
        {view.name === 'expenses' && <ExpensesPage state={db} cur={db.currency} go={go} />}
        {view.name === 'person' && <PersonDetail state={db} cur={db.currency} personName={view.person} dispatch={dispatch} go={go} back={() => setView(view.back || { name: 'people' })} />}
        {view.name === 'expense' && <ExpenseDetail group={expGroup} eid={view.eid} cur={db.currency} dispatch={dispatch} back={() => setView(view.back || { name: 'activity' })} />}
        {view.name === 'totals' && <GroupTotalsPage state={db} cur={db.currency} initialGid={view.gid} go={go} back={home} rootTab={showTabs} dispatch={dispatch} />}
        {(view.name === 'group' && !group) && <Home state={db} dispatch={dispatch} go={go} />}
      </div>
      {showTabs && <TabBar active={view.name} go={go} />}
    </div>
  );
}

// ── Bottom tab bar ────────────────────────────────────────────
function TabBar({ active, go }) {
  const tabs = [
    ['home', 'Home', <path d="M3 10.5L12 3l9 7.5M5 9.5V20h14V9.5" stroke="currentColor" strokeWidth="1.9" fill="none" strokeLinecap="round" strokeLinejoin="round" />],
    ['expenses', 'Expenses', <g><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor" strokeWidth="1.8" fill="none" /><path d="M8 8h8M8 12h8M8 16h5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" /></g>],
    ['totals', 'Totals', <g><rect x="3" y="3.5" width="18" height="17" rx="2" stroke="currentColor" strokeWidth="1.9" fill="none" /><path d="M3 9h18M10 9v11.5" stroke="currentColor" strokeWidth="1.7" /></g>],
    ['people', 'People', <g><circle cx="9" cy="8" r="3.2" stroke="currentColor" strokeWidth="1.9" fill="none" /><path d="M3 19c0-3.3 2.7-6 6-6s6 2.7 6 6" stroke="currentColor" strokeWidth="1.9" fill="none" strokeLinecap="round" /><circle cx="17" cy="8.5" r="2.4" stroke="currentColor" strokeWidth="1.6" fill="none" opacity="0.6" /></g>],
    ['activity', 'Activity', <g><path d="M6 4h12a1 1 0 011 1v15l-3-2-2 2-2-2-2 2-2-2-3 2V5a1 1 0 011-1z" stroke="currentColor" strokeWidth="1.8" fill="none" strokeLinejoin="round" /><path d="M9 9h6M9 12.5h6" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" /></g>],
  ];
  return (
    <div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, maxWidth: 460, margin: '0 auto', background: 'rgba(255,255,255,0.92)', backdropFilter: 'blur(12px)', borderTop: `1px solid ${C.line}`, display: 'flex', padding: '6px 8px calc(8px + env(safe-area-inset-bottom))', zIndex: 50 }}>
      {tabs.map(([id, label, icon]) => {
        const on = active === id;
        return (
          <button key={id} onClick={() => go({ name: id })} style={{ flex: 1, background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3, padding: '6px 0', color: on ? C.blue : C.faint }}>
            <svg width="23" height="23" viewBox="0 0 24 24">{icon}</svg>
            <span style={{ fontSize: 10.5, fontWeight: 700 }}>{label}</span>
          </button>
        );
      })}
    </div>
  );
}

// ── Activity hub: Ledger timeline / Audit table ───────────────
function ActivityHub({ state, events, cur, go, back, rootTab }) {
  const [mode, setMode] = useState('timeline');
  return (
    <div>
      <PageHead title="Activity" sub="Every event · append-only" back={rootTab ? undefined : back} />
      <div style={{ padding: '12px 20px 0' }}>
        <div style={{ display: 'flex', gap: 4, background: C.soft, borderRadius: 11, padding: 3 }}>
          {[['timeline', 'Timeline'], ['table', 'Table']].map(([k, lbl]) => (
            <button key={k} onClick={() => setMode(k)} style={{ flex: 1, height: 36, borderRadius: 8, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, background: mode === k ? C.bg : 'transparent', color: mode === k ? C.ink : C.sub, boxShadow: mode === k ? '0 1px 3px rgba(0,0,0,0.08)' : 'none' }}>{lbl}</button>
          ))}
        </div>
      </div>
      {mode === 'timeline' ? <Ledger state={state} cur={cur} go={go} embedded /> : <Activity state={state} events={events} embedded />}
    </div>
  );
}

// ── Home ──────────────────────────────────────────────────────
function Home({ state, dispatch, go }) {
  const [addG, setAddG] = useState(false);
  const [gName, setGName] = useState('');
  const [gMembers, setGMembers] = useState([]);
  const [showArchived, setShowArchived] = useState(false);
  const [menu, setMenu] = useState(false);
  const [addExp, setAddExp] = useState(false);
  const [pay, setPay] = useState(false);
  const [totSel, setTotSel] = useState('all');
  const [simp, setSimp] = useState(false);
  const [showAllPairs, setShowAllPairs] = useState(false);
  const [expandRecent, setExpandRecent] = useState(false);
  const overall = useMemo(() => {
    let owed = 0, owe = 0;
    state.groups.forEach(g => { if (g.archived) return; const b = netBalances(g)[ME] || 0; if (b > 0) owed += b; else owe += -b; });
    return { owed, owe, net: owed - owe };
  }, [state]);
  const pendingNow = useMemo(() => pendingForMe(state, ME), [state]);
  // keep items the user just approved visible (greyed) until they navigate away & back
  const keyOf = p => p.type === 'groupdel' ? `gd-${p.g.id}` : `e-${p.g.id}-${p.e.id}`;
  const [approvedKeys, setApprovedKeys] = useState({});
  const seenRef = useRef({ order: [], byKey: {} });
  const sk = seenRef.current;
  pendingNow.forEach(p => { const k = keyOf(p); if (!(k in sk.byKey)) sk.order.push(k); sk.byKey[k] = p; });
  const pendingDisplay = sk.order
    .filter(k => approvedKeys[k] || pendingNow.some(p => keyOf(p) === k))
    .map(k => ({ k, p: sk.byKey[k], done: !!approvedKeys[k] }));
  // expense/payment confirmations now live in the Recent list (checkboxes);
  // this section only carries change-requests: edits, deletions, group removals
  const requestDisplay = pendingDisplay.filter(x => x.p.type === 'groupdel' || (x.p.e && (x.p.e.type === 'edit' || x.p.e.type === 'void')));
  const approveNow = p => { setApprovedKeys(a => ({ ...a, [keyOf(p)]: true })); if (p.type === 'groupdel') approveDel(p.g.id); else approveEntry(p.g.id, p.e.id); };
  const pairs = useMemo(() => globalPairwise(state), [state]);
  const recentApprovals = useMemo(() => {
    const out = [];
    state.groups.forEach(g => { if (g.archived) return; baseEntries(g).forEach(e => { if (e.type !== 'expense' && e.type !== 'settle') return; if (effectiveEntry(g, e).voided) return; const mineReq = requiredApprovers(e, g).includes(ME); const confirmed = !!(e.approvals || {})[ME]; out.push({ g, e, pendingMine: mineReq && !confirmed }); }); });
    out.sort((a, b) => (a.pendingMine === b.pendingMine ? b.e.date - a.e.date : a.pendingMine ? -1 : 1));
    return out;
  }, [state]);
  const unconfirmedCount = recentApprovals.filter(r => r.pendingMine).length;
  const disputed = useMemo(() => {
    const out = [];
    state.groups.forEach(g => { if (g.archived) return; baseEntries(g).forEach(e => { if (e.dispute && e.dispute.status === 'open' && !effectiveEntry(g, e).voided) out.push({ g, e }); }); });
    return out.sort((a, b) => b.e.dispute.date - a.e.dispute.date);
  }, [state]);
  const totals = useMemo(() => {
    const gs = state.groups.filter(g => !g.archived && (totSel === 'all' || g.id === totSel));
    const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
    const names = ['You']; gs.forEach(g => g.members.forEach(m => { if (!names.includes(m.name)) names.push(m.name); }));
    const tPaid = {}, tOwed = {}; names.forEach(n => { tPaid[n] = 0; tOwed[n] = 0; });
    gs.forEach(g => baseEntries(g).forEach(e => { if (e.type !== 'expense') return; const eff = effectiveEntry(g, e); if (eff.voided) return; const payer = nameOf(g, eff.paidBy); tPaid[payer] = (tPaid[payer] || 0) + eff.amount; Object.entries(eff.split || {}).forEach(([mid, v]) => { const nm = nameOf(g, mid); tOwed[nm] = (tOwed[nm] || 0) + Number(v); }); }));
    const grand = names.reduce((s, n) => s + tPaid[n], 0);
    return { names: names.filter(n => tPaid[n] > 0.005 || tOwed[n] > 0.005), tPaid, tOwed, grand };
  }, [state, totSel]);

  const approveEntry = (gid, eid) => dispatch('EntryConfirmationSet', gid, { entryId: eid, confirmed: true });
  const approveDel = gid => dispatch('GroupDeletionApproved', gid, {});

  return (
    <div style={{ paddingBottom: 40 }}>
      <div style={{ padding: '20px 20px 0', display: 'flex', alignItems: 'center', gap: 11 }}>
        <Mark size={34} />
        <span style={{ fontSize: 26, fontWeight: 800, color: C.ink, letterSpacing: -0.6 }}>Tally</span>
        <button onClick={() => go({ name: 'person', person: 'You', back: { name: 'home' } })} title="Your profile" style={{ marginLeft: 'auto', width: 40, height: 40, borderRadius: 13, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}>
          <Avatar name="You" size={30} />
        </button>
        <button onClick={() => setMenu(true)} style={{ width: 40, height: 40, borderRadius: 13, background: C.blue, border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `0 6px 16px ${C.blue}40` }}>
          <svg width="19" height="19" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" /></svg>
        </button>
      </div>

      <div style={{ margin: '18px 20px 0', borderRadius: 24, padding: 22, color: '#fff', background: `linear-gradient(145deg, #5B8DEF 0%, ${C.blue} 60%, ${C.blueDk} 100%)`, boxShadow: `0 10px 28px ${C.blue}26` }}>
        <span style={{ fontSize: 12.5, fontWeight: 600, opacity: 0.9 }}>Your balance across {state.groups.filter(g => !g.archived).length} groups</span>
        {Math.abs(overall.net) < 0.01 ? (
          <div style={{ marginTop: 12, fontSize: 30, fontWeight: 800, letterSpacing: -0.8, color: '#fff' }}>All settled up 🎉</div>
        ) : (
          <div style={{ marginTop: 10, display: 'flex', alignItems: 'flex-end', gap: 12, flexWrap: 'wrap' }}>
            <div style={{ fontSize: 46, fontWeight: 850, letterSpacing: -1.6, lineHeight: 0.95, color: overall.net >= 0 ? '#D6F5E1' : '#FFE2CC' }}>{state.currency}{Math.abs(overall.net).toFixed(2)}</div>
            <div style={{ paddingBottom: 6, fontSize: 14.5, fontWeight: 650, opacity: 0.92 }}>{overall.net >= 0 ? 'You are owed' : 'You owe'}</div>
          </div>
        )}
      </div>

      {/* change-requests awaiting your approval (edits, deletions) */}
      {requestDisplay.length > 0 && (
        <div style={{ margin: '22px 20px 0' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ fontSize: 18, fontWeight: 800, color: C.ink }}>Approvals needed</span>
            {requestDisplay.filter(x => !x.done).length > 0 && <span style={{ fontSize: 12, fontWeight: 800, color: '#fff', background: C.orange, borderRadius: 999, minWidth: 20, height: 20, padding: '0 6px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{requestDisplay.filter(x => !x.done).length}</span>}
            <button onClick={() => go({ name: 'pending' })} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.blue }}>See all</button>
          </div>
          {requestDisplay.filter(x => !x.done).length > 1 && (
            <button onClick={() => requestDisplay.filter(x => !x.done).forEach(x => approveNow(x.p))} style={{ width: '100%', marginBottom: 10, height: 40, borderRadius: 12, border: `1.5px solid ${C.orange}`, background: `${C.orange}10`, color: C.orangeDk, fontFamily: 'inherit', fontSize: 14, fontWeight: 750, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7 }}>
              <svg width="15" height="15" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.orangeDk} strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
              Approve all {requestDisplay.filter(x => !x.done).length}
            </button>
          )}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {requestDisplay.map(({ k, p, done }) => <PendingItem key={k} p={p} cur={state.currency} done={done} onApprove={() => approveNow(p)} onOpen={() => {
              if (p.type === 'groupdel') go({ name: 'group', id: p.g.id });
              else { const tid = (p.e.type === 'edit' || p.e.type === 'void') ? p.e.targetId : p.e.id; go({ name: 'expense', gid: p.g.id, eid: tid, back: { name: 'home' } }); }
            }} />)}
          </div>
        </div>
      )}

      {/* ledger + nav cards moved to bottom */}

      {pairs.length > 0 && (() => {
        const owesYou = pairs.filter(p => p.to === 'You');
        const youOwe = pairs.filter(p => p.from === 'You');
        const others = pairs.filter(p => p.from !== 'You' && p.to !== 'You');
        const PAIR_CAP = 4;
        const Col = ({ label, items, side, color }) => {
          const shown = showAllPairs ? items : items.slice(0, PAIR_CAP);
          const maxAmt = items.reduce((m, p) => Math.max(m, p.amount), 0) || 1;
          return (
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, fontWeight: 700, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6 }}>{label}</div>
            {shown.length === 0 ? <div style={{ fontSize: 13, color: C.faint, padding: '6px 0' }}>—</div> : shown.map((p, i) => {
              const other = side === 'to' ? p.from : p.to;
              const sz = 13 + 3 * (p.amount / maxAmt);
              return (
                <div key={i} onClick={() => go({ name: 'person', person: other, back: { name: 'home' } })} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', borderBottom: i < shown.length - 1 ? `1px solid ${C.soft}` : 'none', cursor: 'pointer' }}>
                  <Avatar name={other} size={28} />
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: 13.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{other}</div>
                    <div style={{ fontSize: sz, fontWeight: 800, color }}>{state.currency}{p.amount.toFixed(2)}</div>
                  </div>
                </div>
              );
            })}
          </div>
          );
        };
        return (
          <div style={{ padding: '24px 20px 0' }}>
            <div style={{ display: 'flex', alignItems: 'center', marginBottom: 10 }}>
              <span style={{ fontSize: 18, fontWeight: 800, color: C.ink }}>Who owes who</span>
              <button onClick={() => setSimp(s => !s)} style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, fontWeight: 700, background: simp ? C.blue : C.soft, color: simp ? '#fff' : C.sub, border: `1px solid ${simp ? C.blue : C.line}` }}><svg width="12" height="12" viewBox="0 0 24 24"><path d="M4 7h11m0 0l-3-3m3 3l-3 3M20 17H9m0 0l3-3m-3 3l3 3" stroke={simp ? '#fff' : C.sub} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Simplify</button>
            </div>
            {simp ? (() => {
              const nameBal = {};
              pairs.forEach(p => { nameBal[p.from] = (nameBal[p.from] || 0) - p.amount; nameBal[p.to] = (nameBal[p.to] || 0) + p.amount; });
              const tx = simplify(nameBal);
              if (tx.length === 0) return <div style={{ fontSize: 13, color: C.faint }}>Everyone's settled up 🎉</div>;
              const byTo = {};
              tx.forEach(t => { (byTo[t.to] = byTo[t.to] || []).push(t); });
              const recipients = Object.keys(byTo).sort((a, b) => (byTo[b].length - byTo[a].length) || (byTo[b].reduce((s, t) => s + t.amount, 0) - byTo[a].reduce((s, t) => s + t.amount, 0)));
              return (
                <div>
                <div style={{ fontSize: 12, color: C.sub, background: C.soft, borderRadius: 10, padding: '8px 11px', marginBottom: 12, lineHeight: 1.45 }}>This is the <b style={{ color: C.ink }}>fewest payments</b> that settle everyone — the totals are identical, just rerouted into fewer transfers.</div>
                <div style={{ display: 'flex', overflowX: 'auto', paddingBottom: 4 }}>
                  {recipients.map((rcpt, ci) => (
                    <React.Fragment key={rcpt}>
                      {ci > 0 && <div style={{ width: 1, background: C.line, flexShrink: 0, margin: '0 14px' }} />}
                      <div style={{ minWidth: 148, flex: '0 0 auto' }}>
                        <div style={{ fontSize: 12, fontWeight: 700, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6, whiteSpace: 'nowrap' }}>Owes {rcpt}</div>
                        {byTo[rcpt].sort((a, b) => b.amount - a.amount).map((t, i) => (
                          <div key={i} onClick={() => go({ name: 'person', person: t.from === 'You' ? rcpt : t.from, back: { name: 'home' } })} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', borderBottom: i < byTo[rcpt].length - 1 ? `1px solid ${C.soft}` : 'none', cursor: 'pointer' }}>
                            <Avatar name={t.from} size={26} />
                            <div style={{ minWidth: 0, flex: 1 }}>
                              <div style={{ fontSize: 13.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.from}</div>
                              <div style={{ fontSize: 13, fontWeight: 800, color: rcpt === 'You' ? C.green : t.from === 'You' ? C.orange : C.ink }}>{state.currency}{t.amount.toFixed(2)}</div>
                            </div>
                          </div>
                        ))}
                      </div>
                    </React.Fragment>
                  ))}
                </div>
                </div>
              );
            })() : (<>
            <div style={{ display: 'flex', gap: 16 }}>
              <Col label="Owes you" items={owesYou} side="to" color={C.green} />
              <div style={{ width: 1, background: C.line }} />
              <Col label="You owe" items={youOwe} side="from" color={C.orange} />
            </div>
            {(owesYou.length > PAIR_CAP || youOwe.length > PAIR_CAP || others.length > 0) && (
              <button onClick={() => setShowAllPairs(s => !s)} style={{ width: '100%', marginTop: 8, background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.blue, padding: '6px 0' }}>{showAllPairs ? 'Show less' : `Show all${others.length > 0 ? ` · incl. ${others.length} between others` : ''}`}</button>
            )}
            {showAllPairs && others.length > 0 && (
              <div style={{ marginTop: 16 }}>
                <div style={{ fontSize: 12, fontWeight: 700, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>Between others</div>
                {others.map((p, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 0', borderBottom: i < others.length - 1 ? `1px solid ${C.soft}` : 'none' }}>
                    <span style={{ display: 'inline-flex', alignItems: 'center' }}><Avatar name={p.from} size={26} /><span style={{ marginLeft: -6 }}><Avatar name={p.to} size={26} /></span></span>
                    <div style={{ flex: 1, fontSize: 14, color: C.sub }}><b style={{ color: C.ink }}>{p.from}</b> owes <b style={{ color: C.ink }}>{p.to}</b></div>
                    <div style={{ fontSize: 15, fontWeight: 800, color: C.ink }}>{state.currency}{p.amount.toFixed(2)}</div>
                  </div>
                ))}
              </div>
            )}
            </>)}
          </div>
        );
      })()}

      {disputed.length > 0 && (
        <div style={{ padding: '24px 20px 0' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
            <svg width="17" height="17" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.red} strokeWidth="2" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1.1" fill={C.red} /></svg>
            <span style={{ fontSize: 18, fontWeight: 800, color: C.ink }}>Disputed</span>
            <span style={{ fontSize: 12, fontWeight: 800, color: '#fff', background: C.red, borderRadius: 999, minWidth: 20, height: 20, padding: '0 6px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{disputed.length}</span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {disputed.map(({ g, e }) => <ExpenseCard key={e.id} group={g} E={e} cur={state.currency} showGroup stakeOnly onClick={() => go({ name: 'expense', gid: g.id, eid: e.id, back: { name: 'home' } })} />)}
          </div>
        </div>
      )}

      {recentApprovals.length > 0 && (
        <div style={{ padding: '24px 20px 0' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ fontSize: 18, fontWeight: 800, color: C.ink }}>Recent</span>
            {unconfirmedCount > 0 && <span style={{ fontSize: 12, fontWeight: 800, color: '#fff', background: C.orange, borderRadius: 999, minWidth: 20, height: 20, padding: '0 6px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{unconfirmedCount}</span>}
            <button onClick={() => go({ name: 'expenses' })} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.blue }}>See all</button>
          </div>
          {unconfirmedCount > 0 && <div style={{ fontSize: 12.5, color: C.sub, marginBottom: 10 }}>Tap the circle to confirm your share — only you can confirm your own.</div>}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {recentApprovals.slice(0, 7).map(({ g, e }) => <RecentRow key={e.id} group={g} E={e} cur={state.currency} onOpen={() => go({ name: 'expense', gid: g.id, eid: e.id, back: { name: 'home' } })} onToggle={() => dispatch('EntryConfirmationSet', g.id, { entryId: e.id, confirmed: !((e.approvals || {})[ME]) })} />)}
          </div>
        </div>
      )}

      <div style={{ display: 'flex', alignItems: 'center', padding: '24px 20px 10px' }}>
        <span style={{ fontSize: 18, fontWeight: 800, color: C.ink }}>Groups</span>
        <button onClick={() => setAddG(true)} style={{ marginLeft: 'auto', background: C.soft, border: `1px solid ${C.line}`, borderRadius: 10, padding: '7px 12px', fontSize: 14, fontWeight: 700, color: C.blue, cursor: 'pointer', fontFamily: 'inherit', display: 'flex', alignItems: 'center', gap: 6 }}>
          <svg width="13" height="13" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke={C.blue} strokeWidth="2" strokeLinecap="round" /></svg>New
        </button>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, padding: '0 20px' }}>
        {state.groups.filter(g => !g.archived && !g.deletion).map(g => {
          const myBal = netBalances(g)[ME] || 0;
          const count = baseEntries(g).filter(e => e.type === 'expense' && !effectiveEntry(g, e).voided).length;
          return (
            <div key={g.id} onClick={() => go({ name: 'group', id: g.id })} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: 14, borderRadius: 18, border: `1px solid ${C.line}`, cursor: 'pointer', background: C.bg }}>
              <div style={{ width: 46, height: 46, borderRadius: 13, background: C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22 }}>{g.emoji || '👥'}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 16, fontWeight: 700, color: C.ink }}>{g.name}{g.deletion && <span style={{ fontSize: 11, fontWeight: 700, color: C.red, background: `${C.red}12`, padding: '2px 7px', borderRadius: 6, marginLeft: 8 }}>deletion pending</span>}</div>
                <div style={{ fontSize: 13, color: C.sub, marginTop: 2 }}>{g.members.length} people · {count} expenses</div>
              </div>
              <div style={{ textAlign: 'right' }}>
                <div style={{ fontSize: 11, color: C.faint }}>{Math.abs(myBal) < 0.01 ? 'settled' : myBal > 0 ? 'you lent' : 'you owe'}</div>
                <div style={{ fontSize: 16, fontWeight: 800, color: Math.abs(myBal) < 0.01 ? C.sub : myBal > 0 ? C.green : C.orange }}>{Math.abs(myBal) < 0.01 ? '—' : `${state.currency}${Math.abs(myBal).toFixed(2)}`}</div>
              </div>
            </div>
          );
        })}
      </div>

      {state.groups.filter(g => g.archived || g.deletion).length > 0 && (
        <div style={{ padding: '20px 20px 0' }}>
          <button onClick={() => setShowArchived(s => !s)} style={{ display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.sub, padding: 0 }}>
            <svg width="12" height="12" viewBox="0 0 24 24" style={{ transform: showArchived ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }}><path d="M9 6l6 6-6 6" stroke={C.sub} strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
            Deleted groups ({state.groups.filter(g => g.archived || g.deletion).length})
          </button>
          {showArchived && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
              {state.groups.filter(g => g.archived || g.deletion).map(g => {
                const pending = !g.archived && g.deletion;
                const appr = g.deletion ? g.members.filter(m => g.deletion.approvals[m.id]).length : 0;
                return (
                  <div key={g.id} style={{ borderRadius: 18, border: `1px solid ${pending ? C.orange + '55' : C.line}`, background: pending ? `${C.orange}08` : C.soft }}>
                    <div onClick={() => go({ name: 'group', id: g.id })} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: 14, cursor: 'pointer' }}>
                      <div style={{ width: 40, height: 40, borderRadius: 12, background: C.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20 }}>{g.emoji || '👥'}</div>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 15, fontWeight: 700, color: C.sub, textDecoration: 'line-through' }}>{g.name}</div>
                        <div style={{ fontSize: 12.5, color: pending ? C.orangeDk : C.faint, marginTop: 1, fontWeight: pending ? 700 : 400 }}>{pending ? `⚠ deletion needs approval · ${appr}/${g.members.length}` : `deleted · ${g.members.length} people`}</div>
                      </div>
                      <svg width="8" height="14" viewBox="0 0 9 16"><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                    </div>
                    <div style={{ padding: '0 14px 12px' }}>
                      <button onClick={() => dispatch('GroupDeletionCancelled', g.id, {})} style={{ width: '100%', background: C.bg, border: `1px solid ${C.line}`, borderRadius: 10, padding: '9px', fontSize: 13.5, fontWeight: 700, color: C.blue, cursor: 'pointer', fontFamily: 'inherit' }}>Restore group</button>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>
      )}

      {/* nav cards + ledger — bottom of homepage */}

      <Sheet open={addG} onClose={() => { setAddG(false); setGMembers([]); }} title="New group">
        <Label>Group name</Label>
        <Input value={gName} onChange={setGName} placeholder="e.g. Apartment, Road trip" autoFocus />
        <div style={{ height: 16 }} />
        {(() => {
          const known = [];
          state.groups.filter(g => !g.archived).forEach(g => g.members.forEach(m => { if (m.name !== 'You' && !known.some(k => k.name === m.name)) known.push({ name: m.name, email: m.email || (state.profiles && state.profiles[m.name] && state.profiles[m.name].email) || '' }); }));
          known.sort((a, b) => a.name.localeCompare(b.name));
          if (known.length === 0) return null;
          const toggle = nm => setGMembers(ms => ms.includes(nm) ? ms.filter(x => x !== nm) : [...ms, nm]);
          return (
            <div>
              <Label>Add people <span style={{ fontWeight: 500, color: C.faint }}>(from your other groups)</span></Label>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                {known.map(k => { const on = gMembers.includes(k.name); return (
                  <button key={k.name} onClick={() => toggle(k.name)} style={chipStyle(on)}><Avatar name={k.name} size={22} />{k.name}{on && <svg width="13" height="13" viewBox="0 0 24 24" style={{ marginLeft: 1 }}><path d="M5 13l4 4L19 6" stroke={C.blue} strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}</button>
                ); })}
              </div>
              <div style={{ height: 16 }} />
            </div>
          );
        })()}
        <Btn disabled={!gName.trim()} onClick={() => {
          const gid = uid();
          dispatch('GroupCreated', gid, { name: gName.trim() });
          const known = {}; state.groups.filter(g => !g.archived).forEach(g => g.members.forEach(m => { if (m.name !== 'You') known[m.name] = m.email || (state.profiles && state.profiles[m.name] && state.profiles[m.name].email) || ''; }));
          gMembers.forEach(nm => dispatch('MemberAdded', gid, { memberId: uid(), name: nm, email: known[nm] || '' }));
          setGName(''); setGMembers([]); setAddG(false);
          go({ name: 'group', id: gid });
        }}>Create group</Btn>
      </Sheet>

      <Sheet open={menu} onClose={() => setMenu(false)} title="Quick actions">
        {(() => {
          const Row = ({ icon, label, sub, color, onClick }) => (
            <button onClick={onClick} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 13, padding: '12px 6px', background: 'none', border: 'none', borderBottom: `1px solid ${C.soft}`, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}>
              <span style={{ width: 40, height: 40, borderRadius: 12, background: `${color}14`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{icon}</span>
              <span style={{ flex: 1 }}><span style={{ display: 'block', fontSize: 15, fontWeight: 700, color: C.ink }}>{label}</span><span style={{ display: 'block', fontSize: 12.5, color: C.sub }}>{sub}</span></span>
              <svg width="8" height="14" viewBox="0 0 9 16"><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
            </button>
          );
          const nav = v => { setMenu(false); go(v); };
          return (
            <div>
              <Row color={C.blue} label="Add an expense" sub="Split a new cost in any group" onClick={() => { setMenu(false); setAddExp(true); }} icon={<svg width="20" height="20" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke={C.blue} strokeWidth="2.2" strokeLinecap="round" /></svg>} />
              <Row color={C.green} label="Record a payment" sub="Log money you paid someone" onClick={() => { setMenu(false); setPay(true); }} icon={<svg width="20" height="20" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>} />
              <Row color={C.orange} label="New group" sub="Start splitting with people" onClick={() => { setMenu(false); setAddG(true); }} icon={<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="7" cy="7.5" r="3" stroke={C.orange} strokeWidth="2" /><path d="M2 17c0-2.8 2.2-5 5-5s5 2.2 5 5" stroke={C.orange} strokeWidth="2" strokeLinecap="round" /></svg>} />
              <div style={{ height: 10 }} />
              <div style={{ fontSize: 11.5, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.4, padding: '4px 6px' }}>Go to</div>
              <Row color={C.blue} label="People" sub="Everyone you split with" onClick={() => nav({ name: 'people' })} icon={<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="7" cy="7.5" r="3" stroke={C.blue} strokeWidth="2" /><path d="M2 17c0-2.8 2.2-5 5-5s5 2.2 5 5" stroke={C.blue} strokeWidth="2" strokeLinecap="round" /></svg>} />
              <Row color={C.ink} label="Totals" sub="Paid vs. owed across groups" onClick={() => nav({ name: 'totals' })} icon={<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="3" y="3.5" width="14" height="13" rx="1.5" stroke={C.ink} strokeWidth="1.7" /><path d="M3 8h14M8 8v8.5" stroke={C.ink} strokeWidth="1.5" /></svg>} />
              <Row color={C.green} label="Payments" sub="Money paid between people" onClick={() => nav({ name: 'payments' })} icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M4 8h13m0 0l-3-3m3 3l-3 3M20 16H7m0 0l3-3m-3 3l3 3" stroke={C.green} strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" /></svg>} />
              <Row color={C.orange} label="Waiting on confirmations" sub="Who needs to confirm what" onClick={() => nav({ name: 'pending' })} icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke={C.orange} strokeWidth="1.9" /><path d="M12 7.5V12l3 2" stroke={C.orange} strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" /></svg>} />
              <Row color={C.ink} label="Activity" sub="Append-only event log &amp; audit" onClick={() => nav({ name: 'activity' })} icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M6 4h12a1 1 0 011 1v15l-3-2-2 2-2-2-2 2-2-2-3 2V5a1 1 0 011-1z" stroke={C.ink} strokeWidth="1.7" strokeLinejoin="round" /><path d="M9 9h6M9 12.5h6" stroke={C.ink} strokeWidth="1.7" strokeLinecap="round" /></svg>} />
            </div>
          );
        })()}
      </Sheet>
      <GlobalAddExpense open={addExp} onClose={() => setAddExp(false)} state={state} dispatch={dispatch} />
      <GlobalRecordPayment open={pay} onClose={() => setPay(false)} state={state} dispatch={dispatch} />
    </div>
  );
}

// ── Global add-expense: pick a group, then the editor ─────────
function GlobalAddExpense({ open, onClose, state, dispatch }) {
  const groups = state.groups.filter(g => !g.archived);
  const [gid, setGid] = useState(null);
  useEffect(() => { if (open) setGid(groups.length === 1 ? groups[0].id : null); }, [open]);
  const group = groups.find(g => g.id === gid);
  return (
    <Sheet open={open} onClose={onClose} title={group ? `Add to ${group.emoji} ${group.name}` : 'Add an expense'}>
      {!group ? (
        <div>
          <Label>Which group?</Label>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {groups.length === 0 && <Empty text="Create a group first." />}
            {groups.map(g => (
              <button key={g.id} onClick={() => setGid(g.id)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 12, borderRadius: 14, border: `1px solid ${C.line}`, background: C.bg, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}>
                <span style={{ width: 40, height: 40, borderRadius: 11, background: C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20 }}>{g.emoji}</span>
                <span style={{ flex: 1 }}><span style={{ display: 'block', fontSize: 15, fontWeight: 700, color: C.ink }}>{g.name}</span><span style={{ display: 'block', fontSize: 12.5, color: C.sub }}>{g.members.length} people</span></span>
                <svg width="8" height="14" viewBox="0 0 9 16"><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
              </button>
            ))}
          </div>
        </div>
      ) : (
        <div>
          {groups.length > 1 && <button onClick={() => setGid(null)} style={{ background: 'none', border: 'none', color: C.blue, fontWeight: 700, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit', padding: '0 0 12px', display: 'flex', alignItems: 'center', gap: 5 }}><svg width="7" height="12" viewBox="0 0 9 16"><path d="M7.5 1.5L1.5 8l6 6.5" stroke={C.blue} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Change group</button>}
          <EntryEditor group={group} cur={state.currency} submitLabel="Save expense" onSubmit={d => { addExpenseFrom(dispatch, group.id, d); onClose(); }} />
        </div>
      )}
    </Sheet>
  );
}

// ── Global record-payment: who paid whom, in which group ──────
function GlobalRecordPayment({ open, onClose, state, dispatch, initialPerson }) {
  const groups = state.groups.filter(g => !g.archived && (!initialPerson || g.members.some(m => m.name === initialPerson)));
  const [gid, setGid] = useState(null);
  const [from, setFrom] = useState(ME);
  const [to, setTo] = useState(null);
  const [amt, setAmt] = useState('');
  useEffect(() => { if (open) { setGid(groups.length === 1 ? groups[0].id : null); setFrom(ME); setTo(null); setAmt(''); } }, [open]);
  const group = groups.find(g => g.id === gid);
  useEffect(() => { if (open && initialPerson && group) { const m = group.members.find(mm => mm.name === initialPerson); if (m) setTo(m.id); } }, [gid, open]);
  const cur = state.currency;
  const A = parseFloat(amt) || 0;
  const valid = group && from && to && from !== to && A > 0;
  return (
    <Sheet open={open} onClose={onClose} title={initialPerson ? `Pay ${initialPerson}` : 'Record a payment'}>
      {!group ? (
        <div>
          <Label>Which group?</Label>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {groups.length === 0 && <Empty text="Create a group first." />}
            {groups.map(g => (
              <button key={g.id} onClick={() => setGid(g.id)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 12, borderRadius: 14, border: `1px solid ${C.line}`, background: C.bg, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}>
                <span style={{ width: 40, height: 40, borderRadius: 11, background: C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20 }}>{g.emoji}</span>
                <span style={{ flex: 1 }}><span style={{ display: 'block', fontSize: 15, fontWeight: 700, color: C.ink }}>{g.name}</span><span style={{ display: 'block', fontSize: 12.5, color: C.sub }}>{g.members.length} people</span></span>
                <svg width="8" height="14" viewBox="0 0 9 16"><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
              </button>
            ))}
          </div>
        </div>
      ) : (
        <div>
          {groups.length > 1 && <button onClick={() => setGid(null)} style={{ background: 'none', border: 'none', color: C.blue, fontWeight: 700, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit', padding: '0 0 12px', display: 'flex', alignItems: 'center', gap: 5 }}><svg width="7" height="12" viewBox="0 0 9 16"><path d="M7.5 1.5L1.5 8l6 6.5" stroke={C.blue} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Change group · {group.emoji} {group.name}</button>}
          <Label>Who paid?</Label>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {group.members.map(m => <button key={m.id} onClick={() => { setFrom(m.id); if (to === m.id) setTo(null); }} style={{ ...chipStyle(from === m.id), padding: '9px 16px' }}>{m.name}</button>)}
          </div>
          <div style={{ height: 14 }} />
          <Label>Paid to</Label>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {group.members.filter(m => m.id !== from).map(m => <button key={m.id} onClick={() => setTo(m.id)} style={{ ...chipStyle(to === m.id), padding: '9px 16px' }}>{m.name}</button>)}
          </div>
          <div style={{ height: 14 }} />
          <Label>Amount</Label>
          <Input value={amt} onChange={setAmt} placeholder="0.00" prefix={cur} type="number" />
          <div style={{ height: 20 }} />
          <Btn color={C.green} disabled={!valid} onClick={() => { dispatch('PaymentRecorded', group.id, { paymentId: uid(), from, to, amount: r2(A) }); onClose(); }}>Record payment</Btn>
        </div>
      )}
    </Sheet>
  );
}

function NavCard({ onClick, title, sub, icon, color }) {
  return (
    <button onClick={onClick} style={{ flex: 1, textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit', background: C.bg, border: `1px solid ${C.line}`, borderRadius: 18, padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
      <span style={{ width: 38, height: 38, borderRadius: 11, background: `${color}14`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <svg width="20" height="20" viewBox="0 0 20 20" fill="none">{icon}</svg>
      </span>
      <span>
        <span style={{ display: 'block', fontSize: 15, fontWeight: 700, color: C.ink }}>{title}</span>
        <span style={{ display: 'block', fontSize: 12.5, color: C.sub, marginTop: 1 }}>{sub}</span>
      </span>
    </button>
  );
}

// personal stake line ("you owe…" / "you paid…")
function stakeLine(g, e, cur) {
  if (e.type === 'expense') { const eff = effectiveEntry(g, e); if (eff.paidBy === ME) return ['you paid ' + cur + eff.amount.toFixed(2), C.green]; if (eff.split[ME] != null) return ['you owe ' + cur + Number(eff.split[ME]).toFixed(2), C.orange]; return null; }
  if (e.type === 'settle') { if (e.from === ME) return ['you paid ' + cur + e.amount.toFixed(2), C.green]; if (e.to === ME) return ['you receive ' + cur + e.amount.toFixed(2), C.green]; return null; }
  return null;
}
// short text summary of what an edit changed
function fmtDiffs(g, e, cur) {
  const diffs = editDiffs(g, e);
  if (!diffs.length) return 'details updated';
  return diffs.map(d => d[3] === 'money' ? `amount ${cur}${Number(d[1]).toFixed(2)} → ${cur}${Number(d[2]).toFixed(2)}`
    : d[3] === 'text' ? `“${d[1]}” → “${d[2]}”`
    : d[3] === 'meta' ? `${d[0].toLowerCase()} “${d[1]}” → “${d[2]}”`
    : d[3] === 'person' ? `paid by ${memOf(g, d[1]).name} → ${memOf(g, d[2]).name}`
    : 'shares ' + splitDeltas(g, d[1], d[2]).map(r => `${r[0]} ${cur}${r[1].toFixed(2)}→${cur}${r[2].toFixed(2)}`).join(', ')).join('   ·   ');
}
function PendingItem({ p, cur, onApprove, onOpen, done }) {
  const isExpenseLike = p.type !== 'groupdel' && (p.e.type === 'expense' || p.e.type === 'settle');
  let icon, title, sub, detail = null, detailColor = C.sub, accent = C.blue;
  if (p.type === 'groupdel') {
    icon = '🗑'; title = `Delete “${p.g.name}”`; sub = `${p.g.emoji} group · requested by ${memOf(p.g, p.g.deletion.by).name}`; detail = 'removes the group for everyone'; detailColor = C.red; accent = C.red;
  } else if (!isExpenseLike) {
    const e = p.e; const tgt = p.g.entries.find(x => x.id === e.targetId);
    if (e.type === 'edit') { icon = '✎'; title = `Change to “${tgt ? (tgt.desc || 'Untitled') : '—'}”`; sub = `${p.g.emoji} ${p.g.name} · by ${memOf(p.g, e.by).name}`; detail = fmtDiffs(p.g, e, cur); detailColor = C.blue; accent = C.blue; }
    else { icon = '🗑'; title = `Delete “${tgt ? (tgt.desc || 'Untitled') : '—'}”`; sub = `${p.g.emoji} ${p.g.name} · by ${memOf(p.g, e.by).name}`; detail = 'removes it from balances'; detailColor = C.red; accent = C.red; }
  }

  const confirmBtn = (
    <button onClick={done ? undefined : onApprove} disabled={done} aria-label={done ? 'Confirmed' : 'Confirm'}
      style={{ width: '100%', height: 42, borderRadius: 12, border: 'none', cursor: done ? 'default' : 'pointer', fontFamily: 'inherit', fontSize: 14.5, fontWeight: 750, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, background: done ? `${C.green}14` : C.orange, color: done ? C.green : '#fff', boxShadow: done ? 'none' : `0 6px 14px ${C.orange}33` }}>
      <svg width="16" height="16" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={done ? C.green : '#fff'} strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
      {done ? 'Confirmed' : 'Confirm'}
    </button>
  );

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8, opacity: done ? 0.55 : 1, transition: 'opacity .2s' }}>
      {isExpenseLike
        ? <ExpenseCard group={p.g} E={p.e} cur={cur} showGroup hideWaiting stakeOnly onClick={done ? () => {} : onOpen} />
        : (
          <div onClick={done ? undefined : onOpen} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 12, borderRadius: 14, border: `1px solid ${C.line}`, borderLeft: `3px solid ${accent}`, background: C.bg, cursor: done ? 'default' : 'pointer' }}>
            <div style={{ width: 38, height: 38, borderRadius: 11, background: C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 17, flexShrink: 0 }}>{icon}</div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 15, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
              <div style={{ fontSize: 12.5, color: C.sub, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{sub}</div>
              {detail && <div style={{ fontSize: 12.5, fontWeight: 700, color: detailColor, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{detail}</div>}
            </div>
          </div>
        )}
      {confirmBtn}
    </div>
  );
}

// ── Group detail ──────────────────────────────────────────────
function GroupView({ group, cur, dispatch, go, back, state }) {
  const [addExp, setAddExp] = useState(false);
  const [addMem, setAddMem] = useState(false);
  const [menu, setMenu] = useState(false);
  const [verifyMem, setVerifyMem] = useState(null);
  const [invite, setInvite] = useState(null);   // { link } when the invite sheet is open
  const [copied, setCopied] = useState(false);
  const [removeMem, setRemoveMem] = useState(null);   // member pending removal confirmation
  const syncOn = !!(typeof window !== 'undefined' && window.TALLY_SYNC_ENABLED && window.TallyGroupSync && window.TallyGroupSync.isSynced && window.TallyGroupSync.isSynced(group.id));
  const openInvite = () => {
    try { const inv = window.TallyGroupSync.createInvite(group.id); setInvite(inv); setCopied(false); }
    catch (e) { setInvite({ error: String(e && e.message || e) }); }
  };
  const entries = baseEntries(group).slice().sort((a, b) => b.date - a.date);
  const activeEntries = entries.filter(e => !effectiveEntry(group, e).voided);
  const deletedEntries = entries.filter(e => effectiveEntry(group, e).voided);
  const others = group.members.filter(m => m.id !== ME);
  const unverified = others.filter(m => !m.verified);
  const verifiedCount = group.members.filter(m => m.verified || m.id === ME).length;

  const requestDelete = () => { setMenu(false); dispatch('GroupDeletionRequested', group.id, {}); back(); };
  const cancelDelete = () => dispatch('GroupDeletionCancelled', group.id, {});
  const approveDelete = () => dispatch('GroupDeletionApproved', group.id, {});
  const delA = group.deletion ? { approved: group.members.filter(m => group.deletion.approvals[m.id]).length, total: group.members.length } : null;

  return (
    <div style={{ paddingBottom: 90 }}>
      <PageHead title={`${group.emoji} ${group.name}`} sub={`${group.members.length} people`} back={back}
        right={<button onClick={() => setMenu(true)} style={{ width: 38, height: 38, borderRadius: 11, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><svg width="18" height="5" viewBox="0 0 22 6"><circle cx="3" cy="3" r="2.5" fill={C.sub} /><circle cx="11" cy="3" r="2.5" fill={C.sub} /><circle cx="19" cy="3" r="2.5" fill={C.sub} /></svg></button>} />

      {group.deletion && (
        <div style={{ margin: '14px 20px 0', borderRadius: 14, padding: '12px 14px', background: `${C.red}10`, display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ fontSize: 18 }}>🗑</span>
          <div style={{ flex: 1, fontSize: 13.5, fontWeight: 600, color: C.ink }}>Deletion requested · {delA.approved}/{delA.total} approved</div>
          {group.deletion.by === ME
            ? <button onClick={cancelDelete} style={{ background: C.bg, border: `1px solid ${C.line}`, borderRadius: 9, padding: '7px 11px', fontSize: 13, fontWeight: 700, color: C.sub, cursor: 'pointer', fontFamily: 'inherit' }}>Cancel</button>
            : !group.deletion.approvals[ME] && <button onClick={approveDelete} style={{ background: C.red, color: '#fff', border: 'none', borderRadius: 9, padding: '7px 12px', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>Approve</button>}
        </div>
      )}

      {/* members */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '16px 20px 0', flexWrap: 'wrap' }}>
        {group.members.map(m => {
          const removable = m.id !== ME && !memberHasActivity(group, m.id);
          return (
          <div key={m.id} title={m.email || undefined} style={{ display: 'flex', alignItems: 'center', gap: 7, background: C.soft, border: `1px solid ${C.line}`, borderRadius: 999, padding: '5px 7px 5px 5px', cursor: 'pointer' }}>
            <span onClick={() => go({ name: 'person', person: m.name, back: { name: 'group', id: group.id } })} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
              <Avatar member={m} size={24} /><span style={{ fontSize: 13, fontWeight: 600, color: C.ink }}>{m.name}</span>
              {m.dupName && m.email && <span style={{ fontSize: 11, fontWeight: 600, color: C.faint, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>· {m.email}</span>}
            </span>
            {removable && <button onClick={() => setRemoveMem(m)} title={`Remove ${m.name}`} style={{ width: 18, height: 18, borderRadius: '50%', border: 'none', background: C.line, color: C.sub, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, fontSize: 12, lineHeight: 1 }}>×</button>}
          </div>
        ); })}
        <button onClick={() => setAddMem(true)} style={{ display: 'flex', alignItems: 'center', gap: 5, background: `${C.blue}12`, border: `1px dashed ${C.blue}55`, borderRadius: 999, padding: '6px 13px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.blue }}>
          <svg width="12" height="12" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke={C.blue} strokeWidth="2" strokeLinecap="round" /></svg>Add
        </button>
        {syncOn && <button onClick={openInvite} style={{ display: 'flex', alignItems: 'center', gap: 5, background: `${C.green}12`, border: `1px dashed ${C.green}66`, borderRadius: 999, padding: '6px 13px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.green }}>
          <svg width="13" height="13" viewBox="0 0 24 24"><path d="M4 12v7a1 1 0 001 1h14a1 1 0 001-1v-7M16 6l-4-4-4 4M12 2v13" stroke={C.green} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Invite
        </button>}
      </div>

      {/* key-verification nudge */}
      {unverified.length > 0 && (
        <div style={{ margin: '14px 20px 0', borderRadius: 14, border: `1px solid ${C.orange}3a`, background: `${C.orange}0c`, padding: '12px 14px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <svg width="15" height="15" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.orangeDk} strokeWidth="2" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1" fill={C.orangeDk} /></svg>
            <span style={{ fontSize: 13.5, fontWeight: 750, color: C.ink }}>{unverified.length} {unverified.length === 1 ? 'key' : 'keys'} not verified yet</span>
            <span style={{ marginLeft: 'auto', fontSize: 12, fontWeight: 700, color: C.sub }}>{verifiedCount}/{group.members.length} verified</span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 10 }}>
            {unverified.map(m => (
              <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <Avatar member={m} size={28} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 700, color: C.ink }}>{m.name}</div>
                  <div style={{ fontSize: 11.5, color: C.sub }}>Entries count, but aren’t trust-checked.</div>
                </div>
                <button onClick={() => setVerifyMem(m)} style={{ flexShrink: 0, padding: '7px 14px', borderRadius: 999, background: C.orange, color: '#fff', border: 'none', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>Verify</button>
              </div>
            ))}
          </div>
        </div>
      )}
      {others.length > 0 && unverified.length === 0 && (
        <div style={{ margin: '14px 20px 0', borderRadius: 14, border: `1px solid ${C.green}2e`, background: `${C.green}0c`, padding: '11px 14px', display: 'flex', alignItems: 'center', gap: 9 }}>
          <svg width="16" height="16" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
          <span style={{ fontSize: 13, fontWeight: 700, color: C.green }}>Everyone in this group is verified.</span>
        </div>
      )}

      {/* totals (folds balances + settle) */}
      <div style={{ padding: '18px 20px 0' }}>
        <button onClick={() => go({ name: 'totals', gid: group.id, back: { name: 'group', id: group.id } })} style={{ width: '100%', textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit', background: C.soft, border: `1px solid ${C.line}`, borderRadius: 14, padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 11 }}>
          <span style={{ width: 32, height: 32, borderRadius: 9, background: C.bg, border: `1px solid ${C.line}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><svg width="17" height="17" viewBox="0 0 20 20" fill="none"><rect x="3" y="3.5" width="14" height="13" rx="1.5" stroke={C.ink} strokeWidth="1.7" /><path d="M3 8h14M8 8v8.5" stroke={C.ink} strokeWidth="1.5" /></svg></span>
          <span style={{ flex: 1 }}><span style={{ display: 'block', fontSize: 14.5, fontWeight: 700, color: C.ink }}>Totals &amp; settle up</span><span style={{ display: 'block', fontSize: 12.5, color: C.sub }}>Shares, balances &amp; who pays who</span></span>
          <svg width="8" height="14" viewBox="0 0 9 16"><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </button>
      </div>

      {group.recurring && group.recurring.filter(r => r.active).length > 0 && (
        <div style={{ padding: '16px 20px 0' }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 8 }}>Recurring</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {group.recurring.filter(r => r.active).map(r => (
              <div key={r.id} style={{ display: 'flex', alignItems: 'center', gap: 10, border: `1px solid ${C.line}`, borderRadius: 12, padding: '10px 12px' }}>
                <span style={{ width: 32, height: 32, borderRadius: 9, background: `${C.blue}12`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><svg width="16" height="16" viewBox="0 0 24 24"><path d="M4 7h11m0 0l-3-3m3 3l-3 3M20 17H9m0 0l3-3m-3 3l3 3" stroke={C.blue} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.desc}</div>
                  <div style={{ fontSize: 12, color: C.sub }}>{cur}{Number(r.amount).toFixed(2)} · {r.frequency} · {memOf(group, r.paidBy).name} pays</div>
                </div>
                <button onClick={() => dispatch('RecurringStopped', group.id, { recurringId: r.id })} style={{ flexShrink: 0, background: C.soft, border: `1px solid ${C.line}`, borderRadius: 9, padding: '6px 11px', fontSize: 12.5, fontWeight: 700, color: C.sub, cursor: 'pointer', fontFamily: 'inherit' }}>Stop</button>
              </div>
            ))}
          </div>
        </div>
      )}

      <div style={{ padding: '16px 20px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
        {activeEntries.length === 0 && deletedEntries.length === 0 && <Empty text="No expenses yet. Add the first one." />}
        {activeEntries.map(E => <ExpenseCard key={E.id} group={group} E={E} cur={cur} onClick={() => go({ name: 'expense', gid: group.id, eid: E.id, back: { name: 'group', id: group.id } })} />)}
        {deletedEntries.length > 0 && (
          <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 10 }}>
            <div style={{ fontSize: 12, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.4 }}>Deleted expenses</div>
            {deletedEntries.map(E => <ExpenseCard key={E.id} group={group} E={E} cur={cur} onClick={() => go({ name: 'expense', gid: group.id, eid: E.id, back: { name: 'group', id: group.id } })} />)}
          </div>
        )}
      </div>

      <div style={{ position: 'fixed', bottom: 18, right: 18, left: 0, maxWidth: 460, margin: '0 auto', pointerEvents: 'none', display: 'flex', justifyContent: 'flex-end', paddingRight: 18 }}>
        <button onClick={() => setAddExp(true)} style={{ pointerEvents: 'auto', display: 'inline-flex', alignItems: 'center', gap: 7, height: 44, padding: '0 18px', borderRadius: 22, background: C.blue, color: '#fff', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 14.5, fontWeight: 650, boxShadow: `0 6px 16px ${C.blue}3d` }}><svg width="15" height="15" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" /></svg>Add expense</button>
      </div>

      <Sheet open={addExp} onClose={() => setAddExp(false)} title="Add expense">
        {addExp && <EntryEditor group={group} cur={cur} submitLabel="Save expense" onSubmit={d => { addExpenseFrom(dispatch, group.id, d); setAddExp(false); }} />}
      </Sheet>
      <AddMember open={addMem} onClose={() => setAddMem(false)} dispatch={dispatch} group={group} state={state} />
      <Sheet open={!!removeMem} onClose={() => setRemoveMem(null)} title={removeMem ? `Remove ${removeMem.name}?` : ''}>
        {removeMem && (
          <div>
            <div style={{ fontSize: 14, color: C.sub, lineHeight: 1.5, marginBottom: 16 }}>
              {removeMem.name} will be removed from “{group.name}”. You can only remove people who have no expenses yet, so nothing in the balances changes. They can be re-added or re-invited later.
            </div>
            <Btn color={C.red} onClick={() => { dispatch('MemberRemoved', group.id, { memberId: removeMem.id }); setRemoveMem(null); }}>Remove {removeMem.name}</Btn>
            <div style={{ height: 10 }} />
            <Btn kind="text" onClick={() => setRemoveMem(null)}>Cancel</Btn>
          </div>
        )}
      </Sheet>
      <Sheet open={!!invite} onClose={() => setInvite(null)} title="Invite to this group">
        {invite && invite.error && <div style={{ fontSize: 14, color: C.red, fontWeight: 600 }}>{invite.error}</div>}
        {invite && !invite.error && (
          <div>
            <div style={{ fontSize: 14, color: C.sub, lineHeight: 1.5, marginBottom: 14 }}>Share this link. Anyone who opens it can join “{group.name}” and see its shared ledger — treat it like a secret.</div>
            <div style={{ borderRadius: 12, border: `1px solid ${C.line}`, background: C.soft, padding: 12, fontSize: 12.5, color: C.ink, wordBreak: 'break-all', fontFamily: 'ui-monospace, monospace', marginBottom: 12 }}>{invite.link}</div>
            <Btn color={C.green} onClick={() => { try { navigator.clipboard.writeText(invite.link); setCopied(true); } catch (e) {} }}>
              {copied ? 'Copied ✓' : 'Copy invite link'}
            </Btn>
          </div>
        )}
      </Sheet>
      <Sheet open={!!verifyMem} onClose={() => setVerifyMem(null)} title={verifyMem ? `Verify ${verifyMem.name}` : ''}>
        {verifyMem && (
          <div>
            <div style={{ fontSize: 14, color: C.sub, lineHeight: 1.5, marginBottom: 14 }}>Compare this code with {verifyMem.name} in person or on a call. If it matches, their signing key is genuine.</div>
            <div style={{ borderRadius: 16, border: `1px solid ${C.line}`, background: C.soft, padding: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
              {safetyCode(verifyMem.id + verifyMem.name).map(([n, w], i) => (
                <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '9px 12px', borderRadius: 11, background: C.bg, border: `1px solid ${C.line}` }}>
                  <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12.5, fontWeight: 700, color: C.faint }}>{n}</span>
                  <span style={{ fontSize: 15, fontWeight: 700, color: C.ink }}>{w}</span>
                </div>
              ))}
            </div>
            <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
              <Btn color={C.green} onClick={() => { dispatch('MemberVerified', group.id, { memberId: verifyMem.id }); setVerifyMem(null); }}>
                <svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke="#fff" strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                They match
              </Btn>
              <Btn kind="text" onClick={() => setVerifyMem(null)}>They don’t match</Btn>
            </div>
          </div>
        )}
      </Sheet>
      <Sheet open={menu} onClose={() => setMenu(false)} title="Group options">
        <Btn kind="ghost" onClick={requestDelete} style={{ color: C.red, justifyContent: 'flex-start', paddingLeft: 16 }}>
          <svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M7 7l1 13h8l1-13" stroke={C.red} strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
          {others.length === 0 ? 'Delete group' : 'Request group deletion'}
        </Btn>
        {others.length > 0 && <div style={{ fontSize: 12.5, color: C.faint, marginTop: 10, lineHeight: 1.5 }}>Everyone in the group must approve before it's removed. Nothing is deleted from the ledger.</div>}
      </Sheet>
    </div>
  );
}

// ── Totals page — GLOBAL across all groups, approved / awaiting tables ──
function GroupTotalsPage({ state, cur, initialGid, go, back, rootTab, dispatch }) {
  const groups = state.groups.filter(g => !g.archived);
  const [sel, setSel] = useState(initialGid && groups.some(g => g.id === initialGid) ? initialGid : 'all');
  const [settle, setSettle] = useState(null);
  const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
  const shownGroups = sel === 'all' ? groups : groups.filter(g => g.id === sel);
  const nmeSet = [];
  shownGroups.forEach(g => g.members.forEach(m => { if (!nmeSet.includes(m.name)) nmeSet.push(m.name); }));
  const names = ['You', ...nmeSet.filter(n => n !== 'You')];
  const all = [];
  shownGroups.forEach(g => baseEntries(g).filter(e => e.type === 'expense').forEach(e => { const eff = effectiveEntry(g, e); if (!eff.voided) all.push({ g, e, eff, mine: !!(e.approvals || {})[ME] }); }));
  all.sort((a, b) => (a.mine - b.mine) || (b.e.date - a.e.date));
  const awaiting = all.filter(x => !x.mine).length;
  const openExp = (gid, eid) => go({ name: 'expense', gid, eid, back: { name: 'totals', gid: sel === 'all' ? undefined : sel, back: { name: 'home' } } });
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title="Totals" sub={sel === 'all' ? 'All groups · who paid & shares' : `${shownGroups[0] ? shownGroups[0].emoji + ' ' + shownGroups[0].name : ''} · who paid & shares`} back={rootTab ? undefined : back} />
      <div style={{ padding: '12px 20px 0', display: 'flex', gap: 8, overflowX: 'auto' }}>
        {[{ id: 'all', label: 'All groups', emoji: '📒' }, ...groups.map(g => ({ id: g.id, label: g.name, emoji: g.emoji }))].map(opt => {
          const on = sel === opt.id;
          return (
            <button key={opt.id} onClick={() => setSel(opt.id)} style={{ flexShrink: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '8px 13px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13.5, fontWeight: 700, whiteSpace: 'nowrap', background: on ? C.ink : C.soft, color: on ? '#fff' : C.ink, border: `1.5px solid ${on ? C.ink : C.line}` }}><span style={{ fontSize: 15 }}>{opt.emoji}</span>{opt.label}</button>
          );
        })}
      </div>
      {awaiting > 0 && <div style={{ margin: '14px 20px 0', padding: '10px 14px', borderRadius: 12, background: `${C.orange}12`, fontSize: 13.5, fontWeight: 700, color: C.orangeDk, display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: C.orange }} />{awaiting} awaiting your approval</div>}
      {sel !== 'all' && shownGroups[0] && (() => {
        const g = shownGroups[0]; const bal = netBalances(g); const tx = simplify(bal);
        return (
          <div style={{ padding: '16px 20px 0' }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, marginBottom: 8 }}>Suggested payments</div>
            {tx.length === 0 ? <div style={{ fontSize: 13, color: C.faint }}>Everyone's settled up 🎉</div> : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {tx.map((t, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 14, border: `1px solid ${C.line}` }}>
                    <div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><Avatar member={memOf(g, t.from)} size={26} /><b style={{ fontSize: 13.5, color: C.ink }}>{memOf(g, t.from).name}</b></span>
                      <span style={{ fontSize: 12.5, color: C.sub, fontWeight: 600 }}>pays</span>
                      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><Avatar member={memOf(g, t.to)} size={26} /><b style={{ fontSize: 13.5, color: C.ink }}>{memOf(g, t.to).name}</b></span>
                    </div>
                    <div style={{ fontSize: 14.5, fontWeight: 800, color: C.ink }}>{cur}{t.amount.toFixed(2)}</div>
                    {dispatch && <button onClick={() => setSettle({ ...t, gid: g.id })} style={{ background: C.blue, color: '#fff', border: 'none', borderRadius: 9, padding: '7px 11px', fontSize: 12.5, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>Settle</button>}
                  </div>
                ))}
              </div>
            )}
          </div>
        );
      })()}
      <TotalsList rows={all} names={names} nameOf={nameOf} cur={cur} onRow={openExp} mode="shares" showGroup={sel === 'all'} />
      <div style={{ padding: '16px 20px 0', fontSize: 12, color: C.faint, lineHeight: 1.6 }}>
        <span style={{ color: C.orange, fontWeight: 800 }}>●</span> needs your approval · <span style={{ color: C.green, fontWeight: 800 }}>●</span> approved · tap a row to review
      </div>
      {dispatch && settle && <SettleSheet tx={settle} onClose={() => setSettle(null)} group={groups.find(g => g.id === settle.gid)} cur={cur} dispatch={dispatch} />}
    </div>
  );
}

function TotalsList({ rows, names, nameOf, cur, onRow, mode }) {
  const paidOf = (x, name) => nameOf(x.g, x.eff.paidBy) === name ? x.eff.amount : 0;
  const owedOf = (x, name) => Object.entries(x.eff.split || {}).reduce((s, [mid, v]) => s + (nameOf(x.g, mid) === name ? Number(v) : 0), 0);
  const tPaid = {}, tOwed = {};
  names.forEach(n => { tPaid[n] = rows.reduce((s, x) => s + paidOf(x, n), 0); tOwed[n] = rows.reduce((s, x) => s + owedOf(x, n), 0); });
  const grand = rows.reduce((s, x) => s + x.eff.amount, 0);
  if (rows.length === 0) return <Empty text="No expenses yet." />;
  const COL_PAID = 82, COL_OWED = 82, COL_NET = 80;
  return (
    <div style={{ padding: '12px 20px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
      {/* TOTALS card — on top */}
      <div style={{ borderRadius: 14, background: C.soft, padding: '12px 14px' }}>
        <div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 8 }}>
          <span style={{ fontSize: 12, fontWeight: 800, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4 }}>Totals</span>
          <span style={{ marginLeft: 'auto', fontSize: 12, color: C.faint, fontWeight: 600 }}>{cur}{grand.toFixed(2)}</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingBottom: 6, borderBottom: `1px solid ${C.line}` }}>
          <span style={{ flex: 1 }} />
          <span style={{ width: COL_PAID, textAlign: 'right', fontSize: 10, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3 }}>Paid</span>
          <span style={{ width: COL_OWED, textAlign: 'right', fontSize: 10, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3 }}>Expenses</span>
          <span style={{ width: COL_NET, textAlign: 'right', fontSize: 10, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3 }}>Net</span>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          {names.map(n => { const net = r2(tPaid[n] - tOwed[n]); return (
            <div key={n} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '7px 0', borderBottom: `1px solid ${C.line}` }}>
              <Avatar name={n} size={22} />
              <span style={{ flex: 1, fontSize: 13.5, fontWeight: 700, color: C.ink }}>{n}</span>
              <span style={{ width: COL_PAID, textAlign: 'right', fontSize: 12.5, fontWeight: 700, color: tPaid[n] > 0.005 ? C.green : C.faint }}>{tPaid[n] > 0.005 ? `${cur}${tPaid[n].toFixed(2)}` : '—'}</span>
              <span style={{ width: COL_OWED, textAlign: 'right', fontSize: 12.5, fontWeight: 700, color: tOwed[n] > 0.005 ? C.orangeDk : C.faint }}>{tOwed[n] > 0.005 ? `${cur}${tOwed[n].toFixed(2)}` : '—'}</span>
              <span style={{ width: COL_NET, textAlign: 'right', fontSize: 13.5, fontWeight: 800, color: Math.abs(net) < 0.01 ? C.sub : net > 0 ? C.green : C.orange }}>{Math.abs(net) < 0.01 ? '—' : (net > 0 ? '+' : '−') + cur + Math.abs(net).toFixed(2)}</span>
            </div>
          ); })}
        </div>
      </div>

      {/* expense cards */}
      {rows.map(({ g, e, eff, mine }) => {
        const involved = involvedOf(e, g);
        const accent = mine ? C.green : C.orange;
        const payer = nameOf(g, eff.paidBy);
        return (
          <div key={e.id} onClick={() => onRow(g.id, e.id)} style={{ border: `1px solid ${C.line}`, borderLeft: `3px solid ${accent}`, borderRadius: 14, padding: '11px 13px', cursor: 'pointer' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 7, height: 7, borderRadius: '50%', background: accent, flexShrink: 0 }} />
              <span style={{ fontSize: 14.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{catEmoji(eff.category)} {eff.desc}</span>
              <span style={{ fontSize: 11.5, color: C.faint, flexShrink: 0 }}>· {g.emoji}</span>
              <span style={{ marginLeft: 'auto', fontSize: 14.5, fontWeight: 800, color: C.ink, flexShrink: 0 }}>{cur}{eff.amount.toFixed(2)}</span>
            </div>
            <div style={{ marginTop: 8, marginLeft: 15, display: 'inline-flex', alignItems: 'center', gap: 7, background: `${C.green}14`, borderRadius: 999, padding: '4px 12px 4px 5px' }}>
              <Avatar name={payer} size={20} />
              <span style={{ fontSize: 13, fontWeight: 700, color: C.green }}>{payer} paid {cur}{eff.amount.toFixed(2)}</span>
            </div>
            <div style={{ marginTop: 8, paddingLeft: 15, display: 'flex', flexWrap: 'wrap', gap: '5px 7px' }}>
              {involved.map(id => { const name = nameOf(g, id); const isPayer = eff.paidBy === id; const share = Number(eff.split[id] || 0); const net = r2((isPayer ? eff.amount : 0) - share);
                const val = mode === 'shares'
                  ? { txt: share > 0.005 ? `${cur}${share.toFixed(2)}` : '—', col: C.ink }
                  : { txt: net > 0.005 ? `+${cur}${net.toFixed(2)}` : net < -0.005 ? `−${cur}${Math.abs(net).toFixed(2)}` : '—', col: net > 0.005 ? C.green : net < -0.005 ? C.orangeDk : C.faint };
                return (
                  <span key={id} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: C.soft, borderRadius: 999, padding: '3px 9px 3px 4px' }}>
                    <Avatar name={name} size={18} />
                    <span style={{ fontSize: 12.5, fontWeight: 600, color: C.ink }}>{name}{mode === 'shares' ? '' : ' ·'}</span>
                    <span style={{ fontSize: 12.5, fontWeight: 800, color: val.col }}>{val.txt}</span>
                  </span>
                );
              })}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function GroupEntryRow({ E, group, cur, onClick }) {
  const eff = effectiveEntry(group, E);
  const settle = E.type === 'settle';
  const involved = involvedOf(E, group).map(id => memOf(group, id));
  const a = approvalOf(E, group);
  const ap = E.approvals || {};
  const pendingIds = a.req.filter(id => !ap[id]);
  const mine = !settle && eff.split[ME] != null;
  const lent = settle ? (eff.from === ME ? eff.amount : eff.to === ME ? -eff.amount : 0)
    : (eff.paidBy === ME ? eff.amount - (mine ? eff.split[ME] : 0) : (mine ? -eff.split[ME] : 0));
  return (
    <div onClick={onClick} style={{ padding: '11px 4px', borderBottom: `1px solid ${C.soft}`, cursor: 'pointer', opacity: eff.voided ? 0.5 : 1 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <div style={{ width: 42, height: 42, borderRadius: 12, background: settle ? `${C.green}14` : C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, flexShrink: 0 }}>
          {settle ? <svg width="20" height="20" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg> : catEmoji(eff.category)}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15.5, fontWeight: 700, color: C.ink, textDecoration: eff.voided ? 'line-through' : 'none' }}>
            {settle ? `${memOf(group, eff.from).name} → ${memOf(group, eff.to).name}` : eff.desc}
            {eff.amended && <span style={{ fontSize: 10.5, fontWeight: 700, color: C.blue, background: `${C.blue}12`, padding: '1px 6px', borderRadius: 5, marginLeft: 7 }}>edited by {memOf(group, eff.editedBy).name}</span>}
            {eff.pendingVoid && <span style={{ fontSize: 10.5, fontWeight: 700, color: C.orangeDk, background: `${C.orange}14`, padding: '1px 6px', borderRadius: 5, marginLeft: 7 }}>deletion pending</span>}
          </div>
          <div style={{ fontSize: 12, color: C.sub, marginTop: 3, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{settle ? 'payment' : `${memOf(group, eff.paidBy).name} paid`} · {relDate(E.date)}</div>
        </div>
        <div style={{ textAlign: 'right', flexShrink: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 800, color: Math.abs(lent) < 0.01 ? C.faint : lent > 0 ? C.green : C.orange }}>{settle ? `${cur}${eff.amount.toFixed(2)}` : Math.abs(lent) < 0.01 ? '—' : `${cur}${Math.abs(lent).toFixed(2)}`}</div>
          {!eff.voided && !a.done && (
            <div style={{ marginTop: 5, display: 'flex', alignItems: 'center', gap: 4, justifyContent: 'flex-end' }}>
              <svg width="11" height="11" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke={C.orange} strokeWidth="2.2" fill="none" /><path d="M12 7v5l3 2" stroke={C.orange} strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
              <span style={{ fontSize: 10.5, fontWeight: 700, color: C.orangeDk }}>waiting</span>
              <span style={{ display: 'inline-flex', alignItems: 'center' }}>{pendingIds.slice(0, 3).map((id, i) => <span key={id} style={{ marginLeft: i ? -5 : 0, borderRadius: '50%', boxShadow: '0 0 0 1.5px #fff', display: 'inline-flex' }}><Avatar member={memOf(group, id)} size={16} /></span>)}</span>
            </div>
          )}
        </div>
      </div>
      {!settle && (
        <div style={{ marginTop: 8, paddingLeft: 54, display: 'flex', flexWrap: 'wrap', gap: '5px 6px' }}>
          {Object.keys(eff.split).map(id => { const name = memOf(group, id).name; const isPayer = eff.paidBy === id; const share = Number(eff.split[id] || 0); return (
            <span key={id} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: isPayer ? `${C.green}12` : C.soft, borderRadius: 999, padding: '3px 9px 3px 4px' }}>
              <Avatar member={memOf(group, id)} size={16} />
              <span style={{ fontSize: 12, fontWeight: 600, color: C.ink }}>{name}</span>
              <span style={{ fontSize: 12, fontWeight: 800, color: isPayer ? C.green : C.ink }}>{cur}{share.toFixed(2)}</span>
            </span>
          ); })}
        </div>
      )}
    </div>
  );
}

function AddMember({ open, onClose, dispatch, group, state }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [picked, setPicked] = useState([]);
  const [invite, setInvite] = useState(null);
  const [copied, setCopied] = useState(false);
  useEffect(() => { if (open) { setName(''); setEmail(''); setPicked([]); setInvite(null); setCopied(false); } }, [open]);
  const syncOn = !!(typeof window !== 'undefined' && window.TALLY_SYNC_ENABLED && window.TallyGroupSync && window.TallyGroupSync.isSynced && window.TallyGroupSync.isSynced(group.id));
  const existing = group.members.map(m => m.name);
  const known = [];
  (state.groups || []).filter(g => !g.archived).forEach(g => g.members.forEach(m => { if (m.name !== 'You' && !existing.includes(m.name) && !known.some(k => k.name === m.name)) known.push({ name: m.name, email: m.email || (state.profiles && state.profiles[m.name] && state.profiles[m.name].email) || '' }); }));
  known.sort((a, b) => a.name.localeCompare(b.name));
  const toggle = nm => setPicked(p => p.includes(nm) ? p.filter(x => x !== nm) : [...p, nm]);
  const emailValid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim());

  const addExisting = () => {
    const emap = {}; known.forEach(k => { emap[k.name] = k.email; });
    picked.forEach(nm => dispatch('MemberAdded', group.id, { memberId: uid(), name: nm, email: emap[nm] || '', verified: true }));
    onClose();
  };
  const inviteNew = () => {
    // New people must be invited by email. Synced group → produce an invite link to
    // share (we can't send email yet). Offline group → add a local member by email.
    if (syncOn) {
      try { const inv = window.TallyGroupSync.createInvite(group.id); setInvite(inv); }
      catch (e) { setInvite({ error: String(e && e.message || e) }); }
    } else {
      dispatch('MemberAdded', group.id, { memberId: uid(), name: name.trim() || email.trim().split('@')[0], email: email.trim() });
      onClose();
    }
  };

  return (
    <Sheet open={open} onClose={onClose} title="Add member">
      {known.length > 0 && (
        <div>
          <Label>Add a friend <span style={{ fontWeight: 500, color: C.faint }}>(from your other groups)</span></Label>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
            {known.map(k => { const on = picked.includes(k.name); return (
              <button key={k.name} title={k.email || undefined} onClick={() => toggle(k.name)} style={chipStyle(on)}><Avatar name={k.name} size={22} />{k.name}{on && <svg width="13" height="13" viewBox="0 0 24 24" style={{ marginLeft: 1 }}><path d="M5 13l4 4L19 6" stroke={C.blue} strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}</button>
            ); })}
          </div>
          {picked.length > 0 && <><div style={{ height: 14 }} /><Btn onClick={addExisting}>{`Add ${picked.length} to group`}</Btn></>}
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '18px 0 14px' }}>
            <div style={{ flex: 1, height: 1, background: C.line }} />
            <span style={{ fontSize: 12, fontWeight: 600, color: C.faint }}>or invite someone new</span>
            <div style={{ flex: 1, height: 1, background: C.line }} />
          </div>
        </div>
      )}

      {invite ? (
        invite.error
          ? <div style={{ fontSize: 14, color: C.red, fontWeight: 600 }}>{invite.error}</div>
          : <div>
              <div style={{ fontSize: 14, color: C.sub, lineHeight: 1.5, marginBottom: 14 }}>Send this invite link to {email.trim() || 'your friend'}. When they open it and sign in, they'll join “{group.name}”.</div>
              <div style={{ borderRadius: 12, border: `1px solid ${C.line}`, background: C.soft, padding: 12, fontSize: 12.5, color: C.ink, wordBreak: 'break-all', fontFamily: 'ui-monospace, monospace', marginBottom: 12 }}>{invite.link}</div>
              <Btn color={C.green} onClick={() => { try { navigator.clipboard.writeText(invite.link); setCopied(true); } catch (e) {} }}>{copied ? 'Copied ✓' : 'Copy invite link'}</Btn>
            </div>
      ) : (
        <div>
          <Label>Email <span style={{ fontWeight: 500, color: C.faint }}>(required)</span></Label>
          <Input value={email} onChange={setEmail} placeholder="name@email.com" type="email" />
          <div style={{ height: 14 }} />
          <Label>Name <span style={{ fontWeight: 500, color: C.faint }}>(optional)</span></Label>
          <Input value={name} onChange={setName} placeholder="Friend's name" />
          <div style={{ height: 20 }} />
          <Btn disabled={!emailValid} onClick={inviteNew}>{syncOn ? 'Create invite link' : 'Add to group'}</Btn>
          {!emailValid && email.length > 0 && <div style={{ fontSize: 12.5, color: C.faint, marginTop: 8 }}>Enter a valid email address.</div>}
        </div>
      )}
    </Sheet>
  );
}

// ── Settle (manual, no payment links) ─────────────────────────
function PayEditor({ group, cur, eff, onSubmit }) {
  const [from, setFrom] = useState(eff.from);
  const [to, setTo] = useState(eff.to);
  const [amt, setAmt] = useState(String(eff.amount));
  const A = parseFloat(amt) || 0;
  const valid = from && to && from !== to && A > 0;
  return (
    <div>
      <div style={{ fontSize: 13, color: C.sub, marginBottom: 14, lineHeight: 1.5 }}>Edits apply right away and are flagged for everyone involved to re-confirm.</div>
      <Label>Who paid?</Label>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        {group.members.map(m => <button key={m.id} onClick={() => { setFrom(m.id); if (to === m.id) setTo(null); }} style={{ ...chipStyle(from === m.id), padding: '9px 16px' }}>{m.name}</button>)}
      </div>
      <div style={{ height: 14 }} />
      <Label>Paid to</Label>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        {group.members.filter(m => m.id !== from).map(m => <button key={m.id} onClick={() => setTo(m.id)} style={{ ...chipStyle(to === m.id), padding: '9px 16px' }}>{m.name}</button>)}
      </div>
      <div style={{ height: 14 }} />
      <Label>Amount</Label>
      <Input value={amt} onChange={setAmt} placeholder="0.00" prefix={cur} type="number" />
      <div style={{ height: 20 }} />
      <Btn color={C.green} disabled={!valid} onClick={() => onSubmit({ from, to, amount: r2(A) })}>Save changes</Btn>
    </div>
  );
}

function SettleSheet({ tx, onClose, group, cur, dispatch }) {
  const [amt, setAmt] = useState('');
  useEffect(() => { if (tx) setAmt(String(tx.amount)); }, [tx]);
  if (!tx) return null;
  const from = memOf(group, tx.from), to = memOf(group, tx.to);
  const A = parseFloat(amt) || 0;
  const partial = A > 0 && A < tx.amount - 0.005;
  const record = () => { dispatch('PaymentRecorded', group.id, { paymentId: uid(), from: tx.from, to: tx.to, amount: r2(A) }); onClose(); };
  return (
    <Sheet open={!!tx} onClose={onClose} title="Record a payment">
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, padding: '6px 0 16px' }}>
        <div style={{ textAlign: 'center' }}><Avatar member={from} size={52} /><div style={{ fontSize: 13, color: C.sub, marginTop: 6 }}>{from.name}</div></div>
        <Arrow />
        <div style={{ textAlign: 'center' }}><Avatar member={to} size={52} /><div style={{ fontSize: 13, color: C.sub, marginTop: 6 }}>{to.name}</div></div>
      </div>
      <Label>Amount paid</Label>
      <Input value={amt} onChange={setAmt} prefix={cur} type="number" />
      <div style={{ fontSize: 12.5, color: partial ? C.orangeDk : C.faint, fontWeight: 600, margin: '8px 2px 0' }}>{partial ? `Partial — ${cur}${r2(tx.amount - A).toFixed(2)} will still be owed` : `Full amount owed: ${cur}${tx.amount.toFixed(2)}`}</div>
      <div style={{ background: C.soft, borderRadius: 14, padding: '12px 14px', fontSize: 13, color: C.sub, margin: '16px 0', lineHeight: 1.5 }}>
        Records that <b style={{ color: C.ink }}>{from.name}</b> paid <b style={{ color: C.ink }}>{to.name}</b> outside the app. Both must confirm it on the ledger.
      </div>
      <Btn color={C.green} onClick={record} disabled={A <= 0}>Record payment</Btn>
    </Sheet>
  );
}

// ── Shared entry editor (add + request change) ────────────────
function EntryEditor({ group, cur, initial, onSubmit, submitLabel }) {
  const [desc, setDesc] = useState(initial ? initial.desc : '');
  const [amount, setAmount] = useState(initial && initial.amount ? String(initial.amount) : '');
  const [paidBy, setPaidBy] = useState(initial ? initial.paidBy : ME);
  const [category, setCategory] = useState(initial && initial.category ? initial.category : 'general');
  const [note, setNote] = useState(initial && initial.note ? initial.note : '');
  const [parts, setParts] = useState(() => initial && initial.split ? Object.keys(initial.split) : group.members.map(m => m.id));
  const [mode, setMode] = useState(initial && initial.unequal ? 'amounts' : 'equal');
  const [amts, setAmts] = useState(() => { const o = {}; group.members.forEach(m => { o[m.id] = initial && initial.split && initial.split[m.id] != null ? String(initial.split[m.id]) : ''; }); return o; });
  const [pcts, setPcts] = useState({});
  const [shrs, setShrs] = useState({});
  const [items, setItems] = useState(() => initial && initial.items ? initial.items.map(it => ({ ...it, amount: String(it.amount), sharedBy: [...(it.sharedBy || [])] })) : []);
  const [repeat, setRepeat] = useState(initial && initial.repeat ? initial.repeat : 'none');
  const A = parseFloat(amount) || 0;
  const num = v => parseFloat(v) || 0;
  const toggle = id => setParts(p => p.includes(id) ? p.filter(x => x !== id) : [...p, id]);

  let split = {}, msg = '', ok = true, itemsTotal = 0;
  if (mode === 'items') {
    const acc = {};
    items.forEach(it => { const amt = num(it.amount); itemsTotal += amt; const sb = (it.sharedBy && it.sharedBy.length) ? it.sharedBy : []; sb.forEach(id => { acc[id] = (acc[id] || 0) + amt / sb.length; }); });
    Object.keys(acc).forEach(id => { acc[id] = r2(acc[id]); });
    const ks = Object.keys(acc); if (ks.length) fixRound(acc, r2(itemsTotal), ks);
    split = acc;
    ok = items.length > 0 && items.every(it => num(it.amount) > 0 && it.sharedBy && it.sharedBy.length > 0);
    msg = ok ? `${cur}${r2(itemsTotal).toFixed(2)} · ${items.length} item${items.length === 1 ? '' : 's'}` : 'Add items with amounts & who shares';
  } else if (mode === 'equal') split = equalSplit(A, parts);
  else if (mode === 'amounts') { let s = 0; parts.forEach(id => { split[id] = r2(num(amts[id])); s += num(amts[id]); }); const rem = r2(A - s); ok = Math.abs(rem) < 0.005; msg = ok ? 'Splits add up ✓' : (rem > 0 ? `${cur}${rem.toFixed(2)} left` : `${cur}${Math.abs(rem).toFixed(2)} over`); }
  else if (mode === 'percent') { let tot = 0; parts.forEach(id => { tot += num(pcts[id]); }); parts.forEach(id => { split[id] = r2(A * num(pcts[id]) / 100); }); fixRound(split, A, parts); const d = r2(100 - tot); ok = Math.abs(d) < 0.05; msg = ok ? '100% allocated ✓' : (d > 0 ? `${d.toFixed(0)}% left` : `${Math.abs(d).toFixed(0)}% over`); }
  else { let tot = 0; parts.forEach(id => { tot += num(shrs[id]); }); parts.forEach(id => { split[id] = tot > 0 ? r2(A * num(shrs[id]) / tot) : 0; }); fixRound(split, A, parts); ok = tot > 0; msg = ok ? `${tot} share${tot === 1 ? '' : 's'} total` : 'Add shares'; }
  const finalAmount = mode === 'items' ? r2(itemsTotal) : r2(A);
  const valid = desc.trim() && finalAmount > 0 && Object.keys(split).length > 0 && ok;

  const modes = [['equal', 'Equal'], ['amounts', cur], ['percent', '%'], ['shares', 'Shares'], ['items', 'Items']];
  const setVal = (id, v) => mode === 'amounts' ? setAmts(a => ({ ...a, [id]: v })) : mode === 'percent' ? setPcts(a => ({ ...a, [id]: v })) : setShrs(a => ({ ...a, [id]: v }));
  const getVal = id => mode === 'amounts' ? amts[id] : mode === 'percent' ? pcts[id] : shrs[id];
  const addItem = () => setItems(its => [...its, { id: uid(), name: '', amount: '', sharedBy: [paidBy] }]);
  const setItem = (id, patch) => setItems(its => its.map(it => it.id === id ? { ...it, ...patch } : it));
  const itemShare = (it, mid) => { const sb = it.sharedBy || []; return sb.includes(mid) && num(it.amount) ? num(it.amount) / sb.length : 0; };

  return (
    <div>
      <Label>What for?</Label>
      <Input value={desc} onChange={setDesc} placeholder="e.g. Dinner, Hotel, Gas" autoFocus />
      <div style={{ height: 14 }} />
      <Label>Amount</Label>
      {mode === 'items'
        ? <div style={{ height: 52, borderRadius: 14, border: `1.5px solid ${C.line}`, background: C.soft, display: 'flex', alignItems: 'center', padding: '0 14px', fontSize: 17, fontWeight: 800, color: C.ink }}>{cur}{r2(itemsTotal).toFixed(2)} <span style={{ fontSize: 12.5, fontWeight: 600, color: C.faint, marginLeft: 8 }}>from items</span></div>
        : <Input value={amount} onChange={setAmount} placeholder="0.00" prefix={cur} type="number" />}
      <div style={{ height: 14 }} />
      <Label>Category</Label>
      <div style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 }}>
        {Object.entries(CATS).map(([k, [nm, em]]) => (
          <button key={k} onClick={() => setCategory(k)} style={{ flexShrink: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13.5, fontWeight: 600, background: category === k ? `${C.blue}14` : C.soft, color: category === k ? C.blue : C.ink, border: `1.5px solid ${category === k ? C.blue : C.line}`, whiteSpace: 'nowrap' }}><span style={{ fontSize: 15 }}>{em}</span>{nm}</button>
        ))}
      </div>
      <div style={{ height: 14 }} />
      <Label>Paid by</Label>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        {group.members.map(m => <button key={m.id} onClick={() => setPaidBy(m.id)} style={chipStyle(paidBy === m.id)}><Avatar member={m} size={22} />{m.name}</button>)}
      </div>
      <div style={{ height: 16 }} />
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <Label>Split</Label>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 3, background: C.soft, borderRadius: 10, padding: 3, marginBottom: 8 }}>
          {modes.map(([k, lbl]) => (
            <button key={k} onClick={() => setMode(k)} style={{ border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, padding: '6px 10px', borderRadius: 8, background: mode === k ? C.bg : 'transparent', color: mode === k ? C.ink : C.sub, boxShadow: mode === k ? '0 1px 3px rgba(0,0,0,0.08)' : 'none' }}>{lbl}</button>
          ))}
        </div>
      </div>
      {mode === 'items' ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {items.length === 0 && <div style={{ fontSize: 13, color: C.faint, padding: '4px 0' }}>No items yet — add each line of the receipt and who shared it.</div>}
          {items.map(it => (
            <div key={it.id} style={{ border: `1px solid ${C.line}`, borderRadius: 14, padding: 12 }}>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                <div style={{ flex: 1 }}><Input value={it.name} onChange={v => setItem(it.id, { name: v })} placeholder="Item" style={{ height: 44 }} /></div>
                <div style={{ width: 100 }}><Input value={it.amount} onChange={v => setItem(it.id, { amount: v })} placeholder="0.00" prefix={cur} type="number" style={{ height: 44 }} /></div>
                <button onClick={() => setItems(its => its.filter(x => x.id !== it.id))} style={{ width: 30, height: 30, flexShrink: 0, borderRadius: 8, border: 'none', background: C.soft, cursor: 'pointer', color: C.faint, fontFamily: 'inherit', fontSize: 16 }}>×</button>
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 9 }}>
                {group.members.map(m => { const on = (it.sharedBy || []).includes(m.id); return (
                  <button key={m.id} onClick={() => setItem(it.id, { sharedBy: on ? it.sharedBy.filter(x => x !== m.id) : [...(it.sharedBy || []), m.id] })} style={{ ...chipStyle(on), padding: '4px 10px 4px 4px', fontSize: 12.5 }}><Avatar member={m} size={18} />{m.name}{on && num(it.amount) > 0 && <span style={{ color: C.sub, fontWeight: 600 }}>{cur}{itemShare(it, m.id).toFixed(2)}</span>}</button>
                ); })}
              </div>
            </div>
          ))}
          <button onClick={addItem} style={{ height: 42, borderRadius: 12, border: `1.5px dashed ${C.blue}66`, background: `${C.blue}0a`, color: C.blue, fontWeight: 700, fontSize: 14, cursor: 'pointer', fontFamily: 'inherit', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}><svg width="13" height="13" viewBox="0 0 14 14"><path d="M7 1v12M1 7h12" stroke={C.blue} strokeWidth="2" strokeLinecap="round" /></svg>Add item</button>
          <div style={{ fontSize: 13, fontWeight: 700, textAlign: 'right', color: ok ? C.green : C.orangeDk }}>{msg}</div>
        </div>
      ) : mode === 'equal' ? (
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {group.members.map(m => { const on = parts.includes(m.id); return (
            <button key={m.id} onClick={() => toggle(m.id)} style={chipStyle(on)}><Avatar member={m} size={22} />{m.name}{on && A > 0 && <span style={{ color: C.sub, fontWeight: 600 }}>· {cur}{(split[m.id] || 0).toFixed(2)}</span>}</button>
          ); })}
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {group.members.map(m => { const on = parts.includes(m.id); return (
            <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <button onClick={() => toggle(m.id)} style={{ ...chipStyle(on), flex: 1 }}><Avatar member={m} size={22} />{m.name}{on && A > 0 && mode !== 'amounts' && <span style={{ color: C.sub, fontWeight: 600 }}>· {cur}{(split[m.id] || 0).toFixed(2)}</span>}</button>
              {on && <div style={{ width: 92 }}><Input value={getVal(m.id) || ''} onChange={v => setVal(m.id, v)} placeholder={mode === 'percent' ? '%' : mode === 'shares' ? 'shares' : '0.00'} prefix={mode === 'amounts' ? cur : undefined} type="number" style={{ height: 44 }} /></div>}
            </div>
          ); })}
          <div style={{ fontSize: 13, fontWeight: 700, textAlign: 'right', color: ok ? C.green : C.orangeDk }}>{msg}</div>
        </div>
      )}
      <div style={{ height: 14 }} />
      <Label>Note <span style={{ fontWeight: 500, color: C.faint }}>(optional)</span></Label>
      <Input value={note} onChange={setNote} placeholder="Add a note" />
      <div style={{ height: 16 }} />
      <Label>Repeat</Label>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
        {[['none', "Doesn't repeat"], ['weekly', 'Weekly'], ['monthly', 'Monthly']].map(([k, lbl]) => (
          <button key={k} onClick={() => setRepeat(k)} style={chipStyle(repeat === k)}>{repeat === k && k !== 'none' && <svg width="13" height="13" viewBox="0 0 24 24" style={{ marginRight: 2 }}><path d="M4 7h11m0 0l-3-3m3 3l-3 3M20 17H9m0 0l3-3m-3 3l3 3" stroke={C.blue} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}{lbl}</button>
        ))}
      </div>
      {repeat !== 'none' && <div style={{ fontSize: 12.5, color: C.sub, marginTop: 8, lineHeight: 1.5 }}>A new expense will be added automatically every {repeat === 'weekly' ? 'week' : 'month'}.</div>}
      <div style={{ height: 20 }} />
      <Btn disabled={!valid} onClick={() => onSubmit({ desc: desc.trim(), amount: finalAmount, paidBy, split, unequal: mode !== 'equal', category, note: note.trim(), items: mode === 'items' ? items.map(it => ({ id: it.id, name: it.name.trim() || 'Item', amount: r2(num(it.amount)), sharedBy: it.sharedBy })) : null, repeat })}>{submitLabel}</Btn>
    </div>
  );
}

// ── Ledger ────────────────────────────────────────────────────
function Ledger({ state, cur, go, back, embedded }) {
  const [q, setQ] = useState('');
  const rows = useMemo(() => {
    const all = [];
    state.groups.forEach(g => { if (g.archived) return; g.entries.forEach(e => all.push({ e, g })); });
    return all.sort((x, y) => y.e.date - x.e.date);
  }, [state]);
  const ql = q.trim().toLowerCase();
  const shown = ql ? rows.filter(({ e, g }) => {
    const names = involvedOf(e, g).map(id => memOf(g, id).name).join(' ');
    const t = e.type === 'expense' ? e.desc : e.type === 'settle' ? 'payment settle' : e.type;
    return `${t} ${g.name} ${names} ${e.type === 'expense' ? catName(e.category || 'general') : ''}`.toLowerCase().includes(ql);
  }) : rows;
  const exportCsv = () => {
    const esc = v => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
    const lines = [['Date', 'Group', 'Type', 'Description', 'Category', 'Paid by', 'Amount', 'Confirmed'].join(',')];
    rows.forEach(({ e, g }) => {
      const desc = e.type === 'expense' ? e.desc : e.type === 'settle' ? `${memOf(g, e.from).name} -> ${memOf(g, e.to).name}` : e.type === 'edit' ? 'change request' : 'deletion request';
      const cat = e.type === 'expense' ? catName(e.category || 'general') : '';
      const paid = e.type === 'expense' ? memOf(g, e.paidBy).name : e.type === 'settle' ? memOf(g, e.from).name : '';
      const amt = e.amount != null ? e.amount : (e.patch && e.patch.amount) || '';
      lines.push([new Date(e.date).toLocaleString(), g.name, e.type, desc, cat, paid, amt, approvalOf(e, g).done ? 'yes' : 'no'].map(esc).join(','));
    });
    const blob = new Blob([lines.join('\n')], { type: 'text/csv' });
    const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'tally-ledger.csv'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const csvBtn = <button onClick={exportCsv} style={{ height: 38, padding: '0 12px', borderRadius: 11, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.ink, display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}><svg width="15" height="15" viewBox="0 0 24 24"><path d="M12 3v12m0 0l-4-4m4 4l4-4M5 19h14" stroke={C.ink} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>CSV</button>;
  return (
    <div style={{ paddingBottom: embedded ? 8 : 40 }}>
      {!embedded && <PageHead title="Ledger" sub="Append-only · every event" back={back} right={csvBtn} />}
      <div style={{ padding: '12px 20px 0', display: 'flex', gap: 8, alignItems: 'center' }}>
        <div style={{ flex: 1 }}><Input value={q} onChange={setQ} placeholder="Search expenses, people, categories" style={{ height: 46 }} /></div>
        {embedded && csvBtn}
      </div>
      {shown.length === 0 && <Empty text={ql ? 'No matching entries.' : 'No entries yet.'} />}
      <div style={{ padding: '8px 20px 0' }}>
        {shown.map(({ e, g }) => <LedgerRow key={e.id} e={e} g={g} cur={cur} onClick={() => { const tid = (e.type === 'edit' || e.type === 'void') ? e.targetId : e.id; go({ name: 'expense', gid: g.id, eid: tid, back: { name: 'activity' } }); }} />)}
      </div>
    </div>
  );
}
function LedgerRow({ e, g, cur, onClick }) {
  const a = approvalOf(e, g);
  const involved = involvedOf(e, g).map(id => memOf(g, id));
  const tgt = (e.type === 'edit' || e.type === 'void') ? g.entries.find(x => x.id === e.targetId) : null;
  let icon, iconBg, title, meta, amount;
  if (e.type === 'expense') { icon = catEmoji(e.category); iconBg = C.soft2; title = e.desc; meta = `${memOf(g, e.paidBy).name} paid`; amount = e.amount; }
  else if (e.type === 'settle') { icon = '✓'; iconBg = `${C.green}14`; title = `${memOf(g, e.from).name} → ${memOf(g, e.to).name}`; meta = 'payment'; amount = e.amount; }
  else if (e.type === 'edit') { icon = '✎'; iconBg = `${C.blue}14`; title = `Change to “${tgt ? tgt.desc : '—'}”`; meta = `by ${memOf(g, e.by).name}`; amount = e.patch && e.patch.amount; }
  else { icon = '🗑'; iconBg = `${C.red}12`; title = `Delete “${tgt ? tgt.desc : '—'}”`; meta = `by ${memOf(g, e.by).name}`; amount = null; }
  return (
    <div onClick={onClick} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 0', borderBottom: `1px solid ${C.soft}`, cursor: 'pointer' }}>
      <div style={{ width: 42, height: 42, borderRadius: 12, background: iconBg, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, flexShrink: 0 }}>
        {e.type === 'settle' ? <svg width="20" height="20" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg> : icon}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 15.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
        <div style={{ fontSize: 12.5, color: C.ink, fontWeight: 600, marginTop: 3, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{involved.map(m => m.name).join(', ') || '—'}</div>
        <div style={{ fontSize: 12, color: C.sub, marginTop: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{g.emoji} {g.name} · {meta} · {relDate(e.date)}</div>
      </div>
      <div style={{ textAlign: 'right', flexShrink: 0 }}>
        {amount != null && <div style={{ fontSize: 15, fontWeight: 800, color: C.ink }}>{cur}{amount.toFixed(2)}</div>}
        <div style={{ marginTop: amount != null ? 4 : 0 }}><ApprovalPill a={a} /></div>
      </div>
    </div>
  );
}

// ── Expense / settle detail + requests + approvals ────────────
function ExpenseDetail({ group, eid, cur, dispatch, back }) {
  const [editing, setEditing] = useState(false);
  const [editPay, setEditPay] = useState(false);
  const [confirmDel, setConfirmDel] = useState(false);
  const [comment, setComment] = useState('');
  const [menu, setMenu] = useState(false);
  const [disputeOpen, setDisputeOpen] = useState(false);
  const [disputeReason, setDisputeReason] = useState('');
  const E = group && group.entries.find(x => x.id === eid);
  if (!group || !E) return <div><PageHead title="Not found" back={back} /></div>;

  const settle = E.type === 'settle';
  const eff = effectiveEntry(group, E);
  const a = approvalOf(E, group);
  const ap = E.approvals || {};
  const involved = settle ? [eff.from, eff.to].filter((v, i, arr) => arr.indexOf(v) === i) : [eff.paidBy, ...Object.keys(eff.split || {})].filter((v, i, arr) => arr.indexOf(v) === i);
  const payer = settle ? memOf(group, eff.from) : memOf(group, eff.paidBy);
  const latestPendingEdit = (() => { const eds = group.entries.filter(x => x.type === 'edit' && x.targetId === E.id).sort((a, b) => a.date - b.date); const l = eds[eds.length - 1]; return l && !approvalOf(l, group).done ? l : null; })();
  const pendingVoids = group.entries.filter(x => x.type === 'void' && x.targetId === E.id && !approvalOf(x, group).done);

  const effObj = { type: E.type, desc: eff.desc, amount: eff.amount, paidBy: eff.paidBy, from: eff.from, to: eff.to, split: eff.split, category: eff.category, note: eff.note };
  const toggle = mid => dispatch('EntryConfirmationSet', group.id, { entryId: E.id, confirmed: !ap[mid] }, mid);
  const approveReq = rid => dispatch('EntryConfirmationSet', group.id, { entryId: rid, confirmed: true });
  const submitEdit = d => {
    const { repeat, ...fields } = d;
    const info = recurringInfo(group, eff);
    const curFreq = info ? info.freq : 'none';
    const patch = { ...fields };
    if (repeat !== curFreq) {
      if (info) dispatch('RecurringStopped', group.id, { recurringId: info.rule.id });
      if (repeat !== 'none') {
        const rid = uid();
        dispatch('RecurringAdded', group.id, { recurringId: rid, desc: fields.desc, amount: fields.amount, paidBy: fields.paidBy, split: fields.split, category: fields.category, note: fields.note, items: fields.items, frequency: repeat, anchorTs: E.date });
        patch.recurringId = rid;
      } else { patch.recurringId = null; }
    }
    dispatch('ExpenseEditRequested', group.id, { editId: uid(), targetId: E.id, patch });
    setEditing(false);
  };
  const requestVoid = () => { dispatch('ExpenseVoidRequested', group.id, { voidId: uid(), targetId: E.id }); setConfirmDel(false); };
  const addComment = () => { if (!comment.trim()) return; dispatch('CommentPosted', group.id, { entryId: E.id, commentId: uid(), text: comment.trim() }); setComment(''); };
  const dispute = E.dispute && E.dispute.status === 'open' ? E.dispute : null;
  const raiseDispute = () => { if (!disputeReason.trim()) return; dispatch('EntryDisputed', group.id, { entryId: E.id, reason: disputeReason.trim() }); setDisputeReason(''); setDisputeOpen(false); };
  const resolveDispute = () => dispatch('DisputeResolved', group.id, { entryId: E.id });

  const DiffLine = ({ d }) => {
    if (d[3] === 'split') return (
      <div style={{ marginTop: 6 }}>
        <div style={{ fontSize: 12, color: C.sub, fontWeight: 700, marginBottom: 3 }}>Shares changed</div>
        {splitDeltas(group, d[1], d[2]).map((r, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 13, marginTop: 2 }}>
            <span style={{ color: C.ink, minWidth: 70, fontWeight: 600 }}>{r[0]}</span>
            <span style={{ color: C.faint, textDecoration: 'line-through' }}>{cur}{r[1].toFixed(2)}</span>
            <Arrow /><span style={{ color: r[2] > r[1] ? C.orange : C.green, fontWeight: 700 }}>{cur}{r[2].toFixed(2)}</span>
          </div>
        ))}
      </div>
    );
    const fmt = v => d[3] === 'money' ? `${cur}${Number(v).toFixed(2)}` : d[3] === 'person' ? memOf(group, v).name : `“${String(v) || '—'}”`;
    return (
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginTop: 5, flexWrap: 'wrap' }}>
        <span style={{ color: C.sub, minWidth: 74 }}>{d[0]}</span>
        <span style={{ color: C.faint, textDecoration: 'line-through' }}>{fmt(d[1])}</span>
        <Arrow /><span style={{ color: C.ink, fontWeight: 700 }}>{fmt(d[2])}</span>
      </div>
    );
  };

  // full append-only history of this entry, newest first
  const fmtDiffShort = ds => ds.map(d => d[3] === 'split' ? 'shares: ' + splitDeltas(group, d[1], d[2]).map(r => `${r[0]} ${cur}${r[1].toFixed(2)}→${cur}${r[2].toFixed(2)}`).join(', ') : d[3] === 'meta' ? `${String(d[0]).toLowerCase()} “${d[1]}”→“${d[2]}”` : `${String(d[0]).toLowerCase()} ${d[3] === 'money' ? cur + Number(d[1]).toFixed(2) : d[3] === 'person' ? memOf(group, d[1]).name : d[1]}→${d[3] === 'money' ? cur + Number(d[2]).toFixed(2) : d[3] === 'person' ? memOf(group, d[2]).name : d[2]}`).join('; ');
  const history = (() => {
    const h = [];
    h.push({ ts: E.date, who: E.by, kind: 'add', text: settle ? 'recorded this payment' : 'added this expense', detail: describeEntry(group, E, cur) });
    Object.entries(E.approvals || {}).forEach(([id, ts]) => { if (id !== E.by) h.push({ ts: typeof ts === 'number' ? ts : E.date, who: id, kind: 'confirm', text: 'confirmed the entry' }); });
    group.entries.filter(x => x.type === 'edit' && x.targetId === E.id).forEach(ed => {
      const ds = editDiffs(group, ed);
      h.push({ ts: ed.date, who: ed.by, kind: 'edit', text: 'edited the entry', detail: ds.length ? fmtDiffShort(ds) : 'details updated' });
      Object.entries(ed.approvals || {}).forEach(([id, ts]) => { if (id !== ed.by) h.push({ ts: typeof ts === 'number' ? ts : ed.date, who: id, kind: 'confirm', text: 'approved the change' }); });
    });
    group.entries.filter(x => x.type === 'void' && x.targetId === E.id).forEach(v => {
      h.push({ ts: v.date, who: v.by, kind: 'void', text: 'requested deletion' });
      Object.entries(v.approvals || {}).forEach(([id, ts]) => { if (id !== v.by) h.push({ ts: typeof ts === 'number' ? ts : v.date, who: id, kind: 'void', text: 'approved deletion' }); });
    });
    (E.comments || []).forEach(c => h.push({ ts: c.date, who: c.by, kind: 'comment', text: 'commented', detail: `“${c.text}”` }));
    return h.sort((x, y) => y.ts - x.ts);
  })();
  const histColor = k => k === 'add' ? C.blue : k === 'edit' ? C.orange : k === 'void' ? C.red : k === 'comment' ? C.sub : C.green;

  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title={settle ? 'Settlement' : 'Expense'} sub={`${group.emoji} ${group.name}`} back={back}
        right={!eff.voided ? <div style={{ display: 'flex', gap: 8 }}>
          {!settle && <button onClick={() => setEditing(true)} style={{ height: 38, padding: '0 14px', borderRadius: 11, background: `${C.blue}12`, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13.5, fontWeight: 700, color: C.blue, display: 'flex', alignItems: 'center', gap: 6 }}><svg width="14" height="14" viewBox="0 0 24 24"><path d="M4 20l4-1 11-11-3-3L5 16l-1 4z" stroke={C.blue} strokeWidth="2" fill="none" strokeLinejoin="round" /></svg>Edit</button>}
          {settle && <button onClick={() => setEditPay(true)} style={{ height: 38, padding: '0 14px', borderRadius: 11, background: `${C.blue}12`, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13.5, fontWeight: 700, color: C.blue, display: 'flex', alignItems: 'center', gap: 6 }}><svg width="14" height="14" viewBox="0 0 24 24"><path d="M4 20l4-1 11-11-3-3L5 16l-1 4z" stroke={C.blue} strokeWidth="2" fill="none" strokeLinejoin="round" /></svg>Edit</button>}
          <button onClick={() => setMenu(true)} style={{ width: 38, height: 38, borderRadius: 11, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><svg width="18" height="5" viewBox="0 0 22 6"><circle cx="3" cy="3" r="2.5" fill={C.sub} /><circle cx="11" cy="3" r="2.5" fill={C.sub} /><circle cx="19" cy="3" r="2.5" fill={C.sub} /></svg></button>
        </div> : undefined} />

      <div style={{ margin: '16px 20px 0', borderRadius: 22, background: eff.voided ? '#F6F7F9' : C.soft, padding: '20px 20px', textAlign: 'center', opacity: eff.voided ? 0.7 : 1 }}>
        <div style={{ fontSize: 16, fontWeight: 700, color: C.ink, textDecoration: eff.voided ? 'line-through' : 'none' }}>{settle ? `${payer.name} → ${memOf(group, eff.to).name}` : eff.desc}{!settle && <span style={{ fontWeight: 500, color: C.sub }}> · {catEmoji(eff.category)} {catName(eff.category)}</span>}</div>
        <div style={{ fontSize: 40, fontWeight: 800, color: C.ink, letterSpacing: -1, margin: '4px 0 12px', textDecoration: eff.voided ? 'line-through' : 'none' }}>{cur}{eff.amount.toFixed(2)}</div>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: C.bg, border: `1px solid ${C.line}`, borderRadius: 999, padding: '5px 14px 5px 5px' }}>
          <Avatar member={payer} size={26} /><span style={{ fontSize: 14, fontWeight: 700, color: C.ink }}>{payer.name} paid</span>
        </div>
        <div style={{ fontSize: 12.5, color: C.faint, marginTop: 10 }}>{new Date(E.date).toLocaleDateString(undefined, { weekday: 'short', month: 'long', day: 'numeric', year: 'numeric' })}</div>
        {(() => { const rec = recurringInfo(group, eff); return rec ? (
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 10, background: `${C.blue}10`, color: C.blue, fontSize: 12.5, fontWeight: 700, padding: '5px 12px', borderRadius: 999 }}>
            <svg width="13" height="13" viewBox="0 0 24 24"><path d="M4 7h11m0 0l-3-3m3 3l-3 3M20 17H9m0 0l3-3m-3 3l3 3" stroke={C.blue} strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
            Repeats {rec.freq} · next {shortDate(rec.next)}
          </div>
        ) : null; })()}
      </div>

      {eff.voided && (
        <div style={{ margin: '14px 20px 0', borderRadius: 14, padding: '14px', background: eff.pendingVoid ? `${C.orange}12` : '#F0F1F4', textAlign: 'center' }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: eff.pendingVoid ? C.orangeDk : C.sub, lineHeight: 1.5 }}>{eff.pendingVoid ? '🗑 Deleted · deletion needs approval from everyone involved' : 'This entry was deleted. It stays in the ledger for history.'}</div>
          <button onClick={() => dispatch('EntryRestored', group.id, { targetId: E.id })} style={{ marginTop: 10, background: C.bg, border: `1px solid ${C.line}`, borderRadius: 10, padding: '8px 16px', fontSize: 13.5, fontWeight: 700, color: C.blue, cursor: 'pointer', fontFamily: 'inherit' }}>{eff.pendingVoid ? 'Undo deletion' : 'Restore'}</button>
        </div>
      )}

      {eff.note && (
        <div style={{ margin: '14px 20px 0', borderRadius: 14, padding: '12px 14px', background: C.soft, fontSize: 14, color: C.ink, lineHeight: 1.5 }}><span style={{ color: C.faint, fontWeight: 700, fontSize: 11, letterSpacing: 0.3 }}>NOTE</span><div style={{ marginTop: 3 }}>{eff.note}</div></div>
      )}

      {/* dispute banner */}
      {dispute && !eff.voided && (
        <div style={{ margin: '14px 20px 0', borderRadius: 16, border: `1px solid ${C.red}3a`, background: `${C.red}0c`, padding: '14px 16px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
            <svg width="18" height="18" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.red} strokeWidth="2" strokeLinejoin="round" /><circle cx="12" cy="16.5" r="1.1" fill={C.red} /></svg>
            <span style={{ fontSize: 15, fontWeight: 800, color: C.red }}>Disputed by {memOf(group, dispute.by).name}{dispute.by === ME ? ' (you)' : ''}</span>
            <span style={{ marginLeft: 'auto', fontSize: 11.5, color: C.faint }}>{relDate(dispute.date)}</span>
          </div>
          {dispute.reason && <div style={{ fontSize: 13.5, color: C.ink, marginTop: 7, lineHeight: 1.45 }}>“{dispute.reason}”</div>}
          <div style={{ fontSize: 12.5, color: C.sub, marginTop: 7, lineHeight: 1.45 }}>The amount still stands in balances — resolve it by editing the entry, recording a correction, or clearing the flag once you agree.</div>
          <div style={{ display: 'flex', gap: 8, marginTop: 13 }}>
            {dispute.by === ME
              ? <div style={{ flex: 1 }}><Btn kind="ghost" onClick={resolveDispute} style={{ height: 44 }}>Withdraw dispute</Btn></div>
              : <>
                  {!settle && <div style={{ flex: 1 }}><Btn onClick={() => setEditing(true)} style={{ height: 44 }}>Edit to fix</Btn></div>}
                  <div style={{ flex: 1 }}><Btn kind="ghost" color={C.green} onClick={resolveDispute} style={{ height: 44 }}>Mark resolved</Btn></div>
                </>}
          </div>
        </div>
      )}

      {/* pending requests */}
      {pendingVoids.length > 0 && (
        <div style={{ padding: '0 20px' }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, margin: '22px 0 8px' }}>Pending requests</div>
          {pendingVoids.map(v => { const va = approvalOf(v, group); return (
            <RequestCard key={v.id} color={C.red} title={`Deletion requested by ${memOf(group, v.by).name}`} a={va} canApprove={va.req.includes(ME) && !(v.approvals || {})[ME]} onApprove={() => approveReq(v.id)}>
              <div style={{ fontSize: 13, color: C.sub }}>Removes this entry from balances once everyone approves.</div>
            </RequestCard>
          ); })}
        </div>
      )}

      {/* itemized receipt breakdown */}
      {!eff.voided && !settle && eff.items && eff.items.length > 0 && (
        <div style={{ padding: '0 20px' }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, margin: '24px 0 8px' }}>Items</div>
          {eff.items.map(it => (
            <div key={it.id} style={{ padding: '9px 0', borderBottom: `1px solid ${C.soft}` }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ flex: 1, fontSize: 14.5, fontWeight: 600, color: C.ink }}>{it.name}</span>
                <span style={{ fontSize: 14.5, fontWeight: 800, color: C.ink }}>{cur}{Number(it.amount).toFixed(2)}</span>
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginTop: 6 }}>
                {(it.sharedBy || []).map(mid => (
                  <span key={mid} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: C.soft, borderRadius: 999, padding: '2px 9px 2px 3px' }}>
                    <Avatar member={memOf(group, mid)} size={15} />
                    <span style={{ fontSize: 11.5, fontWeight: 600, color: C.sub }}>{memOf(group, mid).name} {cur}{(it.sharedBy.length ? Number(it.amount) / it.sharedBy.length : 0).toFixed(2)}</span>
                  </span>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* split & confirmations — one combined list */}
      {!eff.voided && (
        <div style={{ padding: '0 20px' }}>
          <div style={{ display: 'flex', alignItems: 'center', margin: '24px 0 2px' }}>
            <span style={{ fontSize: 13, fontWeight: 700, color: C.sub }}>{settle ? 'Payment' : `Split between ${Object.keys(eff.split).length}`}</span>
            <span style={{ marginLeft: 'auto' }}><ApprovalPill a={a} /></span>
          </div>
          <div style={{ fontSize: 12.5, color: C.faint, marginBottom: 4 }}>Confirm your own share below — everyone else confirms from their own device.</div>
          {involved.map(id => { const ok = !!ap[id]; const share = Number(eff.split[id] || 0); const isPayer = settle ? id === eff.from : id === eff.paidBy; const isMe = id === ME; return (
            <div key={id} onClick={isMe ? () => toggle(ME) : undefined} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '11px 0', borderBottom: `1px solid ${C.soft}`, cursor: isMe ? 'pointer' : 'default' }}>
              <Avatar member={memOf(group, id)} size={34} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 15, fontWeight: 600, color: C.ink }}>{memOf(group, id).name}{isMe && <span style={{ color: C.faint, fontWeight: 500 }}> · you</span>}</div>
                <div style={{ fontSize: 12.5, color: isPayer ? C.green : C.sub, marginTop: 1, fontWeight: 600 }}>{settle ? (id === eff.from ? `paid ${cur}${eff.amount.toFixed(2)}` : `received ${cur}${eff.amount.toFixed(2)}`) : (isPayer ? `paid ${cur}${eff.amount.toFixed(2)} · share ${cur}${share.toFixed(2)}` : `share ${cur}${share.toFixed(2)}`)}</div>
              </div>
              <span style={{ flexShrink: 0, display: 'flex', alignItems: 'center', gap: 7 }}>
                {!ok && <span style={{ fontSize: 11, fontWeight: 700, color: isMe ? C.orangeDk : C.faint }}>{isMe ? 'tap to confirm' : 'awaiting'}</span>}
                {ok
                  ? <svg width="22" height="22" viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill={C.green} /><path d="M7 12.5l3 3 7-7.5" stroke="#fff" strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  : <span style={{ display: 'block', width: 22, height: 22, borderRadius: '50%', border: `2px solid ${isMe ? C.orange : C.line}` }} />}
              </span>
            </div>
          ); })}
          {involved.includes(ME) && !ap[ME] && (
            <div style={{ marginTop: 16 }}><Btn color={C.green} onClick={() => toggle(ME)}><svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke="#fff" strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Approve this entry</Btn></div>
          )}
        </div>
      )}

      {/* history */}
      <div style={{ padding: '0 20px' }}>
        <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, margin: '24px 0 10px' }}>History</div>
        <div style={{ position: 'relative' }}>
          {history.map((h, i) => (
            <div key={i} style={{ display: 'flex', gap: 11, paddingBottom: i < history.length - 1 ? 14 : 0 }}>
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }}>
                <span style={{ width: 11, height: 11, borderRadius: '50%', background: histColor(h.kind), marginTop: 3, boxShadow: `0 0 0 3px ${histColor(h.kind)}1f` }} />
                {i < history.length - 1 && <span style={{ flex: 1, width: 2, background: C.line, marginTop: 3, minHeight: 14 }} />}
              </div>
              <div style={{ flex: 1, minWidth: 0, marginTop: -2 }}>
                <div style={{ fontSize: 13.5, color: C.ink, lineHeight: 1.4 }}><b style={{ fontWeight: 700 }}>{memOf(group, h.who).name}</b> {h.text}</div>
                {h.detail && <div style={{ fontSize: 12.5, color: C.sub, marginTop: 2, lineHeight: 1.45 }}>{h.detail}</div>}
                <div style={{ fontSize: 11.5, color: C.faint, marginTop: 2 }}>{new Date(h.ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} · {new Date(h.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* comments */}
      <div style={{ padding: '0 20px' }}>
        <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, margin: '22px 0 10px' }}>Comments</div>
        {(E.comments || []).length === 0 && <div style={{ fontSize: 13, color: C.faint, marginBottom: 12 }}>No comments yet.</div>}
        {(E.comments || []).map((c, i) => (
          <div key={i} style={{ display: 'flex', gap: 10, marginBottom: 12 }}>
            <Avatar member={memOf(group, c.by)} size={30} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: C.ink }}>{memOf(group, c.by).name} <span style={{ color: C.faint, fontWeight: 500, fontSize: 11.5 }}>· {relDate(c.date)}</span>{c.dispute && <span style={{ marginLeft: 6, fontSize: 10, fontWeight: 800, color: C.red, background: `${C.red}14`, padding: '1px 6px', borderRadius: 5 }}>DISPUTE</span>}</div>
              <div style={{ fontSize: 14, color: C.ink, marginTop: 1, lineHeight: 1.4 }}>{c.text}</div>
            </div>
          </div>
        ))}
        <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
          <div style={{ flex: 1 }}><Input value={comment} onChange={setComment} placeholder="Add a comment" style={{ height: 46 }} /></div>
          <button onClick={addComment} disabled={!comment.trim()} style={{ flexShrink: 0, background: comment.trim() ? C.blue : C.soft2, color: comment.trim() ? '#fff' : C.faint, border: 'none', borderRadius: 12, padding: '0 16px', fontSize: 14, fontWeight: 700, cursor: comment.trim() ? 'pointer' : 'default', fontFamily: 'inherit' }}>Post</button>
        </div>
      </div>

      {/* actions moved to the header ⋮ menu */}


      <Sheet open={menu} onClose={() => setMenu(false)} title="Entry options">
        {!settle && <><Btn kind="ghost" onClick={() => { setMenu(false); setEditing(true); }} style={{ justifyContent: 'flex-start', paddingLeft: 16 }}><svg width="17" height="17" viewBox="0 0 24 24"><path d="M4 20l4-1 11-11-3-3L5 16l-1 4z" stroke={C.ink} strokeWidth="1.8" fill="none" strokeLinejoin="round" /></svg>Edit expense</Btn><div style={{ height: 8 }} /></>}
        {!dispute && <><Btn kind="ghost" onClick={() => { setMenu(false); setDisputeOpen(true); }} style={{ color: C.orangeDk, justifyContent: 'flex-start', paddingLeft: 16 }}><svg width="18" height="18" viewBox="0 0 24 24"><path d="M12 3l9.5 16.5H2.5L12 3z" fill="none" stroke={C.orangeDk} strokeWidth="1.8" strokeLinejoin="round" /><circle cx="12" cy="16.7" r="1.1" fill={C.orangeDk} /></svg>Dispute this {settle ? 'payment' : 'expense'}</Btn><div style={{ height: 8 }} /></>}
        <Btn kind="ghost" onClick={() => { setMenu(false); setConfirmDel(true); }} disabled={eff.pendingVoid} style={{ color: C.red, justifyContent: 'flex-start', paddingLeft: 16 }}><svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M7 7l1 13h8l1-13" stroke={C.red} strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>{eff.pendingVoid ? 'Deletion already requested' : 'Request deletion'}</Btn>
      </Sheet>
      <Sheet open={editing} onClose={() => setEditing(false)} title="Edit expense">
        <div style={{ fontSize: 13, color: C.sub, marginBottom: 14, lineHeight: 1.5 }}>Your change is saved right away and logged. It's flagged as needing approval until everyone involved confirms it.</div>
        {editing && <EntryEditor group={group} cur={cur} initial={{ desc: eff.desc, amount: eff.amount, paidBy: eff.paidBy, split: eff.split, unequal: eff.unequal, category: eff.category, note: eff.note, items: eff.items, repeat: (recurringInfo(group, eff) || {}).freq || 'none' }} submitLabel="Save changes" onSubmit={submitEdit} />}
      </Sheet>
      <Sheet open={editPay} onClose={() => setEditPay(false)} title="Edit payment">
        {editPay && <PayEditor group={group} cur={cur} eff={eff} onSubmit={d => { dispatch('ExpenseEditRequested', group.id, { editId: uid(), targetId: E.id, patch: d }); setEditPay(false); }} />}
      </Sheet>
      <Sheet open={confirmDel} onClose={() => setConfirmDel(false)} title="Request deletion">
        <div style={{ fontSize: 14, color: C.sub, marginBottom: 18, lineHeight: 1.5 }}>This proposes voiding <b style={{ color: C.ink }}>{settle ? 'this payment' : `“${eff.desc}”`}</b>. The entry is never erased — once everyone involved approves, it's marked void and removed from balances.</div>
        <Btn color={C.red} onClick={requestVoid}>Request deletion</Btn>
      </Sheet>
      <Sheet open={disputeOpen} onClose={() => setDisputeOpen(false)} title={`Dispute ${settle ? 'payment' : 'expense'}`}>
        <div style={{ fontSize: 14, color: C.sub, marginBottom: 14, lineHeight: 1.5 }}>Flag this entry as wrong and tell the group why. It stays in the ledger and in balances, but everyone sees it's contested until it's resolved.</div>
        <textarea value={disputeReason} onChange={e => setDisputeReason(e.target.value)} autoFocus placeholder="What's wrong? e.g. I wasn't on this one / amount looks too high" rows={3}
          style={{ width: '100%', boxSizing: 'border-box', border: `1.5px solid ${C.line}`, borderRadius: 14, padding: '12px 14px', fontSize: 15, color: C.ink, fontFamily: 'inherit', resize: 'none', outline: 'none', lineHeight: 1.45 }} />
        <div style={{ height: 14 }} />
        <Btn color={C.red} onClick={raiseDispute} disabled={!disputeReason.trim()}>Raise dispute</Btn>
      </Sheet>
    </div>
  );
}

function RequestCard({ color, title, a, canApprove, onApprove, children }) {
  return (
    <div style={{ border: `1px solid ${color}33`, background: `${color}08`, borderRadius: 16, padding: 14, marginBottom: 10 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <span style={{ fontSize: 14, fontWeight: 700, color: C.ink, flex: 1 }}>{title}</span>
        <ApprovalPill a={a} />
      </div>
      {children}
      {canApprove && <button onClick={onApprove} style={{ marginTop: 12, width: '100%', background: color, color: '#fff', border: 'none', borderRadius: 11, padding: '10px', fontSize: 14, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>Approve request</button>}
    </div>
  );
}

// ── Audit log: spreadsheet of every change & addition ─────────
function actType(text) {
  const w = text.split(' ')[0];
  if (/added|created/.test(w)) return ['Add', C.green];
  if (/confirmed|approved/.test(w)) return ['Approve', C.blue];
  if (/requested/.test(w)) return ['Request', C.orangeDk];
  if (/recorded/.test(w)) return ['Payment', '#0891B2'];
  if (/commented/.test(w)) return ['Comment', C.sub];
  if (/cancelled|withdrew|deleted/.test(w)) return ['Revert', C.red];
  return ['Event', C.sub];
}
function Activity({ state, events, embedded }) {
  const log = (events || []).slice().sort((a, b) => b.ts - a.ts || (b.seq || 0) - (a.seq || 0));
  const items = (state.activity || []).slice().sort((a, b) => b.ts - a.ts);
  // group/member name lookups for rendering raw events readably
  const gName = sid => { const g = state.groups.find(x => x.id === sid); return g ? `${g.emoji} ${g.name}` : sid; };
  const personName = (sid, id) => { const g = state.groups.find(x => x.id === sid); const m = g && g.members.find(x => x.id === id); return m ? m.name : (id === 'me' ? 'You' : id); };
  const exportCsv = () => {
    const esc = v => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
    const lines = [['seq', 'iso_timestamp', 'event_id', 'actor', 'event_type', 'stream_id', 'data_json'].join(',')];
    log.slice().reverse().forEach((e, i) => lines.push([i + 1, new Date(e.ts).toISOString(), e.id, e.actor, e.type, e.streamId, JSON.stringify(e.data || {})].map(esc).join(',')));
    const blob = new Blob([lines.join('\n')], { type: 'text/csv' });
    const url = URL.createObjectURL(blob); const el = document.createElement('a'); el.href = url; el.download = 'tally-eventlog.csv'; el.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const exportJson = () => {
    const blob = new Blob([JSON.stringify({ events }, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob); const el = document.createElement('a'); el.href = url; el.download = 'tally-eventlog.json'; el.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const th = { textAlign: 'left', fontSize: 11, fontWeight: 700, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4, padding: '9px 12px', borderBottom: `1.5px solid ${C.line}`, whiteSpace: 'nowrap', position: 'sticky', top: 0, background: C.bg, zIndex: 1 };
  const td = { fontSize: 12.5, color: C.ink, padding: '10px 12px', borderBottom: `1px solid ${C.soft}`, whiteSpace: 'nowrap', verticalAlign: 'top' };
  const mono = { fontVariantNumeric: 'tabular-nums' };
  // readable rendering of an event's data payload
  const renderData = (e) => {
    const d = e.data || {}; const P = id => personName(e.streamId, id);
    const parts = [];
    if (e.type === 'GroupCreated') parts.push(`name: ${d.name}`);
    else if (e.type === 'MemberAdded') parts.push(`${d.name}${d.email ? ` <${d.email}>` : ''}`);
    else if (e.type === 'ProfileSet') parts.push(`${d.name}${d.email !== undefined ? ` email: ${d.email || '—'}` : ''}`);
    else if (e.type === 'ExpenseAdded') { parts.push(`“${d.desc}” ${formatCur(d.amount)}`); parts.push(`paid by ${P(d.paidBy)}`); if (d.category) parts.push(d.category); if (d.recurringId) parts.push('recurring'); parts.push('split: ' + Object.entries(d.split || {}).map(([id, v]) => `${P(id)} ${formatCur(v)}`).join(', ')); if (d.note) parts.push(`note: ${d.note}`); if (d.items) parts.push(`${d.items.length} items`); }
    else if (e.type === 'PaymentRecorded') parts.push(`${P(d.from)} → ${P(d.to)} ${formatCur(d.amount)}`);
    else if (e.type === 'ExpenseEditRequested') { parts.push(`edit of ${d.targetId.slice(0, 6)}`); Object.entries(d.patch || {}).forEach(([k, v]) => parts.push(`${k}: ${k === 'split' ? Object.entries(v).map(([id, a]) => `${P(id)} ${formatCur(a)}`).join('/') : k === 'paidBy' ? P(v) : typeof v === 'number' ? formatCur(v) : String(v)}`)); }
    else if (e.type === 'ExpenseVoidRequested') parts.push(`void ${d.targetId.slice(0, 6)}`);
    else if (e.type === 'EntryRestored') parts.push(`restore ${d.targetId.slice(0, 6)}`);
    else if (e.type === 'EntryConfirmationSet') parts.push(`${d.confirmed ? 'confirm' : 'unconfirm'} ${(d.entryId || '').slice(0, 6)}`);
    else if (e.type === 'CommentPosted') parts.push(`“${d.text}”`);
    else if (e.type === 'RecurringAdded') parts.push(`${d.desc} ${formatCur(d.amount)} ${d.frequency}`);
    else if (e.type === 'RecurringStopped') parts.push(`stop ${(d.recurringId || '').slice(0, 6)}`);
    else if (e.type.startsWith('GroupDeletion')) parts.push(e.type.replace('GroupDeletion', '').toLowerCase());
    return parts.join(' · ');
  };
  function formatCur(v) { return v == null ? '' : '$' + Number(v).toFixed(2); }
  const csvBtn = <button onClick={exportCsv} style={{ height: 38, padding: '0 12px', borderRadius: 11, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.ink, display: 'flex', alignItems: 'center', gap: 6 }}>CSV</button>;
  const jsonBtn = <button onClick={exportJson} style={{ height: 38, padding: '0 12px', borderRadius: 11, background: C.ink, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: '#fff', display: 'flex', alignItems: 'center', gap: 6 }}>JSON</button>;
  const tools = <div style={{ display: 'flex', gap: 8 }}>{csvBtn}{jsonBtn}</div>;
  return (
    <div style={{ paddingBottom: embedded ? 8 : 40 }}>
      {!embedded
        ? <PageHead title="Audit log" sub={`${log.length} events · complete & replayable`} back={back} right={tools} />
        : <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 20px 0' }}><span style={{ fontSize: 12, color: C.faint, flex: 1, lineHeight: 1.4 }}>Every event, in order — this is the complete source of truth.</span>{tools}</div>}
      {log.length === 0 && <Empty text="Nothing logged yet." />}
      {log.length > 0 && (
        <div style={{ marginTop: 14, overflowX: 'auto' }}>
          <table style={{ borderCollapse: 'collapse', width: '100%', minWidth: 760 }}>
            <thead><tr>
              <th style={{ ...th, paddingLeft: 20 }}>#</th>
              <th style={th}>Date</th>
              <th style={th}>Time</th>
              <th style={th}>Actor</th>
              <th style={th}>Event</th>
              <th style={th}>Group</th>
              <th style={{ ...th, paddingRight: 20 }}>Data</th>
            </tr></thead>
            <tbody>
              {log.map((e, i) => { const d = new Date(e.ts); return (
                <tr key={e.id} style={{ background: i % 2 ? `${C.soft}55` : C.bg }}>
                  <td style={{ ...td, ...mono, color: C.faint, paddingLeft: 20 }}>{log.length - i}</td>
                  <td style={{ ...td, ...mono, color: C.sub }}>{d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</td>
                  <td style={{ ...td, ...mono, color: C.sub }}>{d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</td>
                  <td style={td}><span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><Avatar name={e.actor === 'me' ? 'You' : personName(e.streamId, e.actor)} size={20} /><b style={{ fontWeight: 700 }}>{e.actor === 'me' ? 'You' : personName(e.streamId, e.actor)}</b></span></td>
                  <td style={td}><span style={{ fontSize: 11, fontWeight: 700, color: C.blue, background: `${C.blue}12`, padding: '2px 8px', borderRadius: 6, fontFamily: 'monospace' }}>{e.type}</span></td>
                  <td style={{ ...td, color: C.sub }}>{gName(e.streamId)}</td>
                  <td style={{ ...td, whiteSpace: 'normal', minWidth: 280, color: C.ink }}>{renderData(e)}</td>
                </tr>
              ); })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function ExpensesPage({ state, cur, go }) {
  const [q, setQ] = useState('');
  const [expandAll, setExpandAll] = useState(false);
  const rows = useMemo(() => {
    const out = [];
    state.groups.forEach(g => { if (g.archived) return; baseEntries(g).forEach(e => { if (e.type !== 'expense' && e.type !== 'settle') return; const eff = effectiveEntry(g, e); if (eff.voided) return; out.push({ g, e, eff }); }); });
    return out.sort((a, b) => b.e.date - a.e.date);
  }, [state]);
  const ql = q.trim().toLowerCase();
  const shown = ql ? rows.filter(({ g, e, eff }) => `${eff.desc || ''} ${e.type === 'settle' ? 'payment' : ''} ${g.name} ${catName(eff.category)}`.toLowerCase().includes(ql)) : rows;
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title="Expenses" sub={`${rows.length} across all groups`} />
      <div style={{ padding: '12px 20px 0', display: 'flex', gap: 10, alignItems: 'center' }}>
        <div style={{ flex: 1 }}><Input value={q} onChange={setQ} placeholder="Search expenses, payments, groups" style={{ height: 46 }} /></div>
        <button onClick={() => setExpandAll(v => !v)} style={{ flexShrink: 0, height: 46, background: C.soft, border: `1px solid ${C.line}`, borderRadius: 12, padding: '0 13px', fontSize: 13.5, fontWeight: 700, color: C.blue, cursor: 'pointer', fontFamily: 'inherit', display: 'flex', alignItems: 'center', gap: 6 }}>
          {expandAll ? 'Simple' : 'Details'}
          <svg width="11" height="7" viewBox="0 0 12 8" style={{ transform: expandAll ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }}><path d="M1 1l5 5 5-5" stroke={C.blue} strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </button>
      </div>
      {shown.length === 0 && <Empty text={ql ? 'No matching expenses.' : 'No expenses yet.'} />}
      <div style={{ padding: '8px 20px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
        {shown.map(({ g, e }) => <ExpenseCard key={e.id} group={g} E={e} cur={cur} showGroup stakeOnly={!expandAll} onClick={() => go({ name: 'expense', gid: g.id, eid: e.id, back: { name: 'expenses' } })} />)}
      </div>
    </div>
  );
}

function PaymentsPage({ state, cur, go, back }) {
  const groups = state.groups.filter(g => !g.archived);
  const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
  const pays = [];
  groups.forEach(g => baseEntries(g).forEach(e => { if (e.type !== 'settle') return; const eff = effectiveEntry(g, e); if (eff.voided) return; pays.push({ g, e, eff }); }));
  pays.sort((a, b) => b.e.date - a.e.date);
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title="Payments" sub={`${pays.length} recorded across all groups`} back={back} />
      {pays.length === 0 && <Empty text="No payments recorded yet." />}
      <div style={{ padding: '8px 20px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
        {pays.map(({ g, e, eff }) => {
          const a = approvalOf(e, g);
          return (
            <div key={e.id} onClick={() => go({ name: 'expense', gid: g.id, eid: e.id, back: { name: 'payments' } })} style={{ display: 'flex', alignItems: 'center', gap: 11, border: `1px solid ${C.line}`, borderRadius: 14, padding: '11px 13px', cursor: 'pointer' }}>
              <Avatar member={memOf(g, eff.from)} size={30} /><Arrow /><Avatar member={memOf(g, eff.to)} size={30} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14.5, fontWeight: 700, color: C.ink }}>{nameOf(g, eff.from)} paid {nameOf(g, eff.to)}</div>
                <div style={{ fontSize: 12, color: C.sub, marginTop: 1 }}>{g.emoji} {g.name} · {shortDate(e.date)}{a.done ? '' : ' · unconfirmed'}</div>
              </div>
              <div style={{ fontSize: 15.5, fontWeight: 800, color: C.green, flexShrink: 0 }}>{cur}{eff.amount.toFixed(2)}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function PendingConfirmations({ state, cur, go, back, dispatch }) {
  const groups = state.groups.filter(g => !g.archived);
  const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
  // gather every entry awaiting confirmation, and who still owes a confirmation
  const items = [];
  groups.forEach(g => g.entries.forEach(e => {
    if (!['expense', 'settle', 'edit', 'void'].includes(e.type)) return;
    if (e.type === 'expense' || e.type === 'settle') { if (effectiveEntry(g, e).voided) return; }
    if (e.type === 'edit' && !isMonetaryEdit(g, e)) return;
    const a = approvalOf(e, g); if (a.done) return;
    const ap = e.approvals || {};
    const waiting = a.req.filter(id => !ap[id]);
    const tgt = (e.type === 'edit' || e.type === 'void') ? g.entries.find(x => x.id === e.targetId) : null;
    const title = e.type === 'expense' ? (e.desc || 'Untitled') : e.type === 'settle' ? `${nameOf(g, e.from)} → ${nameOf(g, e.to)}` : e.type === 'edit' ? `Change to “${tgt ? (tgt.desc || 'Untitled') : '—'}”` : `Delete “${tgt ? (tgt.desc || 'Untitled') : '—'}”`;
    waiting.forEach(pid => items.push({ g, e, title, person: nameOf(g, pid), isYou: pid === ME }));
  }));
  // group by person
  const byPerson = {};
  items.forEach(it => { (byPerson[it.person] = byPerson[it.person] || []).push(it); });
  const people = Object.keys(byPerson).sort((a, b) => (a === 'You' ? -1 : b === 'You' ? 1 : byPerson[b].length - byPerson[a].length));
  const open = it => { const tid = (it.e.type === 'edit' || it.e.type === 'void') ? it.e.targetId : it.e.id; go({ name: 'expense', gid: it.g.id, eid: tid, back: { name: 'pending' } }); };
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title="Waiting on confirmations" sub={`${items.length} pending across ${people.length} ${people.length === 1 ? 'person' : 'people'}`} back={back} />
      {items.length === 0 && <Empty text="Everything's confirmed 🎉" />}
      <div style={{ padding: '8px 20px 0', display: 'flex', flexDirection: 'column', gap: 18 }}>
        {people.map(person => (
          <div key={person}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 8 }}>
              <Avatar name={person} size={30} />
              <span style={{ fontSize: 15, fontWeight: 800, color: C.ink }}>{person === 'You' ? 'You need to confirm' : `${person} needs to confirm`}</span>
              <span style={{ fontSize: 12, fontWeight: 800, color: '#fff', background: person === 'You' ? C.orange : C.sub, borderRadius: 999, minWidth: 19, height: 19, padding: '0 6px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{byPerson[person].length}</span>
              {person === 'You' && byPerson[person].length > 1 && (
                <button onClick={() => { const seen = {}; byPerson[person].forEach(it => { if (seen[it.e.id]) return; seen[it.e.id] = 1; dispatch('EntryConfirmationSet', it.g.id, { entryId: it.e.id, confirmed: true }); }); }} style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 12px', borderRadius: 999, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 750, background: C.orange, color: '#fff' }}>
                  <svg width="13" height="13" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke="#fff" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  Confirm all
                </button>
              )}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              {byPerson[person].map((it, i) => (
                <div key={i} onClick={() => open(it)} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 0', borderBottom: i < byPerson[person].length - 1 ? `1px solid ${C.soft}` : 'none', cursor: 'pointer' }}>
                  <div style={{ width: 34, height: 34, borderRadius: 10, background: C.soft2, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, flexShrink: 0 }}>{it.e.type === 'expense' ? catEmoji(effectiveEntry(it.g, it.e).category) : it.e.type === 'settle' ? '✓' : it.e.type === 'edit' ? '✎' : '🗑'}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14.5, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{it.title}</div>
                    <div style={{ fontSize: 12, color: C.sub }}>{it.g.emoji} {it.g.name}</div>
                  </div>
                  <svg width="8" height="14" viewBox="0 0 9 16" style={{ flexShrink: 0 }}><path d="M1.5 1.5L7.5 8l-6 6.5" stroke={C.faint} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function PeoplePage({ state, cur, go, back, rootTab }) {
  const groups = state.groups.filter(g => !g.archived);
  const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
  const pairs = globalPairwise(state);
  const netWith = name => pairs.reduce((s, p) => s + (p.to === 'You' && p.from === name ? p.amount : p.from === 'You' && p.to === name ? -p.amount : 0), 0);
  const set = []; groups.forEach(g => g.members.forEach(m => { if (m.name !== 'You' && !set.includes(m.name)) set.push(m.name); }));
  const shared = name => { let c = 0; groups.forEach(g => baseEntries(g).forEach(e => { if (e.type !== 'expense') return; const eff = effectiveEntry(g, e); if (eff.voided) return; const ns = involvedOf(e, g).map(id => nameOf(g, id)); if (ns.includes(name)) c++; })); return c; };
  const totalsFor = name => {
    let paid = 0, owed = 0;
    groups.forEach(g => baseEntries(g).forEach(e => {
      const eff = effectiveEntry(g, e); if (eff.voided) return;
      if (e.type === 'expense') { if (nameOf(g, eff.paidBy) === name) paid += eff.amount; Object.entries(eff.split || {}).forEach(([mid, v]) => { if (nameOf(g, mid) === name) owed += Number(v); }); }
      else if (e.type === 'settle') { if (nameOf(g, eff.from) === name) paid += eff.amount; if (nameOf(g, eff.to) === name) owed += eff.amount; }
    }));
    return { paid: r2(paid), owed: r2(owed) };
  };
  set.sort((a, b) => Math.abs(netWith(b)) - Math.abs(netWith(a)));
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title="People" sub="Everyone you split with" back={rootTab ? undefined : back} />
      <div style={{ padding: '12px 20px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
        {set.length === 0 && <Empty text="No one else yet." />}
        {set.map(name => { const net = r2(netWith(name)); const sc = shared(name); const t = totalsFor(name); return (
          <div key={name} onClick={() => go({ name: 'person', person: name, back: { name: 'people' } })} style={{ borderRadius: 16, border: `1px solid ${C.line}`, padding: 14, cursor: 'pointer' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
              <Avatar name={name} size={42} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 16, fontWeight: 700, color: C.ink }}>{name}</div>
                <div style={{ fontSize: 12.5, color: C.sub, marginTop: 2 }}>{sc} expense{sc === 1 ? '' : 's'}</div>
              </div>
              <div style={{ textAlign: 'right' }}>
                <div style={{ fontSize: 11, color: C.faint }}>{Math.abs(net) < 0.01 ? '' : net > 0 ? 'owes you' : 'you owe'}</div>
                <div style={{ fontSize: 16, fontWeight: 800, color: Math.abs(net) < 0.01 ? C.sub : net > 0 ? C.green : C.orange }}>{Math.abs(net) < 0.01 ? 'settled' : cur + Math.abs(net).toFixed(2)}</div>
              </div>
            </div>
            <div style={{ display: 'flex', gap: 8, marginTop: 11, paddingTop: 11, borderTop: `1px solid ${C.soft}` }}>
              <div style={{ flex: 1, textAlign: 'center' }}><div style={{ fontSize: 10.5, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3 }}>Paid in</div><div style={{ fontSize: 14, fontWeight: 800, color: C.green, marginTop: 2 }}>{cur}{t.paid.toFixed(2)}</div></div>
              <div style={{ width: 1, background: C.soft }} />
              <div style={{ flex: 1, textAlign: 'center' }}><div style={{ fontSize: 10.5, fontWeight: 700, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3 }}>Their share</div><div style={{ fontSize: 14, fontWeight: 800, color: C.orangeDk, marginTop: 2 }}>{cur}{t.owed.toFixed(2)}</div></div>
            </div>
          </div>
        ); })}
      </div>
    </div>
  );
}

function PersonDetail({ state, cur, personName, dispatch, go, back }) {
  const isSelf = personName === 'You';
  const profile = (state.profiles && state.profiles[personName]) || { email: '', phone: '' };
  const [editP, setEditP] = useState(false);
  const [verifyOpen, setVerifyOpen] = useState(false);
  const [payOpen, setPayOpen] = useState(false);
  const [email, setEmail] = useState(profile.email || '');
  useEffect(() => { setEmail(profile.email || ''); }, [personName, profile.email]);
  const groups = state.groups.filter(g => !g.archived);
  const nameOf = (g, id) => (g.members.find(m => m.id === id) || { name: '?' }).name;
  const entries = [];
  groups.forEach(g => baseEntries(g).forEach(e => { const eff = effectiveEntry(g, e); if (eff.voided) return; const ns = involvedOf(e, g).map(id => nameOf(g, id)); if (ns.includes(personName)) entries.push({ g, e, eff }); }));
  entries.sort((a, b) => b.e.date - a.e.date);
  const pairs = globalPairwise(state);
  const net = r2(pairs.reduce((s, p) => s + (p.to === 'You' && p.from === personName ? p.amount : p.from === 'You' && p.to === personName ? -p.amount : 0), 0));
  let paid = 0, owed = 0;
  entries.forEach(({ g, e, eff }) => {
    if (e.type === 'expense') { if (nameOf(g, eff.paidBy) === personName) paid += eff.amount; Object.entries(eff.split || {}).forEach(([mid, v]) => { if (nameOf(g, mid) === personName) owed += Number(v); }); }
    else if (e.type === 'settle') { if (nameOf(g, eff.from) === personName) paid += eff.amount; if (nameOf(g, eff.to) === personName) owed += eff.amount; }
  });
  paid = r2(paid); owed = r2(owed);
  const selfNet = isSelf ? r2(groups.reduce((s, g) => s + (netBalances(g)[ME] || 0), 0)) : net;
  const myGroups = groups.filter(g => g.members.some(m => m.name === personName));
  const memberRefs = isSelf ? [] : myGroups.map(g => ({ g, m: g.members.find(mm => mm.name === personName) })).filter(x => x.m);
  const allVerified = memberRefs.length > 0 && memberRefs.every(x => x.m.verified);
  const rowVals = ({ g, e, eff }) => {
    if (e.type === 'settle') { const p = nameOf(g, eff.from) === personName ? eff.amount : 0; const rcv = nameOf(g, eff.to) === personName ? eff.amount : 0; return { paid: p, share: rcv, net: r2(p - rcv) }; }
    const p = nameOf(g, eff.paidBy) === personName ? eff.amount : 0;
    const sh = Object.entries(eff.split || {}).reduce((s, [mid, v]) => s + (nameOf(g, mid) === personName ? Number(v) : 0), 0);
    return { paid: r2(p), share: r2(sh), net: r2(p - sh) };
  };
  return (
    <div style={{ paddingBottom: 40 }}>
      <PageHead title={personName} sub={isSelf ? 'Your profile' : undefined} back={back} />
      <div style={{ margin: '16px 20px 0', borderRadius: 20, background: C.soft, padding: '22px 20px', textAlign: 'center' }}>
        <Avatar name={personName} size={64} />
        <div style={{ fontSize: 18, fontWeight: 800, color: C.ink, marginTop: 10 }}>{personName}</div>
        <button onClick={() => setEditP(true)} style={{ marginTop: 4, display: 'inline-flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, color: profile.email ? C.sub : C.blue, fontWeight: 600 }}>
          <svg width="14" height="14" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2" stroke={profile.email ? C.sub : C.blue} strokeWidth="1.8" fill="none" /><path d="M4 7l8 6 8-6" stroke={profile.email ? C.sub : C.blue} strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
          {profile.email || 'Add email'}
        </button>
        {!isSelf && (allVerified
          ? <div style={{ marginTop: 9, display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 999, background: `${C.green}12` }}>
              <svg width="14" height="14" viewBox="0 0 24 24"><path d="M12 2l8 3v6c0 5-3.4 8.4-8 11-4.6-2.6-8-6-8-11V5l8-3z" fill="none" stroke={C.green} strokeWidth="1.9" strokeLinejoin="round" /><path d="M8.5 12l2.4 2.4L16 9.5" stroke={C.green} strokeWidth="1.9" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
              <span style={{ fontSize: 12.5, fontWeight: 750, color: C.green }}>Verified key</span>
            </div>
          : <button onClick={() => setVerifyOpen(true)} style={{ marginTop: 9, display: 'inline-flex', alignItems: 'center', gap: 7, padding: '8px 15px', borderRadius: 999, background: C.orange, color: '#fff', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 750, boxShadow: `0 5px 14px ${C.orange}33` }}>
              <svg width="14" height="14" viewBox="0 0 24 24"><path d="M12 2l8 3v6c0 5-3.4 8.4-8 11-4.6-2.6-8-6-8-11V5l8-3z" fill="none" stroke="#fff" strokeWidth="1.9" strokeLinejoin="round" /></svg>
              Verify {personName}
            </button>)}
        <div style={{ borderTop: `1px solid ${C.line}`, margin: '16px 0 0', paddingTop: 14 }}>
          <div style={{ fontSize: 13, color: C.sub }}>{Math.abs(selfNet) < 0.01 ? 'all settled up' : isSelf ? (selfNet > 0 ? "you're owed overall" : 'you owe overall') : (selfNet > 0 ? 'owes you' : 'you owe')}</div>
          {Math.abs(selfNet) >= 0.01 && <div style={{ fontSize: 32, fontWeight: 800, letterSpacing: -1, color: selfNet > 0 ? C.green : C.orange }}>{cur}{Math.abs(selfNet).toFixed(2)}</div>}
        </div>
        <div style={{ display: 'flex', justifyContent: 'center', gap: 10, marginTop: 14 }}>
          <div style={{ flex: 1, background: C.bg, borderRadius: 12, padding: '9px 6px' }}><div style={{ fontSize: 10.5, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3, fontWeight: 700 }}>{isSelf ? 'You paid' : 'Paid in'}</div><div style={{ fontSize: 15.5, fontWeight: 800, color: C.green, marginTop: 2 }}>{cur}{paid.toFixed(2)}</div></div>
          <div style={{ flex: 1, background: C.bg, borderRadius: 12, padding: '9px 6px' }}><div style={{ fontSize: 10.5, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3, fontWeight: 700 }}>{isSelf ? 'Your spending' : 'Spending'}</div><div style={{ fontSize: 15.5, fontWeight: 800, color: C.orangeDk, marginTop: 2 }}>{cur}{owed.toFixed(2)}</div></div>
        </div>
        {!isSelf && <button onClick={() => setPayOpen(true)} style={{ marginTop: 12, width: '100%', height: 46, borderRadius: 13, background: C.green, color: '#fff', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 15, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, boxShadow: `0 6px 16px ${C.green}33` }}><svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 12h14m0 0l-5-5m5 5l-5 5" stroke="#fff" strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>Pay {personName}</button>}
      </div>
      {!isSelf && (
        <div style={{ margin: '14px 20px 0', borderRadius: 16, border: `1px solid ${C.line}`, padding: '14px 16px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <svg width="15" height="15" viewBox="0 0 24 24"><circle cx="8" cy="8" r="5" fill="none" stroke={C.sub} strokeWidth="1.8" /><path d="M11.5 11.5L20 20m-3-3l2-2m-4 0l1.5 1.5" stroke={C.sub} strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
            <span style={{ fontSize: 12, fontWeight: 800, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4 }}>Encryption</span>
            <span style={{ marginLeft: 'auto' }}>{allVerified
              ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', borderRadius: 999, background: `${C.green}12`, fontSize: 11.5, fontWeight: 750, color: C.green }}>✓ Verified</span>
              : <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', borderRadius: 999, background: `${C.orange}14`, fontSize: 11.5, fontWeight: 750, color: C.orangeDk }}>Unverified</span>}</span>
          </div>
          <div style={{ fontSize: 10.5, color: C.faint, fontWeight: 700, marginBottom: 4, textTransform: 'uppercase', letterSpacing: 0.3 }}>Ed25519 public key</div>
          <div style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12.5, color: C.ink, lineHeight: 1.7, wordBreak: 'break-all' }}>{pubKeyHex(personName)}</div>
          <button onClick={() => setVerifyOpen(true)} style={{ marginTop: 12, width: '100%', height: 40, borderRadius: 11, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13.5, fontWeight: 700, color: C.blue, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7 }}><svg width="14" height="14" viewBox="0 0 24 24"><path d="M12 2l8 3v6c0 5-3.4 8.4-8 11-4.6-2.6-8-6-8-11V5l8-3z" fill="none" stroke={C.blue} strokeWidth="1.8" strokeLinejoin="round" /></svg>{allVerified ? 'View safety code' : 'Compare safety code to verify'}</button>
        </div>
      )}
      {myGroups.length > 0 && (
        <div style={{ padding: '16px 20px 0' }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: C.sub, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 8 }}>{isSelf ? 'Your groups' : 'Shared groups'}</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
            {myGroups.map(g => <button key={g.id} onClick={() => go({ name: 'group', id: g.id })} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 13px 7px 11px', borderRadius: 999, background: C.soft, border: `1px solid ${C.line}`, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: C.ink }}><span style={{ fontSize: 15 }}>{g.emoji}</span>{g.name}</button>)}
          </div>
        </div>
      )}
      <Sheet open={editP} onClose={() => setEditP(false)} title={isSelf ? 'Edit your profile' : `Edit ${personName}`}>
        <Label>Email</Label>
        <Input value={email} onChange={setEmail} placeholder="name@email.com" type="email" autoFocus />
        <div style={{ height: 18 }} />
        <Btn onClick={() => { dispatch('ProfileSet', state.groups[0] ? state.groups[0].id : 'profile', { name: personName, email: email.trim() }); setEditP(false); }}>Save profile</Btn>
      </Sheet>
      <Sheet open={verifyOpen} onClose={() => setVerifyOpen(false)} title={`Verify ${personName}`}>
        <div style={{ fontSize: 14, color: C.sub, marginBottom: 14, lineHeight: 1.5 }}>Compare this code with {personName} in person or on a call. If it matches, their key is genuine — they'll be marked verified across {memberRefs.length === 1 ? 'your shared group' : `all ${memberRefs.length} shared groups`}.</div>
        <div style={{ borderRadius: 16, border: `1px solid ${C.line}`, background: C.soft, padding: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          {safetyCode(personName).map(([n, w], i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '9px 12px', borderRadius: 11, background: C.bg, border: `1px solid ${C.line}` }}>
              <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12.5, fontWeight: 700, color: C.faint }}>{n}</span>
              <span style={{ fontSize: 15, fontWeight: 700, color: C.ink }}>{w}</span>
            </div>
          ))}
        </div>
        <div style={{ height: 16 }} />
        <Btn color={C.green} onClick={() => { memberRefs.forEach(x => { if (!x.m.verified) dispatch('MemberVerified', x.g.id, { memberId: x.m.id }); }); setVerifyOpen(false); }}><svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke="#fff" strokeWidth="2.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>They match</Btn>
        <div style={{ height: 8 }} />
        <Btn kind="text" onClick={() => setVerifyOpen(false)}>They don't match</Btn>
      </Sheet>
      <GlobalRecordPayment open={payOpen} onClose={() => setPayOpen(false)} state={state} dispatch={dispatch} initialPerson={personName} />
      <div style={{ padding: '0 20px' }}>
        <div style={{ fontSize: 13, fontWeight: 700, color: C.sub, margin: '22px 0 2px' }}>Shared expenses &amp; payments</div>
        {entries.length === 0 && <Empty text="Nothing shared yet." />}
        {entries.map(({ g, e, eff }) => { const isExp = e.type === 'expense'; const v = rowVals({ g, e, eff }); return (
          <div key={e.id} onClick={() => go({ name: 'expense', gid: g.id, eid: e.id, back: { name: 'person', person: personName, back: { name: 'people' } } })} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '11px 0', borderBottom: `1px solid ${C.soft}`, cursor: 'pointer' }}>
            <div style={{ width: 38, height: 38, borderRadius: 11, background: isExp ? C.soft2 : `${C.green}14`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 17, flexShrink: 0 }}>{isExp ? catEmoji(eff.category) : <svg width="18" height="18" viewBox="0 0 24 24"><path d="M5 13l4 4L19 6" stroke={C.green} strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}</div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 15, fontWeight: 700, color: C.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{isExp ? eff.desc : `${nameOf(g, eff.from)} → ${nameOf(g, eff.to)}`}</div>
              <div style={{ fontSize: 12, color: C.sub, marginTop: 1 }}>{g.emoji} {g.name} · {relDate(e.date)}</div>
              <div style={{ fontSize: 11.5, marginTop: 3 }}><span style={{ color: v.paid > 0.005 ? C.green : C.faint, fontWeight: 600 }}>paid {cur}{v.paid.toFixed(2)}</span> <span style={{ color: C.faint }}>·</span> <span style={{ color: v.net > 0.005 ? C.green : v.net < -0.005 ? C.orange : C.faint, fontWeight: 600 }}>net {v.net > 0.005 ? '+' : v.net < -0.005 ? '−' : ''}{cur}{Math.abs(v.net).toFixed(2)}</span></div>
            </div>
            <div style={{ textAlign: 'right', flexShrink: 0 }}>
              <div style={{ fontSize: 10, color: C.faint, textTransform: 'uppercase', letterSpacing: 0.3, fontWeight: 700 }}>{isExp ? 'spend' : 'received'}</div>
              <div style={{ fontSize: 15.5, fontWeight: 800, color: v.share > 0.005 ? C.ink : C.faint }}>{cur}{v.share.toFixed(2)}</div>
            </div>
          </div>
        ); })}
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(
  window.AuthGate ? <window.AuthGate><App /></window.AuthGate> : <App />
);
