// NEXUS PLATFORM — Document Chain Builder (Vertical + Power Features)
// Enterprise-scale drag-and-drop linkage workbench with inference engine,
// right-click context menus on every surface, multi-select bulk ops,
// undo/redo history, keyboard shortcuts, chronological auto-arrange,
// phase operations (duplicate, merge, recolor, sort, notes), flags,
// pins, anchors, priority overrides, date-conflict validator, JSON
// export, preset loader, and an activity log.

const Tndc = window.ArbiterTokens;
const {
  useState: useNxDcState,
  useMemo: useNxDcMemo,
  useRef: useNxDcRef,
  useEffect: useNxDcEffect,
  useCallback: useNxDcCb,
} = React;

// ---------- Inference engine ----------
function __nxInferLink(a, b, allLinks) {
  if (!a || !b) return null;
  let score = 0;
  const reasons = [];
  if (a.matter && b.matter && a.matter === b.matter) { score += 0.32; reasons.push('same matter'); }
  else if (a.matter && b.matter) reasons.push('cross-matter');
  const explicit = allLinks && allLinks.find(l => (l.from === a.id && l.to === b.id) || (l.from === b.id && l.to === a.id));
  if (explicit) { score += 0.48; reasons.push(`linked: ${explicit.type.replace(/-/g, ' ')}`); }
  const ta = __nxParseDate(a.dateRange);
  const tb = __nxParseDate(b.dateRange);
  let days = null;
  if (ta != null && tb != null) {
    days = Math.round((tb - ta) / 86400000);
    const abs = Math.abs(days);
    if (abs < 90) { score += 0.18; reasons.push(`${abs}d apart`); }
    else if (abs < 365) { score += 0.12; reasons.push(`${Math.round(abs / 30)}mo apart`); }
    else if (abs < 365 * 3) { score += 0.06; reasons.push(`${Math.round(abs / 365)}yr apart`); }
  }
  const toks = (s) => (s || '').toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 3);
  const ka = new Set([...toks(a.name), ...toks(a.summary), ...(a.aliases || []).flatMap(toks)]);
  const kb = new Set([...toks(b.name), ...toks(b.summary), ...(b.aliases || []).flatMap(toks)]);
  let shared = 0;
  ka.forEach(t => { if (kb.has(t)) shared++; });
  if (shared) { score += Math.min(0.22, shared * 0.05); reasons.push(`${shared} keyword${shared > 1 ? 's' : ''}`); }
  let verb = 'relates to';
  if (explicit) verb = explicit.type.replace(/-/g, ' ');
  else if (days != null && days > 0) verb = 'precedes';
  else if (days != null && days < 0) verb = 'follows';
  else if (shared >= 3) verb = 'corroborates';
  return { verb, score: Math.min(1, score), reasons, days, explicit: !!explicit };
}

function __nxParseDate(s) {
  if (!s) return null;
  const m = String(s).match(/^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?/);
  if (!m) return null;
  return Date.UTC(+m[1], (+m[2] || 1) - 1, +m[3] || 1);
}
function __nxConfidenceColor(s) { return s >= 0.7 ? '#059669' : s >= 0.45 ? '#F59E0B' : '#B45309'; }
function __nxFmtDays(d) { if (d == null) return '—'; const a = Math.abs(d); return a < 60 ? `${d}d` : a < 720 ? `${Math.round(d / 30)}mo` : `${(d / 365).toFixed(1)}yr`; }

let __nxGroupSeq = 0;
function __nxNewGroupId() { return `G-${Date.now().toString(36)}-${(__nxGroupSeq++).toString(36)}`; }
const GROUP_COLORS = ['#C026D3', '#3B82F6', '#0D9488', '#F59E0B', '#A855F7', '#059669', '#DB2777', '#6366F1', '#EA580C', '#14B8A6'];

function __nxSeedGroups(allDocs) {
  const byId = Object.fromEntries(allDocs.map(d => [d.id, d]));
  const pick = (ids) => ids.map(id => byId[id]).filter(Boolean).map(d => d.id);
  return [
    { id: __nxNewGroupId(), name: 'Phase 1 — LBO Origination', color: GROUP_COLORS[0], collapsed: false, note: '2022 transaction paperwork that seeded the conspiracy.', docIds: pick(['E-D-001', 'E-D-003', 'E-D-005']) },
    { id: __nxNewGroupId(), name: 'Phase 2 — Apex Formation & Wires', color: GROUP_COLORS[1], collapsed: false, note: 'Board approval, shell formation, and the $14.2M wire series.', docIds: pick(['E-D-002', 'E-D-004']) },
    { id: __nxNewGroupId(), name: 'Phase 3 — Parallel Matters', color: GROUP_COLORS[2], collapsed: false, note: 'Related antitrust and IP threads referenced in discovery.', docIds: pick(['E-D-008', 'E-D-006', 'E-D-007']) },
  ].filter(g => g.docIds.length > 0);
}

// ---------- Context Menu component ----------
function NxCtxMenu({ menu, onClose, children }) {
  const ref = useNxDcRef(null);
  useNxDcEffect(() => {
    if (!menu) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const onEsc = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onEsc);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onEsc); };
  }, [menu, onClose]);
  if (!menu) return null;
  // Clamp so menu stays on-screen
  const W = 240, H = 380;
  const vx = Math.min(menu.x, window.innerWidth - W - 8);
  const vy = Math.min(menu.y, window.innerHeight - H - 8);
  return (
    <div ref={ref} style={{
      position: 'fixed', left: vx, top: vy, zIndex: 1000, minWidth: `${W}px`, maxHeight: `${H}px`, overflowY: 'auto',
      background: Tndc.color.bg.card, border: `1px solid ${Tndc.color.border.medium}`,
      borderRadius: Tndc.radius.md, boxShadow: '0 10px 30px rgba(15,23,42,0.18), 0 2px 6px rgba(15,23,42,0.08)',
      padding: '4px',
      fontFamily: Tndc.font.family,
    }}>
      {children}
    </div>
  );
}

function MenuItem({ icon, label, shortcut, onClick, disabled, danger, subItems }) {
  const nx = window.__nx;
  const [openSub, setOpenSub] = useNxDcState(false);
  return (
    <div
      onMouseEnter={() => setOpenSub(true)}
      onMouseLeave={() => setOpenSub(false)}
      style={{ position: 'relative' }}
    >
      <button
        onClick={disabled ? undefined : onClick}
        disabled={disabled}
        style={{
          width: '100%', display: 'grid', gridTemplateColumns: '18px 1fr auto', alignItems: 'center', gap: '8px',
          padding: '6px 10px', border: 'none', background: 'transparent', cursor: disabled ? 'not-allowed' : 'pointer',
          borderRadius: '4px', fontSize: '12px', fontFamily: Tndc.font.family,
          color: danger ? nx.crimson : (disabled ? Tndc.color.text.tertiary : Tndc.color.text.primary),
          textAlign: 'left',
        }}
        onMouseOver={(e) => { if (!disabled) e.currentTarget.style.background = Tndc.color.bg.secondary; }}
        onMouseOut={(e) => { e.currentTarget.style.background = 'transparent'; }}
      >
        <span style={{ fontSize: '12px', color: danger ? nx.crimson : Tndc.color.text.tertiary, textAlign: 'center' }}>{icon || ''}</span>
        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
        <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>{subItems ? '▸' : (shortcut || '')}</span>
      </button>
      {subItems && openSub && (
        <div style={{
          position: 'absolute', left: '100%', top: 0, marginLeft: '2px',
          minWidth: '180px', background: Tndc.color.bg.card, border: `1px solid ${Tndc.color.border.medium}`,
          borderRadius: Tndc.radius.md, boxShadow: '0 10px 30px rgba(15,23,42,0.18)',
          padding: '4px', zIndex: 1001, maxHeight: '260px', overflowY: 'auto',
        }}>
          {subItems}
        </div>
      )}
    </div>
  );
}

function MenuSep() {
  return <div style={{ height: '1px', background: Tndc.color.border.light, margin: '4px 6px' }} />;
}

function MenuLabel({ children }) {
  return <div style={{ padding: '4px 10px', fontSize: '9px', fontWeight: 700, color: Tndc.color.text.tertiary, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{children}</div>;
}

// ---------- Main Component ----------
function NexusDocumentChain({ data }) {
  const nx = window.__nx;
  const allDocs = useNxDcMemo(() => data.entities.filter(e => e.type === 'document'), [data]);
  const docById = useNxDcMemo(() => Object.fromEntries(allDocs.map(d => [d.id, d])), [allDocs]);

  // ---------- State ----------
  const [groups, setGroups] = useNxDcState(() => __nxSeedGroups(allDocs));
  const [flags, setFlags] = useNxDcState({});               // { [docId]: { pinned, flagged, anchor, priority } }
  const [selection, setSelection] = useNxDcState(new Set()); // Set<docId> for multi-select
  const [history, setHistory] = useNxDcState({ past: [], future: [] });
  const [ctxMenu, setCtxMenu] = useNxDcState(null);
  const [activityLog, setActivityLog] = useNxDcState([]);

  const [matterFilter, setMatterFilter] = useNxDcState('All matters');
  const [libSearch, setLibSearch] = useNxDcState('');
  const [chainSearch, setChainSearch] = useNxDcState('');
  const [density, setDensity] = useNxDcState('comfortable');
  const [selectedCard, setSelectedCard] = useNxDcState(null); // { gi, di }
  const [drag, setDrag] = useNxDcState(null);
  const [dropTarget, setDropTarget] = useNxDcState(null);
  const [collapsedAll, setCollapsedAll] = useNxDcState(false);
  const [multiMode, setMultiMode] = useNxDcState(false);
  const [filterFlag, setFilterFlag] = useNxDcState('all'); // all | pinned | flagged | anchor | weak

  const matters = useNxDcMemo(() => ['All matters', ...Array.from(new Set(allDocs.map(d => d.matter)))], [allDocs]);
  const chainDocIds = useNxDcMemo(() => new Set(groups.flatMap(g => g.docIds)), [groups]);
  const chainCount = chainDocIds.size;

  // ---------- History (undo/redo) ----------
  const snapshot = () => JSON.parse(JSON.stringify({ groups, flags }));
  const commit = (mutator, msg) => {
    setHistory(h => ({ past: [...h.past, snapshot()].slice(-50), future: [] }));
    mutator();
    if (msg) logActivity(msg);
  };
  const undo = () => setHistory(h => {
    if (!h.past.length) return h;
    const prev = h.past[h.past.length - 1];
    const curr = snapshot();
    setGroups(prev.groups);
    setFlags(prev.flags);
    logActivity('↶ Undo');
    return { past: h.past.slice(0, -1), future: [...h.future, curr].slice(-50) };
  });
  const redo = () => setHistory(h => {
    if (!h.future.length) return h;
    const next = h.future[h.future.length - 1];
    const curr = snapshot();
    setGroups(next.groups);
    setFlags(next.flags);
    logActivity('↷ Redo');
    return { past: [...h.past, curr].slice(-50), future: h.future.slice(0, -1) };
  });
  const logActivity = (msg) => setActivityLog(l => [{ msg, t: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }, ...l].slice(0, 25));

  // ---------- Derived data ----------
  const flatChain = useNxDcMemo(() => {
    const out = [];
    groups.forEach((g, gi) => g.docIds.forEach((id, di) => {
      const d = docById[id]; if (d) out.push({ doc: d, gi, di, groupId: g.id });
    }));
    return out;
  }, [groups, docById]);

  const pairs = useNxDcMemo(() => {
    const out = [];
    for (let i = 0; i < flatChain.length - 1; i++) out.push(__nxInferLink(flatChain[i].doc, flatChain[i + 1].doc, data.links));
    return out;
  }, [flatChain, data.links]);

  const avgConfidence = pairs.length ? Math.round((pairs.reduce((s, p) => s + (p?.score || 0), 0) / pairs.length) * 100) : 0;
  const explicitCount = pairs.filter(p => p?.explicit).length;
  const weakCount = pairs.filter(p => (p?.score || 0) < 0.45).length;
  const matterSet = new Set(flatChain.map(x => x.doc.matter));
  const dates = flatChain.map(x => __nxParseDate(x.doc.dateRange)).filter(v => v != null).sort((a, b) => a - b);
  const spanDays = dates.length >= 2 ? Math.round((dates[dates.length - 1] - dates[0]) / 86400000) : 0;

  // Date conflicts: consecutive cards where next date < prev date
  const conflictSet = useNxDcMemo(() => {
    const s = new Set();
    for (let i = 1; i < flatChain.length; i++) {
      const a = __nxParseDate(flatChain[i - 1].doc.dateRange);
      const b = __nxParseDate(flatChain[i].doc.dateRange);
      if (a != null && b != null && b < a) s.add(flatChain[i].doc.id);
    }
    return s;
  }, [flatChain]);

  const groupStats = useNxDcMemo(() => groups.map(g => {
    const docs = g.docIds.map(id => docById[id]).filter(Boolean);
    const gd = docs.map(d => __nxParseDate(d.dateRange)).filter(v => v != null).sort((a, b) => a - b);
    const years = gd.length ? [new Date(gd[0]).getUTCFullYear(), new Date(gd[gd.length - 1]).getUTCFullYear()] : null;
    let sum = 0, n = 0;
    for (let i = 0; i < docs.length - 1; i++) {
      const p = __nxInferLink(docs[i], docs[i + 1], data.links);
      if (p) { sum += p.score; n++; }
    }
    return {
      count: docs.length,
      years: years ? (years[0] === years[1] ? `${years[0]}` : `${years[0]}–${years[1]}`) : '—',
      avg: n ? Math.round((sum / n) * 100) : null,
      keyCount: docs.filter(d => d.priority === 'key').length,
      flaggedCount: docs.filter(d => flags[d.id]?.flagged).length,
      anchorCount: docs.filter(d => flags[d.id]?.anchor).length,
    };
  }), [groups, docById, data.links, flags]);

  // Filter chain cards by flag filter
  const shouldDim = (docId) => {
    if (filterFlag === 'all') return false;
    const f = flags[docId] || {};
    if (filterFlag === 'pinned') return !f.pinned;
    if (filterFlag === 'flagged') return !f.flagged;
    if (filterFlag === 'anchor') return !f.anchor;
    if (filterFlag === 'weak') {
      const idx = flatChain.findIndex(x => x.doc.id === docId);
      if (idx < 1) return true;
      return (pairs[idx - 1]?.score || 0) >= 0.45;
    }
    return false;
  };

  const libraryDocs = useNxDcMemo(() => allDocs
    .filter(d => !chainDocIds.has(d.id))
    .filter(d => matterFilter === 'All matters' || d.matter === matterFilter)
    .filter(d => !libSearch || (d.name + ' ' + d.summary + ' ' + d.id + ' ' + d.matter).toLowerCase().includes(libSearch.toLowerCase())),
  [allDocs, chainDocIds, matterFilter, libSearch]);

  // ---------- Operations ----------
  const setGroupsCommit = (fn, msg) => commit(() => setGroups(fn), msg);
  const setFlagsCommit = (fn, msg) => commit(() => setFlags(fn), msg);

  const toggleCollapse = (gi) => setGroups(gs => gs.map((g, i) => i === gi ? { ...g, collapsed: !g.collapsed } : g));
  const setAllCollapsed = (v) => { setCollapsedAll(v); setGroups(gs => gs.map(g => ({ ...g, collapsed: v }))); };
  const renameGroup = (gi, name) => setGroups(gs => gs.map((g, i) => i === gi ? { ...g, name } : g));
  const setGroupNote = (gi, note) => setGroupsCommit(gs => gs.map((g, i) => i === gi ? { ...g, note } : g), 'Edited phase note');
  const recolorGroup = (gi, color) => setGroupsCommit(gs => gs.map((g, i) => i === gi ? { ...g, color } : g), 'Recolored phase');
  const addGroup = () => setGroupsCommit(gs => [...gs, { id: __nxNewGroupId(), name: `New phase ${gs.length + 1}`, color: GROUP_COLORS[gs.length % GROUP_COLORS.length], collapsed: false, note: '', docIds: [] }], 'Added phase');
  const removeGroup = (gi) => setGroupsCommit(gs => gs.length <= 1 ? gs.map((g, i) => i === gi ? { ...g, docIds: [] } : g) : gs.filter((_, i) => i !== gi), 'Removed phase');
  const moveGroup = (gi, dir) => setGroupsCommit(gs => {
    const next = gs.slice();
    const t = gi + dir;
    if (t < 0 || t >= gs.length) return gs;
    [next[gi], next[t]] = [next[t], next[gi]];
    return next;
  }, dir < 0 ? 'Moved phase up' : 'Moved phase down');
  const duplicateGroup = (gi) => setGroupsCommit(gs => {
    const src = gs[gi]; if (!src) return gs;
    const copy = { ...src, id: __nxNewGroupId(), name: `${src.name} (copy)`, docIds: [] };
    return [...gs.slice(0, gi + 1), copy, ...gs.slice(gi + 1)];
  }, 'Duplicated phase (shell)');
  const mergeIntoPrev = (gi) => setGroupsCommit(gs => {
    if (gi === 0) return gs;
    const next = gs.slice();
    next[gi - 1] = { ...next[gi - 1], docIds: [...next[gi - 1].docIds, ...next[gi].docIds] };
    next.splice(gi, 1);
    return next;
  }, 'Merged phase into previous');
  const sortGroupByDate = (gi, dir = 1) => setGroupsCommit(gs => gs.map((g, i) => {
    if (i !== gi) return g;
    const ids = g.docIds.slice();
    ids.sort((a, b) => {
      const fa = flags[a]?.pinned ? -1 : 0;
      const fb = flags[b]?.pinned ? -1 : 0;
      if (fa !== fb) return fa - fb;
      const ta = __nxParseDate(docById[a]?.dateRange) || 0;
      const tb = __nxParseDate(docById[b]?.dateRange) || 0;
      return (ta - tb) * dir;
    });
    return { ...g, docIds: ids };
  }), dir > 0 ? 'Sorted phase by date ↑' : 'Sorted phase by date ↓');

  const autoArrangeChronological = () => setGroupsCommit(gs => {
    // Flatten all docs, sort by date, redistribute across existing group buckets by year clusters
    const all = gs.flatMap(g => g.docIds);
    all.sort((a, b) => {
      const fa = flags[a]?.pinned ? -1 : 0;
      const fb = flags[b]?.pinned ? -1 : 0;
      if (fa !== fb) return fa - fb;
      return (__nxParseDate(docById[a]?.dateRange) || 0) - (__nxParseDate(docById[b]?.dateRange) || 0);
    });
    // Distribute evenly across groups
    if (gs.length === 0) return gs;
    const per = Math.ceil(all.length / gs.length);
    return gs.map((g, i) => ({ ...g, docIds: all.slice(i * per, (i + 1) * per) }));
  }, 'Auto-arranged chronologically');

  const toggleFlag = (docId, key) => setFlagsCommit(f => ({
    ...f, [docId]: { ...(f[docId] || {}), [key]: !(f[docId]?.[key]) },
  }), `Toggled ${key}`);
  const setPriorityOverride = (docId, p) => setFlagsCommit(f => ({
    ...f, [docId]: { ...(f[docId] || {}), priority: p },
  }), `Set priority ${p}`);

  const removeFromChain = (docId) => setGroupsCommit(gs => gs.map(g => ({ ...g, docIds: g.docIds.filter(id => id !== docId) })), 'Removed document');
  const clearChain = () => setGroupsCommit(gs => gs.map(g => ({ ...g, docIds: [] })), 'Cleared chain');

  const moveDocToPhase = (docId, targetGi) => setGroupsCommit(gs => {
    const cleaned = gs.map(g => ({ ...g, docIds: g.docIds.filter(id => id !== docId) }));
    if (targetGi < 0 || targetGi >= cleaned.length) return cleaned;
    cleaned[targetGi].docIds = [...cleaned[targetGi].docIds, docId];
    return cleaned;
  }, 'Moved to phase');

  // Multi-select bulk ops
  const selArray = () => [...selection];
  const bulkRemove = () => {
    if (!selection.size) return;
    const ids = selArray();
    setGroupsCommit(gs => gs.map(g => ({ ...g, docIds: g.docIds.filter(id => !selection.has(id)) })), `Bulk removed ${ids.length}`);
    setSelection(new Set());
  };
  const bulkMove = (targetGi) => {
    if (!selection.size) return;
    const ids = selArray();
    setGroupsCommit(gs => {
      const cleaned = gs.map(g => ({ ...g, docIds: g.docIds.filter(id => !selection.has(id)) }));
      if (targetGi < 0 || targetGi >= cleaned.length) return cleaned;
      cleaned[targetGi].docIds = [...cleaned[targetGi].docIds, ...ids];
      return cleaned;
    }, `Bulk moved ${ids.length}`);
    setSelection(new Set());
  };
  const bulkFlag = (key) => {
    if (!selection.size) return;
    const ids = selArray();
    setFlagsCommit(f => { const next = { ...f }; ids.forEach(id => { next[id] = { ...(next[id] || {}), [key]: true }; }); return next; }, `Bulk ${key} ${ids.length}`);
  };

  // Export / import
  const exportChain = async () => {
    const payload = { version: 1, exported: new Date().toISOString(), groups, flags };
    const json = JSON.stringify(payload, null, 2);
    try { await navigator.clipboard.writeText(json); logActivity(' Exported JSON to clipboard'); }
    catch (_) { logActivity('Exported (clipboard unavailable)'); }
    // Also open as blob in a new tab
    try {
      const blob = new Blob([json], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a'); a.href = url; a.download = 'doc-chain.json'; a.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } catch (_) {}
  };

  // Load preset from data.chains
  const loadPreset = (chainId) => {
    const chain = data.chains.find(c => c.id === chainId);
    if (!chain) return;
    const docIds = [];
    const walk = (n) => { if (!n) return; if (n.type === 'document' && docById[n.id]) docIds.push(n.id); (n.children || []).forEach(walk); };
    walk(chain.root);
    setGroupsCommit(gs => [{
      id: __nxNewGroupId(), name: chain.name, color: GROUP_COLORS[0], collapsed: false,
      note: chain.matter, docIds,
    }], `Loaded preset: ${chain.name}`);
  };

  // ---------- Drag & drop ----------
  const startDrag = (docId, from, e) => {
    setDrag({ docId, from });
    try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', docId); } catch (_) {}
  };
  const endDrag = () => { setDrag(null); setDropTarget(null); };
  const onSlotOver = (gi, di, e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDropTarget({ gi, di }); };
  const onSlotDrop = (gi, di, e) => {
    e.preventDefault();
    const docId = drag?.docId;
    if (!docId) return endDrag();
    setGroupsCommit(gs => {
      const next = gs.map(g => ({ ...g, docIds: g.docIds.slice() }));
      let fromGi = -1, fromDi = -1;
      next.forEach((g, i) => { const idx = g.docIds.indexOf(docId); if (idx >= 0) { fromGi = i; fromDi = idx; } });
      if (fromGi >= 0) next[fromGi].docIds.splice(fromDi, 1);
      let targetDi = di;
      if (fromGi === gi && fromDi < di) targetDi = di - 1;
      if (!next[gi]) return gs;
      next[gi].docIds.splice(Math.min(targetDi, next[gi].docIds.length), 0, docId);
      return next;
    }, 'Moved document');
    endDrag();
  };
  const onLibraryDrop = (e) => {
    e.preventDefault();
    if (!drag || drag.from === 'library') return endDrag();
    const docId = drag.docId;
    setGroupsCommit(gs => gs.map(g => ({ ...g, docIds: g.docIds.filter(id => id !== docId) })), 'Returned to library');
    endDrag();
  };

  // ---------- Keyboard shortcuts ----------
  useNxDcEffect(() => {
    const onKey = (e) => {
      // Avoid hijacking when typing in inputs
      const tag = (e.target.tagName || '').toLowerCase();
      if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
      const meta = e.ctrlKey || e.metaKey;
      if (meta && e.key.toLowerCase() === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
      else if (meta && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) { e.preventDefault(); redo(); }
      else if (meta && e.key.toLowerCase() === 'a') {
        e.preventDefault();
        setSelection(new Set(flatChain.map(x => x.doc.id)));
        setMultiMode(true);
      }
      else if (e.key === 'Escape') { setSelection(new Set()); setCtxMenu(null); setMultiMode(false); }
      else if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selection.size) { bulkRemove(); }
        else if (selectedCard) {
          const doc = groups[selectedCard.gi]?.docIds[selectedCard.di];
          if (doc) removeFromChain(doc);
        }
      } else if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && selectedCard) {
        e.preventDefault();
        const cur = flatChain.findIndex(x => x.gi === selectedCard.gi && x.di === selectedCard.di);
        if (cur < 0) return;
        const nextIdx = cur + (e.key === 'ArrowDown' ? 1 : -1);
        if (nextIdx < 0 || nextIdx >= flatChain.length) return;
        setSelectedCard({ gi: flatChain[nextIdx].gi, di: flatChain[nextIdx].di });
      } else if (e.key === 'p' && selectedCard) {
        const doc = groups[selectedCard.gi]?.docIds[selectedCard.di];
        if (doc) toggleFlag(doc, 'pinned');
      } else if (e.key === 'f' && selectedCard) {
        const doc = groups[selectedCard.gi]?.docIds[selectedCard.di];
        if (doc) toggleFlag(doc, 'flagged');
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });

  // ---------- Context menu builders ----------
  const openCardMenu = (e, docId, gi, di) => {
    e.preventDefault(); e.stopPropagation();
    setCtxMenu({ kind: 'card', x: e.clientX, y: e.clientY, docId, gi, di });
  };
  const openPhaseMenu = (e, gi) => {
    e.preventDefault(); e.stopPropagation();
    setCtxMenu({ kind: 'phase', x: e.clientX, y: e.clientY, gi });
  };
  const openTrackMenu = (e) => {
    e.preventDefault();
    setCtxMenu({ kind: 'track', x: e.clientX, y: e.clientY });
  };
  const closeMenu = () => setCtxMenu(null);
  const run = (fn) => () => { fn(); closeMenu(); };

  // ---------- Rendering ----------
  const cardPad = density === 'compact' ? '6px 10px' : '10px 12px';
  const cardGap = density === 'compact' ? '4px' : '8px';

  const selectedDoc = selectedCard ? (docById[groups[selectedCard.gi]?.docIds[selectedCard.di]] || null) : null;
  const selectedFlatIdx = selectedCard ? flatChain.findIndex(x => x.gi === selectedCard.gi && x.di === selectedCard.di) : -1;
  const selectedPair = selectedFlatIdx > 0 ? pairs[selectedFlatIdx - 1] : null;
  const selectedLinks = selectedDoc ? data.links.filter(l => l.from === selectedDoc.id || l.to === selectedDoc.id) : [];

  const connectorBar = (infer) => {
    if (!infer) return null;
    const color = __nxConfidenceColor(infer.score);
    return (
      <div style={{ display: 'flex', alignItems: 'stretch', gap: '8px', padding: '4px 0', paddingLeft: '14px' }}>
        <div style={{ width: '3px', background: `linear-gradient(${color}, ${color}66)`, borderRadius: '2px', flexShrink: 0 }} />
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', paddingLeft: '6px' }}>
          <span style={{ fontSize: '10px', fontFamily: Tndc.font.mono, fontWeight: 700, color, padding: '1px 8px', borderRadius: '10px', background: `${color}15`, border: `1px solid ${color}33` }}>{Math.round(infer.score * 100)}%</span>
          <span style={{ fontSize: '11px', color: Tndc.color.text.primary, fontWeight: 600, fontStyle: 'italic' }}>{infer.verb}</span>
          {infer.explicit && <span style={{ ...nx.tag, background: nx.artifactBg, color: nx.artifact, fontSize: '9px' }}>ok grounded</span>}
          {infer.reasons.slice(0, 3).map((r, i) => (<span key={i} style={{ fontSize: '10px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>· {r}</span>))}
          {infer.days != null && <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>· Δ {__nxFmtDays(infer.days)}</span>}
        </div>
      </div>
    );
  };

  const dropSlot = (gi, di, isActive) => (
    <div
      onDragOver={(e) => onSlotOver(gi, di, e)}
      onDrop={(e) => onSlotDrop(gi, di, e)}
      onDragLeave={() => { if (dropTarget && dropTarget.gi === gi && dropTarget.di === di) setDropTarget(null); }}
      style={{
        height: isActive ? '44px' : '8px', margin: '2px 0', borderRadius: '6px',
        border: isActive ? `2px dashed ${nx.fuchsia}` : '2px dashed transparent',
        background: isActive ? nx.fuchsiaBg : 'transparent', transition: 'all 0.12s',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: '10px', fontWeight: 700, color: nx.fuchsia, letterSpacing: '0.08em', textTransform: 'uppercase',
      }}
    >{isActive && 'drop here'}</div>
  );

  const toggleSelect = (docId) => setSelection(s => { const n = new Set(s); n.has(docId) ? n.delete(docId) : n.add(docId); return n; });

  const renderDocCard = (doc, gi, di, isDragging) => {
    const isSelected = selectedCard && selectedCard.gi === gi && selectedCard.di === di;
    const isMatch = chainSearch && (doc.name + ' ' + doc.summary + ' ' + doc.id).toLowerCase().includes(chainSearch.toLowerCase());
    const dim = (chainSearch && !isMatch) || shouldDim(doc.id);
    const f = flags[doc.id] || {};
    const pri = f.priority || doc.priority;
    const ts = nx.typeStyle('document');
    const isChecked = selection.has(doc.id);
    const hasConflict = conflictSet.has(doc.id);

    return (
      <div
        draggable={!multiMode}
        onDragStart={(e) => startDrag(doc.id, { gi, di }, e)}
        onDragEnd={endDrag}
        onClick={(e) => {
          if (e.shiftKey || multiMode) { toggleSelect(doc.id); setMultiMode(true); }
          else setSelectedCard({ gi, di });
        }}
        onContextMenu={(e) => openCardMenu(e, doc.id, gi, di)}
        style={{
          display: 'grid',
          gridTemplateColumns: `${multiMode ? '18px ' : ''}minmax(36px, auto) 1fr auto`,
          alignItems: 'center', gap: cardGap, padding: cardPad,
          background: dim ? Tndc.color.bg.secondary : (isChecked ? nx.fuchsiaBg : Tndc.color.bg.card),
          border: `1px solid ${isSelected ? nx.fuchsia : (hasConflict ? nx.crimson : Tndc.color.border.light)}`,
          borderLeft: `3px solid ${f.anchor ? '#C026D3' : nx.doc}`,
          borderRadius: Tndc.radius.md, cursor: 'grab',
          opacity: isDragging ? 0.35 : (dim ? 0.45 : 1),
          boxShadow: isSelected ? `0 0 0 2px ${nx.fuchsiaBg}, 0 2px 6px rgba(192,38,211,0.12)` : '0 1px 2px rgba(15,23,42,0.04)',
          userSelect: 'none', transition: 'opacity 0.1s, box-shadow 0.1s',
        }}
        title="Drag · click: select · shift-click: multi · right-click: menu"
      >
        {multiMode && (
          <input type="checkbox" checked={isChecked} onClick={(e) => e.stopPropagation()} onChange={() => toggleSelect(doc.id)} style={{ cursor: 'pointer' }} />
        )}
        <div style={{ display: 'flex', alignItems: 'center', gap: '4px', color: Tndc.color.text.tertiary, fontSize: '14px', fontFamily: Tndc.font.mono }}>
          <span style={{ lineHeight: 1 }}>⋮⋮</span>
          <span style={{ fontSize: '10px', fontWeight: 700, color: nx.fuchsia }}>#{(flatChain.findIndex(x => x.gi === gi && x.di === di) + 1).toString().padStart(2, '0')}</span>
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: density === 'compact' ? 0 : '2px', flexWrap: 'wrap' }}>
            <span style={{ ...nx.tag, background: ts.bg, color: ts.color, fontSize: '9px' }}>{ts.icon}</span>
            <span style={{ fontSize: '10px', fontFamily: Tndc.font.mono, color: nx.doc, fontWeight: 700 }}>{doc.id}</span>
            <span style={{ fontSize: '10px', fontFamily: Tndc.font.mono, color: hasConflict ? nx.crimson : nx.event }}>· {doc.dateRange || 'undated'}</span>
            {f.pinned && <span title="Pinned" style={{ fontSize: '11px' }}></span>}
            {f.flagged && <span title="Flagged" style={{ fontSize: '11px', color: nx.crimson }}><Icons.Flag size={11}/></span>}
            {f.anchor && <span title="Anchor" style={{ fontSize: '11px', color: nx.fuchsia }}></span>}
            {hasConflict && <span title="Date out of order" style={{ ...nx.tag, background: nx.crimsonBg, color: nx.crimson, fontSize: '9px' }}>! date</span>}
          </div>
          <div style={{ fontSize: density === 'compact' ? '11px' : '12px', fontWeight: 700, color: Tndc.color.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{doc.name}</div>
          {density !== 'compact' && (
            <div style={{ fontSize: '11px', color: Tndc.color.text.secondary, lineHeight: 1.4, marginTop: '2px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{doc.summary}</div>
          )}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 }}>
          <span style={{ fontSize: '9px', color: Tndc.color.text.tertiary, maxWidth: '110px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{doc.matter}</span>
          <span style={{ ...nx.tag, background: pri === 'key' ? nx.crimsonBg : pri === 'high' ? nx.eventBg : Tndc.color.bg.secondary, color: pri === 'key' ? nx.crimson : pri === 'high' ? nx.event : Tndc.color.text.secondary, fontSize: '9px' }}>{pri}</span>
          <span style={{ fontSize: '10px', fontFamily: Tndc.font.mono, color: nx.fuchsia, fontWeight: 700, minWidth: '32px', textAlign: 'right' }}>{doc.linkCount}↔</span>
          <button onClick={(e) => { e.stopPropagation(); removeFromChain(doc.id); }}
            style={{ border: 'none', background: 'transparent', color: Tndc.color.text.tertiary, fontSize: '16px', cursor: 'pointer', padding: '0 2px', lineHeight: 1 }}
            title="Remove">×</button>
        </div>
      </div>
    );
  };

  // ---------- Render ----------
  return (
    <div onClick={(e) => { if (ctxMenu && !e.target.closest('[data-ctx-menu]')) closeMenu(); }}>
      {/* Hero */}
      <div style={{ ...nx.card, borderLeft: `3px solid ${nx.fuchsia}`, background: `linear-gradient(135deg, ${nx.fuchsiaBg} 0%, ${nx.eventBg} 100%)` }}>
        <div style={{ padding: 'clamp(14px, 1.3vw, 20px) clamp(16px, 2vw, 28px)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '6px', flexWrap: 'wrap' }}>
            <span style={{ fontSize: 'clamp(11px, 0.8vw, 13px)', fontWeight: 700, color: nx.fuchsia, textTransform: 'uppercase', letterSpacing: '0.08em' }}>◆ Document Chain Builder</span>
            <span style={{ ...nx.tag, background: nx.fuchsiaBg, color: nx.fuchsia, border: `1px solid ${nx.fuchsiaBorder}` }}>Enterprise · Inference · Scale</span>
            <span style={{ ...nx.tag, background: Tndc.color.bg.secondary, color: Tndc.color.text.secondary, fontFamily: Tndc.font.mono, fontSize: '10px' }}>right-click anywhere</span>
          </div>
          <div style={{ fontSize: 'clamp(12px, 0.85vw + 3px, 14px)', color: Tndc.color.text.secondary, lineHeight: 1.55, maxWidth: '1040px' }}>
            Drag documents into phases, right-click for actions, shift-click for multi-select, <b>cmdZ</b> / <b>cmd⇧Z</b> to undo/redo, <b>p/f</b> to pin/flag, <b>Del</b> to remove.
            The inference engine grades every pair; the validator flags out-of-order dates.
          </div>
        </div>
      </div>

      {/* KPI strip */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(150px, 100%), 1fr))', gap: 'clamp(8px, 0.6vw + 6px, 14px)', marginBottom: 'clamp(12px, 1vw + 6px, 18px)' }}>
        <div style={nx.stat}><span style={nx.statLabel}>Chain</span><span style={{ ...nx.statValue, color: nx.fuchsia }}>{chainCount}</span><span style={{ ...nx.statDelta, color: Tndc.color.text.tertiary }}>{groups.length} phase{groups.length !== 1 ? 's' : ''}</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Avg confidence</span><span style={{ ...nx.statValue, color: __nxConfidenceColor(avgConfidence / 100) }}>{avgConfidence}%</span><span style={{ ...nx.statDelta, color: Tndc.color.text.tertiary }}>{pairs.length} pairs</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Grounded</span><span style={{ ...nx.statValue, color: nx.artifact }}>{explicitCount}</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Weak</span><span style={{ ...nx.statValue, color: '#B45309' }}>{weakCount}</span><span style={{ ...nx.statDelta, color: Tndc.color.text.tertiary }}>&lt;45%</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Conflicts</span><span style={{ ...nx.statValue, color: conflictSet.size ? nx.crimson : Tndc.color.text.primary }}>{conflictSet.size}</span><span style={{ ...nx.statDelta, color: Tndc.color.text.tertiary }}>date order</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Pinned</span><span style={{ ...nx.statValue, color: nx.event }}>{Object.values(flags).filter(f => f.pinned).length}</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Flagged</span><span style={{ ...nx.statValue, color: nx.crimson }}>{Object.values(flags).filter(f => f.flagged).length}</span></div>
        <div style={nx.stat}><span style={nx.statLabel}>Span</span><span style={{ ...nx.statValue, color: nx.event, fontSize: 'clamp(15px, 0.8vw + 10px, 20px)' }}>{spanDays ? `${spanDays}d` : '—'}</span></div>
      </div>

      {/* Toolbar */}
      <div style={{ ...nx.card, marginBottom: 'clamp(10px, 1vw, 16px)' }}>
        <div style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
          <input placeholder="Search within chain…" value={chainSearch} onChange={(e) => setChainSearch(e.target.value)}
            style={{ flex: '1 1 180px', minWidth: '160px', padding: '6px 10px', borderRadius: '6px', border: `1px solid ${Tndc.color.border.light}`, fontSize: '12px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.primary }} />

          <select value={filterFlag} onChange={(e) => setFilterFlag(e.target.value)}
            style={{ padding: '6px 10px', borderRadius: '6px', border: `1px solid ${Tndc.color.border.light}`, fontSize: '11px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.secondary }}>
            <option value="all">All</option>
            <option value="pinned"> Pinned only</option>
            <option value="flagged">flag Flagged only</option>
            <option value="anchor"> Anchors only</option>
            <option value="weak">Weak links &lt;45%</option>
          </select>

          <div style={{ display: 'flex', gap: '0', border: `1px solid ${Tndc.color.border.light}`, borderRadius: '6px', overflow: 'hidden' }}>
            {['comfortable', 'compact'].map(d => (
              <button key={d} onClick={() => setDensity(d)}
                style={{ padding: '6px 10px', fontSize: '11px', fontWeight: 600, border: 'none', cursor: 'pointer', fontFamily: Tndc.font.family, background: density === d ? nx.fuchsiaBg : Tndc.color.bg.card, color: density === d ? nx.fuchsia : Tndc.color.text.secondary }}>{d}</button>
            ))}
          </div>

          <button style={nx.btnSecondary} onClick={undo} disabled={!history.past.length} title="Undo (cmdZ)">↶ Undo</button>
          <button style={nx.btnSecondary} onClick={redo} disabled={!history.future.length} title="Redo (cmd⇧Z)">↷ Redo</button>
          <button style={nx.btnSecondary} onClick={() => setAllCollapsed(!collapsedAll)}>{collapsedAll ? 'Expand' : 'Collapse'} all</button>
          <button style={nx.btnSecondary} onClick={addGroup}>+ Phase</button>
          <button style={nx.btnSecondary} onClick={autoArrangeChronological} title="Sort entire chain by date">◷ Auto-arrange</button>
          <button style={nx.btnSecondary} onClick={() => setMultiMode(m => !m)} title="Toggle multi-select (shift-click cards)">{multiMode ? 'ok Multi' : 'Multi'}</button>
          <button style={nx.btnSecondary} onClick={exportChain} title="Export JSON">↗ Export</button>
          <select onChange={(e) => { if (e.target.value) loadPreset(e.target.value); e.target.value = ''; }}
            style={{ padding: '6px 10px', borderRadius: '6px', border: `1px solid ${Tndc.color.border.light}`, fontSize: '11px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.secondary }}>
            <option value="">Load preset…</option>
            {data.chains.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
          <button style={nx.btnSecondary} onClick={() => clearChain()} disabled={!chainCount}>Clear</button>
          <div style={{ flex: 1 }} />
          <button style={nx.btnPrimary}>Save chain →</button>
        </div>

        {/* Multi-select bulk toolbar */}
        {multiMode && selection.size > 0 && (
          <div style={{ padding: '8px 14px', borderTop: `1px solid ${Tndc.color.border.light}`, background: nx.fuchsiaBg, display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
            <span style={{ fontSize: '11px', fontWeight: 700, color: nx.fuchsia }}>{selection.size} selected</span>
            <select onChange={(e) => { if (e.target.value !== '') { bulkMove(+e.target.value); e.target.value = ''; } }}
              style={{ padding: '5px 9px', borderRadius: '5px', border: `1px solid ${nx.fuchsiaBorder}`, fontSize: '11px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.primary }}>
              <option value="">Move to phase…</option>
              {groups.map((g, i) => <option key={g.id} value={i}>{g.name}</option>)}
            </select>
            <button style={nx.btnSecondary} onClick={() => bulkFlag('pinned')}> Pin all</button>
            <button style={nx.btnSecondary} onClick={() => bulkFlag('flagged')}>flag Flag all</button>
            <button style={nx.btnSecondary} onClick={() => bulkFlag('anchor')}> Anchor all</button>
            <button style={{ ...nx.btnSecondary, color: nx.crimson, borderColor: `${nx.crimson}55` }} onClick={bulkRemove}>Remove all</button>
            <button style={nx.btnGhost} onClick={() => { setSelection(new Set()); setMultiMode(false); }}>Cancel</button>
          </div>
        )}
      </div>

      {/* Main workbench */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(300px, 100%), 1fr))', gap: 'clamp(12px, 1vw + 6px, 18px)', alignItems: 'flex-start' }}>
        {/* Outline */}
        <div style={{ ...nx.card, marginBottom: 0, flex: '0 0 auto' }}>
          <div style={nx.cardH}><span>Outline · {groups.length} phases</span><span style={{ fontSize: '10px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>{chainCount} docs</span></div>
          <div style={{ padding: '8px', display: 'flex', flexDirection: 'column', gap: '4px', maxHeight: '560px', overflowY: 'auto' }}>
            {groups.map((g, gi) => {
              const gs = groupStats[gi] || { count: 0, avg: null, years: '—' };
              return (
                <button key={g.id}
                  onClick={() => { const el = document.getElementById(`nx-phase-${g.id}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }}
                  onContextMenu={(e) => openPhaseMenu(e, gi)}
                  style={{ display: 'grid', gridTemplateColumns: '4px 1fr auto', alignItems: 'center', gap: '8px', padding: '8px 10px', borderRadius: '6px', border: 'none', cursor: 'pointer', background: Tndc.color.bg.secondary, textAlign: 'left', fontFamily: Tndc.font.family }}>
                  <span style={{ width: '4px', alignSelf: 'stretch', background: g.color, borderRadius: '2px' }} />
                  <span style={{ minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: '11px', fontWeight: 700, color: Tndc.color.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{g.name}</span>
                    <span style={{ display: 'block', fontSize: '9px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>
                      {gs.years} · {gs.count} docs{gs.avg != null ? ` · ${gs.avg}%` : ''}
                      {gs.flaggedCount ? ` · flag${gs.flaggedCount}` : ''}{gs.anchorCount ? ` · ${gs.anchorCount}` : ''}
                    </span>
                  </span>
                  <span style={{ fontSize: '10px', fontFamily: Tndc.font.mono, color: g.color, fontWeight: 700 }}>{gs.count}</span>
                </button>
              );
            })}
          </div>
          {/* Activity log */}
          <div style={{ padding: '8px 12px', borderTop: `1px solid ${Tndc.color.border.light}`, background: Tndc.color.bg.secondary }}>
            <div style={{ fontSize: '9px', fontWeight: 700, color: Tndc.color.text.tertiary, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '4px' }}>Activity · {activityLog.length}</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', maxHeight: '100px', overflowY: 'auto' }}>
              {activityLog.length === 0 ? <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary, fontStyle: 'italic' }}>No actions yet.</span>
                : activityLog.slice(0, 12).map((a, i) => (
                  <div key={i} style={{ display: 'flex', gap: '6px', fontSize: '10px' }}>
                    <span style={{ fontFamily: Tndc.font.mono, color: Tndc.color.text.tertiary }}>{a.t}</span>
                    <span style={{ color: Tndc.color.text.secondary }}>{a.msg}</span>
                  </div>
                ))}
            </div>
          </div>
        </div>

        {/* Track */}
        <div style={{ ...nx.card, marginBottom: 0, flex: '2 1 520px', minWidth: 0 }}>
          <div style={nx.cardH}>
            <span>Linkage track · vertical · {chainCount} docs / {groups.length} phases</span>
            <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary }}>drag · right-click · shift-click</span>
          </div>
          <div
            onContextMenu={openTrackMenu}
            style={{ padding: 'clamp(8px, 0.8vw, 14px)', background: Tndc.color.bg.secondary, maxHeight: '760px', overflowY: 'auto' }}
          >
            {groups.length === 0 && (
              <div style={{ padding: '40px 20px', textAlign: 'center', color: Tndc.color.text.tertiary, fontSize: '12px' }}>
                No phases. <button onClick={addGroup} style={{ ...nx.btnGhost, fontSize: '12px' }}>+ Add phase</button>
              </div>
            )}

            {groups.map((g, gi) => {
              const gs = groupStats[gi] || { count: 0, avg: null, years: '—', keyCount: 0 };
              const isTargetEmpty = drag && g.docIds.length === 0 && dropTarget && dropTarget.gi === gi;
              return (
                <div key={g.id} id={`nx-phase-${g.id}`} style={{ marginBottom: '14px' }}>
                  {/* Phase header */}
                  <div
                    onContextMenu={(e) => openPhaseMenu(e, gi)}
                    style={{ position: 'sticky', top: 0, zIndex: 2, display: 'grid', gridTemplateColumns: 'auto 4px 1fr auto', gap: '10px', alignItems: 'center', padding: '10px 12px', background: Tndc.color.bg.card, border: `1px solid ${Tndc.color.border.light}`, borderLeft: `4px solid ${g.color}`, borderRadius: Tndc.radius.md, marginBottom: g.collapsed ? 0 : '8px', boxShadow: '0 1px 3px rgba(15,23,42,0.04)' }}>
                    <button onClick={() => toggleCollapse(gi)} style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: Tndc.color.text.secondary, fontSize: '12px', padding: '0 2px' }} title={g.collapsed ? 'Expand' : 'Collapse'}>{g.collapsed ? '▶' : '▼'}</button>
                    <span style={{ width: '4px' }} />
                    <div style={{ minWidth: 0 }}>
                      <input value={g.name} onChange={(e) => renameGroup(gi, e.target.value)}
                        style={{ width: '100%', fontSize: '13px', fontWeight: 700, color: Tndc.color.text.primary, border: 'none', background: 'transparent', fontFamily: Tndc.font.family, padding: '2px 0', outline: 'none' }} />
                      <div style={{ fontSize: '10px', color: Tndc.color.text.tertiary, display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
                        <span style={{ fontFamily: Tndc.font.mono }}>{gs.count} docs</span>
                        <span style={{ fontFamily: Tndc.font.mono }}>{gs.years}</span>
                        {gs.avg != null && <span style={{ fontFamily: Tndc.font.mono, color: __nxConfidenceColor(gs.avg / 100) }}>avg {gs.avg}%</span>}
                        {gs.flaggedCount > 0 && <span style={{ color: nx.crimson, fontWeight: 700 }}>flag {gs.flaggedCount}</span>}
                        {gs.anchorCount > 0 && <span style={{ color: nx.fuchsia, fontWeight: 700 }}> {gs.anchorCount}</span>}
                      </div>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
                      <button onClick={() => moveGroup(gi, -1)} disabled={gi === 0} style={{ border: `1px solid ${Tndc.color.border.light}`, background: Tndc.color.bg.card, fontSize: '10px', padding: '2px 6px', borderRadius: '4px', cursor: 'pointer', color: Tndc.color.text.secondary }}>↑</button>
                      <button onClick={() => moveGroup(gi, 1)} disabled={gi === groups.length - 1} style={{ border: `1px solid ${Tndc.color.border.light}`, background: Tndc.color.bg.card, fontSize: '10px', padding: '2px 6px', borderRadius: '4px', cursor: 'pointer', color: Tndc.color.text.secondary }}>↓</button>
                      <button onClick={(e) => openPhaseMenu(e, gi)} style={{ border: `1px solid ${Tndc.color.border.light}`, background: Tndc.color.bg.card, fontSize: '10px', padding: '2px 6px', borderRadius: '4px', cursor: 'pointer', color: Tndc.color.text.secondary }} title="Phase menu">⋯</button>
                    </div>
                  </div>

                  {!g.collapsed && g.note && (
                    <div style={{ padding: '6px 12px', fontSize: '11px', color: Tndc.color.text.secondary, fontStyle: 'italic', marginLeft: '10px', borderLeft: `2px dotted ${g.color}44` }}>{g.note}</div>
                  )}

                  {!g.collapsed && (
                    <div style={{ paddingLeft: '10px', borderLeft: `2px dotted ${g.color}44`, marginLeft: '8px' }}>
                      {dropSlot(gi, 0, !!(drag && dropTarget && dropTarget.gi === gi && dropTarget.di === 0))}

                      {g.docIds.length === 0 ? (
                        <div onDragOver={(e) => onSlotOver(gi, 0, e)} onDrop={(e) => onSlotDrop(gi, 0, e)}
                          style={{ padding: '20px 14px', textAlign: 'center', fontSize: '11px', color: Tndc.color.text.tertiary, fontStyle: 'italic', border: `2px dashed ${isTargetEmpty ? nx.fuchsia : Tndc.color.border.light}`, background: isTargetEmpty ? nx.fuchsiaBg : 'transparent', borderRadius: Tndc.radius.md }}>
                          Drop documents here to populate this phase
                        </div>
                      ) : g.docIds.map((docId, di) => {
                        const doc = docById[docId]; if (!doc) return null;
                        const isDragging = drag && drag.docId === doc.id;
                        const flatIdx = flatChain.findIndex(x => x.gi === gi && x.di === di);
                        const inboundInfer = flatIdx > 0 ? pairs[flatIdx - 1] : null;
                        return (
                          <React.Fragment key={doc.id}>
                            {di > 0 && connectorBar(inboundInfer)}
                            {di === 0 && gi > 0 && flatIdx > 0 && (
                              <div style={{ padding: '2px 0' }}>
                                <div style={{ padding: '4px 10px', borderRadius: '6px', background: `${g.color}08`, border: `1px dashed ${g.color}55`, marginBottom: '4px', fontSize: '10px', color: Tndc.color.text.secondary, display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
                                  <span style={{ fontWeight: 700, color: g.color, textTransform: 'uppercase', letterSpacing: '0.06em', fontSize: '9px' }}>↪ cross-phase</span>
                                  {inboundInfer && (<>
                                    <span style={{ fontFamily: Tndc.font.mono, color: __nxConfidenceColor(inboundInfer.score), fontWeight: 700 }}>{Math.round(inboundInfer.score * 100)}%</span>
                                    <span style={{ fontStyle: 'italic' }}>{inboundInfer.verb}</span>
                                    {inboundInfer.reasons[0] && <span style={{ color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono }}>· {inboundInfer.reasons[0]}</span>}
                                  </>)}
                                </div>
                              </div>
                            )}
                            {renderDocCard(doc, gi, di, isDragging)}
                            {dropSlot(gi, di + 1, !!(drag && dropTarget && dropTarget.gi === gi && dropTarget.di === di + 1))}
                          </React.Fragment>
                        );
                      })}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>

        {/* Right rail */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(12px, 1vw + 6px, 18px)', flex: '1 1 320px', minWidth: 0 }}>
          {/* Inspector */}
          <div style={{ ...nx.card, marginBottom: 0 }}>
            <div style={nx.cardH}>
              <span>Inspector{selectedDoc ? ` · #${selectedFlatIdx + 1}` : ''}</span>
              <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary }}>click a card</span>
            </div>
            {!selectedDoc ? (
              <div style={{ padding: '22px 16px', fontSize: '12px', color: Tndc.color.text.tertiary, textAlign: 'center' }}>Select a document to inspect inference detail and corpus links.</div>
            ) : (
              <div style={{ padding: '12px 14px' }}>
                <div style={{ fontSize: '13px', fontWeight: 700, color: Tndc.color.text.primary }}>{selectedDoc.name}</div>
                <div style={{ fontSize: '10px', color: Tndc.color.text.tertiary, marginTop: '2px', fontFamily: Tndc.font.mono }}>{selectedDoc.id} · {selectedDoc.dateRange || 'undated'} · {selectedDoc.matter}</div>
                <div style={{ fontSize: '11px', color: Tndc.color.text.secondary, lineHeight: 1.5, marginTop: '6px' }}>{selectedDoc.summary}</div>
                <div style={{ display: 'flex', gap: '6px', marginTop: '8px', flexWrap: 'wrap' }}>
                  <button style={nx.btnSecondary} onClick={() => toggleFlag(selectedDoc.id, 'pinned')}>{flags[selectedDoc.id]?.pinned ? 'ok Pinned' : ' Pin'}</button>
                  <button style={nx.btnSecondary} onClick={() => toggleFlag(selectedDoc.id, 'flagged')}>{flags[selectedDoc.id]?.flagged ? 'ok Flagged' : 'flag Flag'}</button>
                  <button style={nx.btnSecondary} onClick={() => toggleFlag(selectedDoc.id, 'anchor')}>{flags[selectedDoc.id]?.anchor ? 'ok Anchor' : ' Anchor'}</button>
                </div>
                {selectedPair ? (
                  <div style={{ padding: '10px 12px', background: nx.fuchsiaBg, borderRadius: '6px', border: `1px solid ${nx.fuchsiaBorder}`, marginTop: '10px' }}>
                    <div style={{ fontSize: '9px', fontWeight: 700, color: nx.fuchsia, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '4px' }}>Inferred from #{selectedFlatIdx} →</div>
                    <div style={{ fontSize: '12px', fontWeight: 700, color: __nxConfidenceColor(selectedPair.score) }}>
                      {flatChain[selectedFlatIdx - 1].doc.name.slice(0, 38)}{flatChain[selectedFlatIdx - 1].doc.name.length > 38 ? '…' : ''} <span style={{ color: Tndc.color.text.secondary, fontWeight: 500, fontStyle: 'italic' }}>{selectedPair.verb}</span> this
                    </div>
                    <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '6px' }}>
                      <span style={{ ...nx.tag, background: __nxConfidenceColor(selectedPair.score) + '15', color: __nxConfidenceColor(selectedPair.score) }}>{Math.round(selectedPair.score * 100)}%</span>
                      {selectedPair.explicit && <span style={{ ...nx.tag, background: nx.artifactBg, color: nx.artifact }}>ok grounded</span>}
                      {selectedPair.reasons.map(r => (<span key={r} style={{ ...nx.tag, background: Tndc.color.bg.secondary, color: Tndc.color.text.secondary, fontFamily: Tndc.font.mono }}>{r}</span>))}
                    </div>
                  </div>
                ) : (
                  <div style={{ padding: '8px 10px', background: Tndc.color.bg.secondary, borderRadius: '6px', fontSize: '11px', color: Tndc.color.text.tertiary, marginTop: '10px' }}>Head of chain · no upstream inference.</div>
                )}
                <div style={{ fontSize: '9px', fontWeight: 700, color: Tndc.color.text.tertiary, textTransform: 'uppercase', letterSpacing: '0.08em', margin: '10px 0 4px' }}>Corpus links · {selectedLinks.length}</div>
                {selectedLinks.length === 0 ? <div style={{ fontSize: '11px', color: Tndc.color.text.tertiary, fontStyle: 'italic' }}>No corpus links.</div>
                  : <div style={{ display: 'flex', flexDirection: 'column', gap: '3px', maxHeight: '180px', overflowY: 'auto' }}>
                    {selectedLinks.slice(0, 20).map((l, i) => {
                      const ls = nx.linkStyle(l.type);
                      const otherId = l.from === selectedDoc.id ? l.to : l.from;
                      const other = data.entities.find(e => e.id === otherId);
                      const ts = other ? nx.typeStyle(other.type) : null;
                      return (
                        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 6px', background: Tndc.color.bg.secondary, borderRadius: '4px', fontSize: '10px' }}>
                          <span style={{ ...nx.tag, background: `${ls.color}15`, color: ls.color, fontSize: '9px' }}>{ls.label}</span>
                          {ts && <span style={{ color: ts.color }}>{ts.icon}</span>}
                          <span style={{ flex: 1, color: Tndc.color.text.primary, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{other ? other.name : otherId}</span>
                        </div>
                      );
                    })}
                  </div>}
              </div>
            )}
          </div>

          {/* Library */}
          <div style={{ ...nx.card, marginBottom: 0 }} onDragOver={(e) => e.preventDefault()} onDrop={onLibraryDrop}>
            <div style={nx.cardH}>
              <span>Library · {libraryDocs.length} / {allDocs.length - chainCount}</span>
              <span style={{ fontSize: '10px', color: Tndc.color.text.tertiary }}>drop here to remove</span>
            </div>
            <div style={{ padding: '8px 12px', borderBottom: `1px solid ${Tndc.color.border.light}`, display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
              <input placeholder="Search library…" value={libSearch} onChange={(e) => setLibSearch(e.target.value)}
                style={{ flex: '1 1 120px', minWidth: '110px', padding: '5px 9px', borderRadius: '5px', border: `1px solid ${Tndc.color.border.light}`, fontSize: '11px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.primary }} />
              <select value={matterFilter} onChange={(e) => setMatterFilter(e.target.value)}
                style={{ padding: '5px 8px', borderRadius: '5px', border: `1px solid ${Tndc.color.border.light}`, fontSize: '10px', fontFamily: Tndc.font.family, background: Tndc.color.bg.card, color: Tndc.color.text.secondary }}>
                {matters.map(m => <option key={m} value={m}>{m}</option>)}
              </select>
            </div>
            <div style={{ padding: '8px', maxHeight: '380px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '4px' }}>
              {libraryDocs.length === 0 ? <div style={{ padding: '18px', textAlign: 'center', fontSize: '11px', color: Tndc.color.text.tertiary }}>No documents match the filter.</div>
                : libraryDocs.map(d => (
                  <div key={d.id} draggable onDragStart={(e) => startDrag(d.id, 'library', e)} onDragEnd={endDrag}
                    style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: '8px', alignItems: 'center', padding: '6px 8px', background: Tndc.color.bg.secondary, border: `1px solid ${Tndc.color.border.light}`, borderLeft: `3px solid ${nx.doc}`, borderRadius: '5px', cursor: 'grab', opacity: (drag?.docId === d.id) ? 0.35 : 1, userSelect: 'none' }}
                    title="Drag onto the linkage track">
                    <span style={{ color: Tndc.color.text.tertiary, fontSize: '12px', lineHeight: 1 }}>⋮⋮</span>
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: '11px', fontWeight: 600, color: Tndc.color.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.name}</div>
                      <div style={{ fontSize: '9px', color: Tndc.color.text.tertiary, fontFamily: Tndc.font.mono, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.id} · {d.dateRange || '—'} · {d.matter}</div>
                    </div>
                    <span style={{ ...nx.tag, background: d.priority === 'key' ? nx.crimsonBg : d.priority === 'high' ? nx.eventBg : Tndc.color.bg.card, color: d.priority === 'key' ? nx.crimson : d.priority === 'high' ? nx.event : Tndc.color.text.tertiary, fontSize: '9px' }}>{d.priority}</span>
                  </div>
                ))}
            </div>
          </div>
        </div>
      </div>

      {/* ---------- Context Menu ---------- */}
      {ctxMenu && ctxMenu.kind === 'card' && (() => {
        const doc = docById[ctxMenu.docId]; if (!doc) return null;
        const f = flags[doc.id] || {};
        return (
          <div data-ctx-menu>
            <NxCtxMenu menu={ctxMenu} onClose={closeMenu}>
              <MenuLabel>{doc.id} · {doc.name.slice(0, 26)}{doc.name.length > 26 ? '…' : ''}</MenuLabel>
              <MenuItem icon="●" label="Open in Inspector" shortcut="Enter" onClick={run(() => setSelectedCard({ gi: ctxMenu.gi, di: ctxMenu.di }))} />
              <MenuSep />
              <MenuItem icon="" label={f.pinned ? 'Unpin' : 'Pin'} shortcut="P" onClick={run(() => toggleFlag(doc.id, 'pinned'))} />
              <MenuItem icon="flag" label={f.flagged ? 'Unflag' : 'Flag'} shortcut="F" onClick={run(() => toggleFlag(doc.id, 'flagged'))} />
              <MenuItem icon="" label={f.anchor ? 'Clear anchor' : 'Mark as anchor'} onClick={run(() => toggleFlag(doc.id, 'anchor'))} />
              <MenuItem icon="◐" label="Set priority" subItems={
                <>
                  {['key', 'high', 'medium', 'low'].map(p => (
                    <MenuItem key={p} icon={(f.priority || doc.priority) === p ? 'ok' : ''} label={p} onClick={run(() => setPriorityOverride(doc.id, p))} />
                  ))}
                </>
              } />
              <MenuItem icon="→" label="Move to phase" subItems={
                <>
                  {groups.map((g, i) => (
                    <MenuItem key={g.id} icon={i === ctxMenu.gi ? 'ok' : ''} label={g.name} onClick={run(() => moveDocToPhase(doc.id, i))} disabled={i === ctxMenu.gi} />
                  ))}
                </>
              } />
              <MenuSep />
              <MenuItem icon="⧉" label="Copy ID" onClick={run(async () => { try { await navigator.clipboard.writeText(doc.id); logActivity(`Copied ${doc.id}`); } catch (_) {} })} />
              <MenuItem icon="+" label="Duplicate entry" onClick={run(() => setGroupsCommit(gs => gs.map((g, i) => i === ctxMenu.gi ? { ...g, docIds: [...g.docIds.slice(0, ctxMenu.di + 1), doc.id, ...g.docIds.slice(ctxMenu.di + 1)] } : g), 'Duplicated entry'))} />
              <MenuSep />
              <MenuItem icon="×" label="Remove from chain" shortcut="Del" danger onClick={run(() => removeFromChain(doc.id))} />
            </NxCtxMenu>
          </div>
        );
      })()}

      {ctxMenu && ctxMenu.kind === 'phase' && groups[ctxMenu.gi] && (
        <div data-ctx-menu>
          <NxCtxMenu menu={ctxMenu} onClose={closeMenu}>
            <MenuLabel>Phase · {groups[ctxMenu.gi].name.slice(0, 28)}</MenuLabel>
            <MenuItem icon="" label="Rename" onClick={run(() => {
              const el = document.querySelector(`#nx-phase-${groups[ctxMenu.gi].id} input`); if (el) el.focus();
            })} />
            <MenuItem icon="" label="Edit note" onClick={run(() => {
              const next = prompt('Phase note:', groups[ctxMenu.gi].note || '');
              if (next != null) setGroupNote(ctxMenu.gi, next);
            })} />
            <MenuItem icon="◐" label="Recolor" subItems={
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px', padding: '6px' }}>
                {GROUP_COLORS.map(c => (
                  <button key={c} onClick={run(() => recolorGroup(ctxMenu.gi, c))}
                    style={{ width: '24px', height: '24px', borderRadius: '4px', background: c, border: c === groups[ctxMenu.gi].color ? '2px solid #000' : '1px solid rgba(0,0,0,0.1)', cursor: 'pointer' }} title={c} />
                ))}
              </div>
            } />
            <MenuSep />
            <MenuItem icon="↑" label="Sort by date (oldest first)" onClick={run(() => sortGroupByDate(ctxMenu.gi, 1))} />
            <MenuItem icon="↓" label="Sort by date (newest first)" onClick={run(() => sortGroupByDate(ctxMenu.gi, -1))} />
            <MenuSep />
            <MenuItem icon="▲" label="Move up" disabled={ctxMenu.gi === 0} onClick={run(() => moveGroup(ctxMenu.gi, -1))} />
            <MenuItem icon="▼" label="Move down" disabled={ctxMenu.gi === groups.length - 1} onClick={run(() => moveGroup(ctxMenu.gi, 1))} />
            <MenuItem icon="+" label="Duplicate phase" onClick={run(() => duplicateGroup(ctxMenu.gi))} />
            <MenuItem icon="⇠" label="Merge into previous" disabled={ctxMenu.gi === 0} onClick={run(() => mergeIntoPrev(ctxMenu.gi))} />
            <MenuItem icon="▶▼" label={groups[ctxMenu.gi].collapsed ? 'Expand' : 'Collapse'} onClick={run(() => toggleCollapse(ctxMenu.gi))} />
            <MenuSep />
            <MenuItem icon="×" label="Delete phase" danger onClick={run(() => removeGroup(ctxMenu.gi))} />
          </NxCtxMenu>
        </div>
      )}

      {ctxMenu && ctxMenu.kind === 'track' && (
        <div data-ctx-menu>
          <NxCtxMenu menu={ctxMenu} onClose={closeMenu}>
            <MenuLabel>Chain · {chainCount} docs · {groups.length} phases</MenuLabel>
            <MenuItem icon="+" label="Add phase" onClick={run(addGroup)} />
            <MenuItem icon="◷" label="Auto-arrange chronologically" onClick={run(autoArrangeChronological)} />
            <MenuItem icon="▼" label={collapsedAll ? 'Expand all' : 'Collapse all'} onClick={run(() => setAllCollapsed(!collapsedAll))} />
            <MenuSep />
            <MenuItem icon="↶" label="Undo" shortcut="cmdZ" disabled={!history.past.length} onClick={run(undo)} />
            <MenuItem icon="↷" label="Redo" shortcut="cmd⇧Z" disabled={!history.future.length} onClick={run(redo)} />
            <MenuSep />
            <MenuItem icon="" label={multiMode ? 'Exit multi-select' : 'Enter multi-select'} onClick={run(() => setMultiMode(m => !m))} />
            <MenuItem icon="ok" label="Select all" shortcut="cmdA" onClick={run(() => { setSelection(new Set(flatChain.map(x => x.doc.id))); setMultiMode(true); })} />
            <MenuSep />
            <MenuItem icon="↗" label="Export JSON" onClick={run(exportChain)} />
            <MenuItem icon="↘" label="Load preset" subItems={
              <>
                {data.chains.map(c => <MenuItem key={c.id} label={c.name} onClick={run(() => loadPreset(c.id))} />)}
              </>
            } />
            <MenuSep />
            <MenuItem icon="×" label="Clear chain" danger disabled={!chainCount} onClick={run(clearChain)} />
          </NxCtxMenu>
        </div>
      )}
    </div>
  );
}

window.NexusDocumentChain = NexusDocumentChain;
