// notebook.jsx — Sources & Atoms (the evidence layer underpinning AlphaSuite)
//
// "Sources:" is the foundational concept. Every score, every brief, every
// portfolio card traces back to here:
//   1. Multi-modal source corpus (PDFs, transcripts, GTM analysis, research,
//      connectors). Add via upload / URL / autoresearch / connectors.
//   2. Atom graph — discrete insights extracted from sources, cross-linked.
//      Obsidian-style: hover an atom dims unconnected ones.
//   3. Atom list view — same atoms as a sortable list.
//
// Two file-internal datasets (SOURCES, ATOMS) define the corpus for
// Smithfield Digital. They are referenced by the rich chat as well.

// ────────────────────────────────────────────────────────────
// 1. SOURCES — multi-modal ingest
// ────────────────────────────────────────────────────────────
const SOURCES = [
  { id: 'pitch',    kind: 'pdf',         title: 'Smithfield Pitch Deck 2025',         origin: 'Founder upload',      date: '10 May', pages: 42, atoms: 14, axis: 'product',
    excerpt: 'Smithfield Digital is the operating system for mid-market supply-chain finance. We process 2.3M transactions daily for 47 enterprise customers across UK and DACH.' },
  { id: 'tech',     kind: 'pdf',         title: 'Technical Audit · Q1 2026',          origin: 'Frontline Advisory',  date: '10 May', pages: 18, atoms: 12, axis: 'tech',
    excerpt: 'Infrastructure runs on AWS EKS with auto-scaling. Matching engine processes 2.3M txns/day. No proprietary dataset underlies the matching logic; rules-engine + heuristics tuned per customer.' },
  { id: 'fin',      kind: 'xlsx',        title: 'Financial Model · v4.2',             origin: 'CFO export',          date: '10 May', pages: null, atoms: 8, axis: 'comm',
    excerpt: 'ARR £2.1m April 26 · NRR 108% · 71% of revenue concentrated in 3 enterprise accounts · 18-month runway at current burn.' },
  { id: 'comp',     kind: 'pdf',         title: 'Competitor Teardown · Veritas',       origin: 'In-house research',   date: '08 May', pages: 12, atoms: 11, axis: 'market',
    excerpt: 'Veritas Platform raised £14m Series B in March 2026, targeting adjacent workflow. DataBridge entering UK from H2 2026.' },
  { id: 'team',     kind: 'pdf',         title: 'Team Bios & Org Chart',               origin: 'HR upload',           date: '09 May', pages: 8,  atoms: 7,  axis: 'team',
    excerpt: 'CEO James Harlow (prev. Monzo). CPO Sofia Tanaka (prev. Figma). CTO position vacant since March 2026. 28 FTE across product, engineering, GTM.' },
  { id: 'road',     kind: 'pdf',         title: 'Product Roadmap 2025-26',             origin: 'Founder upload',      date: '11 May', pages: 22, atoms: 9,  axis: 'vision',
    excerpt: 'Q3: mobile app launch · Q4: API marketplace · 2026 vision: AI-native workflow automation across enterprise mid-market.' },
  { id: 'intvw1',   kind: 'transcript',  title: 'CEO Interview · 4 May',               origin: 'Zoom · 47 min',       date: '04 May', pages: 12, atoms: 11, axis: 'vision',
    excerpt: '"We\'re not an AI company, we\'re an operations company. AI is a feature that makes the workflow defensible — but the workflow itself is the product."' },
  { id: 'intvw2',   kind: 'transcript',  title: 'Customer Reference · ACME UK',        origin: 'Zoom · 32 min',       date: '06 May', pages: 9,  atoms: 8,  axis: 'product',
    excerpt: '"Onboarding took 4 weeks longer than promised. Once running, the matching engine has cut our reconciliation time by 71%. We\'d switch only if a competitor undercut by ≥40%."' },
  { id: 'gtm',      kind: 'analysis',    title: 'GTM Analysis · Q2',                   origin: 'In-house research',   date: '07 May', pages: 14, atoms: 9,  axis: 'comm',
    excerpt: 'Inbound demand-gen weak. Outbound motion not yet built. ABM list of 240 mid-market targets ready but no SDR layer. Channel partners untapped.' },
  { id: 'market',   kind: 'research',    title: 'Market Sizing · Mid-Market SCF',      origin: 'Auto-research',       date: '11 May', pages: 9,  atoms: 7,  axis: 'market',
    excerpt: 'UK + DACH mid-market SCF TAM £1.4bn growing 14% YoY. Top 3 incumbents control 38% but show low NPS in 2025 surveys (avg 11).' },
];

const KIND_LABEL = {
  pdf: 'PDF', xlsx: 'SHEET', transcript: 'TRANSCRIPT',
  analysis: 'ANALYSIS', research: 'RESEARCH', url: 'WEB', md: 'NOTE',
};

// ────────────────────────────────────────────────────────────
// 2. ATOMS — discrete insights, positioned for the graph
// ────────────────────────────────────────────────────────────
// Coordinates are normalized 0..1. Source nodes are anchors; atoms cluster
// around their source. A handful of "bridge" atoms sit between sources and
// carry cross-edges — these are what makes the graph feel like a brain.

// Source node positions (rough octagonal layout, GTM at centre)
const SOURCE_POS = {
  pitch:  { x: 0.12, y: 0.32 },
  road:   { x: 0.30, y: 0.10 },
  tech:   { x: 0.66, y: 0.10 },
  comp:   { x: 0.86, y: 0.32 },
  fin:    { x: 0.88, y: 0.66 },
  team:   { x: 0.66, y: 0.90 },
  intvw1: { x: 0.34, y: 0.92 },
  intvw2: { x: 0.10, y: 0.66 },
  gtm:    { x: 0.42, y: 0.46 },
  market: { x: 0.70, y: 0.50 },
};

// Build atoms via halo + featured-with-edges
function buildAtoms() {
  const atoms = [];
  let seed = 1;
  const rand = () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; };

  // featured atoms — visible labels, cross-edges
  const featured = [
    { id: 'a-flywheel',  src: 'tech',   axis: 'ai',     rag: 'amber',
      label: 'No data flywheel', conf: 0.94, page: 7,
      excerpt: 'Matching engine processes 2.3M txns/day, but no proprietary dataset underlies the matching logic. Rules-engine + per-customer heuristics tuned by hand.',
      x: 0.50, y: 0.22, edges: ['a-veritas','a-cto','a-not-ai'] },
    { id: 'a-veritas',   src: 'comp',   axis: 'market', rag: 'amber',
      label: 'Veritas Series B', conf: 0.98, page: 3,
      excerpt: 'Veritas Platform raised £14m Series B in March 2026 led by Index Ventures. Targets adjacent mid-market workflow with overlapping ICP.',
      x: 0.74, y: 0.30, edges: ['a-flywheel','a-databridge'] },
    { id: 'a-databridge',src: 'comp',   axis: 'market', rag: 'amber',
      label: 'DataBridge UK', conf: 0.86, page: 8,
      excerpt: 'DataBridge announced UK entry for H2 2026. Currently US-only; team partially-poached from Adyen UK.',
      x: 0.78, y: 0.46, edges: ['a-veritas'] },
    { id: 'a-cto',       src: 'team',   axis: 'team',   rag: 'amber',
      label: 'CTO vacant', conf: 1.0,  page: 4,
      excerpt: 'CTO seat vacant since 14 March 2026 (resignation). Interim CTO is co-founder Tanaka (CPO). Recruitment opened May 1, three candidates in process.',
      x: 0.56, y: 0.74, edges: ['a-flywheel','a-arr','a-defensibility'] },
    { id: 'a-arr',       src: 'fin',    axis: 'comm',   rag: 'amber',
      label: '71% in 3 accts', conf: 1.0, page: 2,
      excerpt: '71% of ARR concentrated in three enterprise customers. Largest (ACME UK) accounts for 34% alone. Renewal terms: 12-month, all up Q1 2027.',
      x: 0.72, y: 0.66, edges: ['a-cto','a-nrr','a-onboarding'] },
    { id: 'a-nrr',       src: 'fin',    axis: 'comm',   rag: 'green',
      label: 'NRR 108%', conf: 1.0, page: 2,
      excerpt: 'Net Revenue Retention 108% trailing 12 months. Expansion ARR £210k from existing accounts via volume tier upgrades.',
      x: 0.58, y: 0.58, edges: ['a-arr'] },
    { id: 'a-soc2',      src: 'tech',   axis: 'tech',   rag: 'green',
      label: 'SOC2 II passed', conf: 1.0, page: 14,
      excerpt: 'SOC2 Type II audit completed 8 May 2026 by Drata. No major findings; two minor remediations closed within 48h.',
      x: 0.72, y: 0.20, edges: [] },
    { id: 'a-onboarding',src: 'intvw2', axis: 'product',rag: 'amber',
      label: '4 wks onboarding slip', conf: 0.84, page: 3,
      excerpt: '"Onboarding took 4 weeks longer than promised. Implementation team helpful but underresourced — single solutions engineer."',
      x: 0.26, y: 0.66, edges: ['a-arr','a-impl'] },
    { id: 'a-impl',      src: 'team',   axis: 'product',rag: 'amber',
      label: 'Implementation thin', conf: 0.78, page: 6,
      excerpt: 'One solutions engineer hired Jan 2026. Three more in roadmap for Q3. Implementation pipeline currently 6 customers deep.',
      x: 0.42, y: 0.78, edges: ['a-onboarding'] },
    { id: 'a-defensibility', src: 'intvw1', axis: 'vision', rag: 'amber',
      label: '"AI is a feature"', conf: 0.92, page: 8,
      excerpt: '"We\'re not an AI company. AI is a feature that makes the workflow defensible — but the workflow itself is the product." — J.H., CEO',
      x: 0.40, y: 0.30, edges: ['a-cto','a-not-ai'] },
    { id: 'a-not-ai',    src: 'gtm',    axis: 'brand',  rag: 'amber',
      label: 'No category POV', conf: 0.81, page: 9,
      excerpt: 'No public AI thesis. Limited content marketing on AI-native workflow. Competitors have moved to claim category language first.',
      x: 0.40, y: 0.40, edges: ['a-defensibility','a-flywheel'] },
    { id: 'a-tam',       src: 'market', axis: 'market', rag: 'green',
      label: 'TAM £1.4bn · +14%', conf: 0.88, page: 2,
      excerpt: 'UK + DACH mid-market SCF total addressable market £1.4bn, growing 14% YoY. Incumbents control 38% but show low NPS (avg 11).',
      x: 0.74, y: 0.42, edges: ['a-veritas'] },
    { id: 'a-recon',     src: 'intvw2', axis: 'product', rag: 'green',
      label: '71% recon saving', conf: 0.94, page: 4,
      excerpt: '"The matching engine has cut our reconciliation time by 71%. Switching cost is ~6 months of integration work."',
      x: 0.20, y: 0.58, edges: ['a-onboarding'] },
    { id: 'a-pipeline',  src: 'gtm',    axis: 'comm',   rag: 'amber',
      label: 'No SDR layer', conf: 0.96, page: 4,
      excerpt: 'ABM list of 240 mid-market targets ready. No SDR layer yet — founder-led sales only. Channel partner motion untapped.',
      x: 0.46, y: 0.54, edges: ['a-arr'] },
    { id: 'a-roadmap',   src: 'road',   axis: 'vision', rag: 'green',
      label: 'Mobile Q3 · API Q4', conf: 1.0, page: 4,
      excerpt: 'Q3 2026: mobile app launch. Q4: API marketplace. 2026 vision: AI-native workflow automation. Milestones tied to Series B raise.',
      x: 0.36, y: 0.20, edges: [] },
  ];

  featured.forEach(a => atoms.push({ ...a, featured: true, r: 5.5 }));

  // halo atoms — generated around each source
  Object.entries(SOURCE_POS).forEach(([sid]) => {
    const src = SOURCES.find(s => s.id === sid);
    if (!src) return;
    const n = src.atoms - featured.filter(f => f.src === sid).length;
    const base = SOURCE_POS[sid];
    for (let i = 0; i < n; i++) {
      const angle = (i / n) * Math.PI * 2 + rand() * 1.2;
      const r = 0.045 + rand() * 0.05;
      atoms.push({
        id: `${sid}-h${i}`,
        src: sid,
        axis: src.axis,
        rag: i % 5 === 0 ? 'red' : i % 3 === 0 ? 'amber' : 'green',
        label: '',
        conf: 0.6 + rand() * 0.4,
        x: base.x + Math.cos(angle) * r,
        y: base.y + Math.sin(angle) * r * 1.1,
        featured: false,
        r: 2.4 + rand() * 1.2,
        edges: [],
      });
    }
  });

  // sprinkle a few cross-halo edges for visual richness
  const crossSeeds = [
    ['pitch-h2','tech-h1'], ['road-h0','tech-h3'], ['fin-h1','intvw2-h0'],
    ['gtm-h0','market-h2'], ['team-h2','intvw1-h1'], ['comp-h2','market-h0'],
    ['intvw2-h0','pitch-h1'], ['road-h1','gtm-h2'],
  ];
  crossSeeds.forEach(([a, b]) => {
    const A = atoms.find(x => x.id === a);
    if (A) A.edges = [...(A.edges || []), b];
  });

  return atoms;
}

const ATOMS = buildAtoms();

// Lookup helpers
const ATOM_BY_ID = Object.fromEntries(ATOMS.map(a => [a.id, a]));
const SOURCE_BY_ID = Object.fromEntries(SOURCES.map(s => [s.id, s]));

// Neighbor set: atoms connected to A (atom→atom edges + parent source siblings)
function neighborsOf(atomId) {
  const a = ATOM_BY_ID[atomId];
  if (!a) return new Set();
  const set = new Set([atomId, `src:${a.src}`]);
  (a.edges || []).forEach(e => set.add(e));
  // back-edges
  ATOMS.forEach(x => { if ((x.edges || []).includes(atomId)) set.add(x.id); });
  return set;
}

// ────────────────────────────────────────────────────────────
// AtomGraph SVG — Obsidian-style
// ────────────────────────────────────────────────────────────
function AtomGraph({ width, height, onSelect, selected, dense = true, showLabels = true }) {
  const [hover, setHover] = React.useState(null);
  const focus = selected || hover;
  const neighborhood = focus ? neighborsOf(focus) : null;
  const isDimmed = (id) => neighborhood && !neighborhood.has(id);

  const px = (xy) => ({ cx: xy.x * width, cy: xy.y * height });
  const ragColor = { red: 'var(--red)', amber: 'var(--amber)', green: 'var(--green)' };

  // Build edges
  const edges = [];
  // atom → source (parent) - faint
  ATOMS.forEach(a => {
    const src = SOURCE_POS[a.src];
    if (!src) return;
    edges.push({ from: a.id, to: `src:${a.src}`, x1: a.x, y1: a.y, x2: src.x, y2: src.y, kind: 'parent' });
  });
  // atom → atom (cross-edges)
  ATOMS.forEach(a => {
    (a.edges || []).forEach(other => {
      const b = ATOM_BY_ID[other];
      if (!b) return;
      edges.push({ from: a.id, to: b.id, x1: a.x, y1: a.y, x2: b.x, y2: b.y, kind: 'cross' });
    });
  });

  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}
      style={{ display: 'block', background: 'var(--canvas)' }}>
      <defs>
        <radialGradient id="atomGlow" cx="0.5" cy="0.5" r="0.5">
          <stop offset="0%" stopColor="var(--gold)" stopOpacity="0.6" />
          <stop offset="100%" stopColor="var(--gold)" stopOpacity="0" />
        </radialGradient>
      </defs>

      {/* edges first */}
      {edges.map((e, i) => {
        const dim = focus && !(neighborhood.has(e.from) && neighborhood.has(e.to));
        const stroke = e.kind === 'cross' ? 'var(--ink-3)' : 'var(--elev-3)';
        return (
          <line key={i}
            x1={e.x1 * width} y1={e.y1 * height}
            x2={e.x2 * width} y2={e.y2 * height}
            stroke={stroke} strokeWidth={e.kind === 'cross' ? 0.9 : 0.5}
            opacity={dim ? 0.08 : (e.kind === 'cross' ? 0.6 : 0.35)}
            style={{ transition: 'opacity 200ms' }}
          />
        );
      })}

      {/* atoms */}
      {ATOMS.map(a => {
        const { cx, cy } = px(a);
        const dimmed = isDimmed(a.id);
        const isFocus = focus === a.id;
        return (
          <g key={a.id}
             onMouseEnter={() => setHover(a.id)}
             onMouseLeave={() => setHover(null)}
             onClick={() => onSelect && onSelect(a.id)}
             style={{ cursor: 'pointer', opacity: dimmed ? 0.15 : 1, transition: 'opacity 200ms' }}>
            {isFocus && (
              <circle cx={cx} cy={cy} r={a.r * 4} fill="url(#atomGlow)" />
            )}
            <circle cx={cx} cy={cy} r={a.r}
              fill={ragColor[a.rag]}
              stroke={isFocus ? 'var(--ink)' : 'none'}
              strokeWidth={isFocus ? 1.5 : 0}
            />
            {a.featured && showLabels && (
              <text x={cx + a.r + 6} y={cy + 3}
                fill={isFocus ? 'var(--ink)' : 'var(--ink-2)'}
                fontFamily="var(--mono)" fontSize="10" letterSpacing="0.04em">
                {a.label}
              </text>
            )}
          </g>
        );
      })}

      {/* sources */}
      {Object.entries(SOURCE_POS).map(([sid, pos]) => {
        const src = SOURCES.find(s => s.id === sid);
        if (!src) return null;
        const cx = pos.x * width, cy = pos.y * height;
        const id = `src:${sid}`;
        const dimmed = focus && !neighborhood.has(id);
        return (
          <g key={sid}
             onMouseEnter={() => setHover(id)}
             onMouseLeave={() => setHover(null)}
             onClick={() => onSelect && onSelect(id)}
             style={{ cursor: 'pointer', opacity: dimmed ? 0.18 : 1, transition: 'opacity 200ms' }}>
            <rect x={cx - 7} y={cy - 7} width={14} height={14}
              fill="var(--ink)" stroke="none"/>
            {showLabels && (
              <text x={cx} y={cy + 26}
                textAnchor="middle"
                fill="var(--ink)"
                fontFamily="var(--mono)" fontSize="10" letterSpacing="0.12em"
                style={{ textTransform: 'uppercase' }}>
                {src.title.length > 22 ? src.title.slice(0, 20) + '…' : src.title}
              </text>
            )}
          </g>
        );
      })}
    </svg>
  );
}

// ────────────────────────────────────────────────────────────
// Atom detail panel (shown when an atom is selected)
// ────────────────────────────────────────────────────────────
function AtomDetail({ atomId, sourceId, onClose }) {
  // Accept either atom or "src:xxx"
  let isSource = sourceId || (atomId && atomId.startsWith('src:'));
  let s = null, a = null;
  if (isSource) {
    const sid = sourceId || atomId.replace('src:', '');
    s = SOURCE_BY_ID[sid];
  } else {
    a = ATOM_BY_ID[atomId];
    if (a) s = SOURCE_BY_ID[a.src];
  }
  if (!s) return (
    <div style={{ padding: 24, color: 'var(--ink-3)', fontFamily: 'var(--mono)', fontSize: 11 }}>
      Hover or tap a node to inspect.
    </div>
  );

  return (
    <div style={{ padding: '20px 22px 24px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
        <Kicker color={isSource ? 'var(--ink-2)' : 'var(--gold)'}>
          {isSource ? 'Source' : 'Atom'} · {isSource ? KIND_LABEL[s.kind] : s.title.slice(0,18)+'…'}
        </Kicker>
        {onClose && (
          <button onClick={onClose} style={{
            background: 'transparent', border: 'none', color: 'var(--ink-3)',
            fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer',
          }}>✕</button>
        )}
      </div>

      {!isSource && a && (
        <>
          <div style={{ marginTop: 10, fontFamily: 'var(--serif)', fontSize: 24, lineHeight: 1.25,
            color: 'var(--ink)', letterSpacing: '-0.01em' }}>
            {a.label || `Atom ${a.id}`}
          </div>
          <div style={{ display: 'flex', gap: 14, marginTop: 10,
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.14em',
            textTransform: 'uppercase', color: 'var(--ink-3)' }}>
            <span><RagDot rag={a.rag} size={6} style={{ marginRight: 6, verticalAlign: 'middle' }}/>{a.rag}</span>
            <span>· {AXES.find(x => x.key === a.axis)?.label || a.axis}</span>
            <span>· conf {Math.round(a.conf * 100)}%</span>
          </div>
          <div style={{ marginTop: 16, fontFamily: 'var(--serif)', fontSize: 16, lineHeight: 1.45,
            color: 'var(--ink-2)', fontStyle: 'italic' }}>
            "{a.excerpt}"
          </div>
        </>
      )}

      {isSource && (
        <>
          <div style={{ marginTop: 10, fontFamily: 'var(--serif)', fontSize: 22, lineHeight: 1.25,
            color: 'var(--ink)' }}>
            {s.title}
          </div>
          <div style={{ display: 'flex', gap: 12, marginTop: 8,
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.14em',
            textTransform: 'uppercase', color: 'var(--ink-3)' }}>
            <span>{KIND_LABEL[s.kind]}</span>
            <span>· {s.origin}</span>
            <span>· {s.date}</span>
            {s.pages && <span>· {s.pages}pp</span>}
            <span>· {s.atoms} atoms</span>
          </div>
          <div style={{ marginTop: 14, fontFamily: 'var(--serif)', fontSize: 15, lineHeight: 1.45,
            color: 'var(--ink-2)' }}>
            "{s.excerpt}"
          </div>
        </>
      )}

      {/* provenance footer */}
      <div style={{ marginTop: 22, paddingTop: 12, borderTop: '1px solid var(--elev-2)',
        display: 'flex', justifyContent: 'space-between',
        fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
        textTransform: 'uppercase', color: 'var(--ink-3)' }}>
        <span>From {s.origin}</span>
        <span>Ingested {s.date}</span>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// Sources list (for left panel)
// ────────────────────────────────────────────────────────────
function SourcesList({ onSelect, selectedId, compact = false }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      {SOURCES.map(s => {
        const selected = selectedId === `src:${s.id}`;
        return (
          <button key={s.id} onClick={() => onSelect(`src:${s.id}`)} style={{
            display: 'block', textAlign: 'left', width: '100%',
            background: selected ? 'var(--elev-2)' : 'transparent',
            border: 'none', color: 'var(--ink)', cursor: 'pointer',
            padding: compact ? '12px 16px' : '14px 18px',
            borderTop: '1px solid var(--elev-1)',
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
              <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em',
                textTransform: 'uppercase', color: 'var(--ink-3)' }}>{KIND_LABEL[s.kind]}</span>
              <span style={{ fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--ink-3)',
                letterSpacing: '0.1em' }}>{s.date}</span>
            </div>
            <div style={{ fontFamily: 'var(--serif)', fontSize: compact ? 16 : 18,
              lineHeight: 1.25, marginTop: 6, color: 'var(--ink)' }}>{s.title}</div>
            {!compact && (
              <div style={{ marginTop: 8, fontSize: 12, lineHeight: 1.45,
                color: 'var(--ink-2)', display: '-webkit-box', WebkitLineClamp: 2,
                WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
                "{s.excerpt}"
              </div>
            )}
            <div style={{ marginTop: 10, display: 'flex', justifyContent: 'space-between',
              fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
              textTransform: 'uppercase', color: 'var(--ink-3)' }}>
              <span>{s.origin}</span>
              <span><span style={{ color: 'var(--gold)' }}>{s.atoms}</span> atoms</span>
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// Atom list (table view, alt to graph)
// ────────────────────────────────────────────────────────────
function AtomList({ onSelect, selectedId, max = 30 }) {
  const featured = ATOMS.filter(a => a.featured);
  const others = ATOMS.filter(a => !a.featured).slice(0, max - featured.length);
  const items = [...featured, ...others];
  return (
    <div>
      {items.map((a) => {
        const src = SOURCE_BY_ID[a.src];
        const selected = selectedId === a.id;
        return (
          <button key={a.id} onClick={() => onSelect(a.id)} style={{
            display: 'grid',
            gridTemplateColumns: 'auto 1fr 100px 60px',
            gap: 16, alignItems: 'center',
            width: '100%', textAlign: 'left', padding: '12px 18px',
            background: selected ? 'var(--elev-2)' : 'transparent',
            border: 'none', borderTop: '1px solid var(--elev-1)',
            color: 'var(--ink)', cursor: 'pointer',
          }}>
            <RagDot rag={a.rag} size={7} />
            <div>
              <div style={{ fontSize: 13, color: 'var(--ink)', lineHeight: 1.35 }}>
                {a.label || <span style={{ color: 'var(--ink-3)', fontFamily: 'var(--mono)' }}>{a.id}</span>}
              </div>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
                textTransform: 'uppercase', color: 'var(--ink-3)', marginTop: 3 }}>
                {AXES.find(x => x.key === a.axis)?.label || a.axis}
              </div>
            </div>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10,
              color: 'var(--ink-3)', letterSpacing: '0.1em',
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {src?.title.slice(0, 16)}
            </div>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10,
              color: 'var(--ink-2)', textAlign: 'right' }}>
              {Math.round(a.conf * 100)}%
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// Add-Source dialog (compact, inline)
// ────────────────────────────────────────────────────────────
function AddSourceDialog({ onClose, compact = false }) {
  const [tab, setTab] = React.useState('upload');
  return (
    <div style={{
      position: 'absolute', inset: 0, background: 'rgba(8,8,6,0.78)',
      backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
      zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{
        width: 'min(560px, 92%)', background: 'var(--surface)',
        boxShadow: '0 30px 60px rgba(0,0,0,0.6)',
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          padding: '20px 24px 14px' }}>
          <div>
            <Kicker>Add to corpus</Kicker>
            <div style={{ fontFamily: 'var(--serif)', fontSize: 26, marginTop: 4, color: 'var(--ink)' }}>
              Source
            </div>
          </div>
          <button onClick={onClose} style={{ background: 'transparent', border: 'none',
            color: 'var(--ink-2)', fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer' }}>✕</button>
        </div>

        <div style={{ display: 'flex', borderTop: '1px solid var(--elev-2)',
          borderBottom: '1px solid var(--elev-2)' }}>
          {[
            { id: 'upload', label: 'Upload' },
            { id: 'url',    label: 'URL' },
            { id: 'research',label: 'Auto Research' },
            { id: 'connect',label: 'Connectors' },
          ].map(t => (
            <button key={t.id} onClick={() => setTab(t.id)} style={{
              flex: 1, padding: '14px 0', background: 'transparent', border: 'none',
              color: tab === t.id ? 'var(--ink)' : 'var(--ink-3)',
              fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
              textTransform: 'uppercase', cursor: 'pointer', position: 'relative',
            }}>
              {t.label}
              {tab === t.id && <div style={{ position: 'absolute', bottom: -1, left: 0, right: 0,
                height: 2, background: 'var(--ink)' }} />}
            </button>
          ))}
        </div>

        <div style={{ padding: '22px 24px 24px', minHeight: 220 }}>
          {tab === 'upload' && (
            <div>
              <div style={{
                border: '1px dashed var(--elev-3)', padding: '34px 16px',
                textAlign: 'center', color: 'var(--ink-3)',
                fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.12em',
                textTransform: 'uppercase',
              }}>
                Drop files · pdf · md · txt · csv · xlsx · vtt
              </div>
              <div style={{ marginTop: 18, fontFamily: 'var(--mono)', fontSize: 10,
                color: 'var(--ink-3)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>
                Auto-classifies kind · extracts atoms · cross-links into graph
              </div>
            </div>
          )}
          {tab === 'url' && (
            <div>
              <Kicker>Web page or PDF</Kicker>
              <input placeholder="https://"
                style={{ marginTop: 10, width: '100%', padding: '12px 14px',
                  background: 'var(--elev-1)', border: 'none', color: 'var(--ink)',
                  fontFamily: 'var(--mono)', fontSize: 13, outline: 'none' }} />
              <div style={{ marginTop: 14, fontFamily: 'var(--mono)', fontSize: 10,
                color: 'var(--ink-3)' }}>
                Headless fetch · readability extract · markdown ingest
              </div>
            </div>
          )}
          {tab === 'research' && (
            <div>
              <Kicker>Subject &amp; objective</Kicker>
              <input placeholder="e.g. EU mid-market SCF · Q2 2026"
                style={{ marginTop: 10, width: '100%', padding: '12px 14px',
                  background: 'var(--elev-1)', border: 'none', color: 'var(--ink)',
                  fontFamily: 'var(--sans)', fontSize: 14, outline: 'none' }} />
              <textarea placeholder="What should the agent look for?"
                rows={3}
                style={{ marginTop: 10, width: '100%', padding: '12px 14px',
                  background: 'var(--elev-1)', border: 'none', color: 'var(--ink)',
                  fontFamily: 'var(--sans)', fontSize: 14, outline: 'none', resize: 'none' }} />
              <div style={{ marginTop: 14, display: 'flex', justifyContent: 'space-between' }}>
                <span style={{ fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--ink-3)',
                  letterSpacing: '0.12em', textTransform: 'uppercase' }}>
                  Max 12 sources · query agent v2.1
                </span>
                <button style={{ background: 'var(--gold)', border: 'none', color: 'var(--canvas)',
                  padding: '10px 18px', fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
                  textTransform: 'uppercase', cursor: 'pointer', fontWeight: 500 }}>
                  Run research →
                </button>
              </div>
            </div>
          )}
          {tab === 'connect' && (
            <div>
              <Kicker>Sync from external services</Kicker>
              <div style={{ marginTop: 14, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                {['Google Drive','Notion','GitHub','Slack','Linear','Web Crawler'].map(c => (
                  <button key={c} style={{
                    padding: '12px 14px', background: 'var(--elev-1)', border: 'none',
                    color: 'var(--ink)', textAlign: 'left', cursor: 'pointer',
                    display: 'flex', justifyContent: 'space-between',
                  }}>
                    <span style={{ fontSize: 13 }}>{c}</span>
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em',
                      color: 'var(--ink-3)' }}>SOON</span>
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// Mobile Notebook screen
// ────────────────────────────────────────────────────────────
function MNotebook({ companyId, back, goScore, goChat }) {
  const c = COMPANIES.find(x => x.id === companyId);
  const [view, setView] = React.useState('graph'); // sources / graph / list
  const [selected, setSelected] = React.useState('a-flywheel');
  const [adding, setAdding] = React.useState(false);

  return (
    <div style={{ background: 'var(--canvas)', minHeight: '100%', display: 'flex',
      flexDirection: 'column', paddingBottom: 78, position: 'relative' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px 0' }}>
        <button onClick={back} style={{
          display: 'inline-flex', alignItems: 'center', gap: 8,
          background: 'transparent', border: 'none', color: 'var(--ink-2)',
          fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
          textTransform: 'uppercase', cursor: 'pointer', padding: '6px 8px',
        }}>
          <Chev /> Portfolio
        </button>
        <button onClick={() => setAdding(true)} style={{
          background: 'transparent', border: 'none', color: 'var(--gold)',
          fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
          textTransform: 'uppercase', cursor: 'pointer', padding: '6px 8px',
        }}>+ Source</button>
      </div>

      <div style={{ padding: '12px 24px 4px' }}>
        <Kicker>Notebook · 10 sources · 135 atoms</Kicker>
        <div style={{ fontFamily: 'var(--serif)', fontSize: 32, lineHeight: 0.95,
          letterSpacing: '-0.02em', marginTop: 4 }}>
          {c.name}{' '}
          <span style={{ color: 'var(--ink-3)', fontStyle: 'italic' }}>Digital</span>
        </div>
      </div>

      <MTabStrip active="notebook" goScore={goScore} goChat={goChat} />

      {/* view toggle */}
      <div style={{ display: 'flex', gap: 0, padding: '14px 16px 8px' }}>
        {[
          { id: 'sources', label: 'Sources' },
          { id: 'graph',   label: 'Graph' },
          { id: 'list',    label: 'List' },
        ].map(t => (
          <button key={t.id} onClick={() => setView(t.id)} style={{
            flex: 1, padding: '8px 0', border: 'none',
            background: view === t.id ? 'var(--elev-2)' : 'var(--elev-1)',
            color: view === t.id ? 'var(--ink)' : 'var(--ink-3)',
            fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.14em',
            textTransform: 'uppercase', cursor: 'pointer',
          }}>{t.label}</button>
        ))}
      </div>

      {view === 'graph' && (
        <div style={{ position: 'relative' }}>
          <AtomGraph width={361} height={340} onSelect={setSelected}
            selected={selected} showLabels={false} />
          <div style={{ position: 'absolute', top: 8, right: 8,
            fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
            textTransform: 'uppercase', color: 'var(--ink-3)' }}>
            Tap a node
          </div>
        </div>
      )}

      {view === 'sources' && (
        <div style={{ background: 'var(--surface)' }}>
          <SourcesList onSelect={(id) => { setSelected(id); }} selectedId={selected} compact />
        </div>
      )}

      {view === 'list' && (
        <div style={{ background: 'var(--surface)' }}>
          <AtomList onSelect={setSelected} selectedId={selected} max={20} />
        </div>
      )}

      {/* detail bottom sheet */}
      <div style={{ background: 'var(--surface)', marginTop: 8 }}>
        <AtomDetail atomId={selected} />
      </div>

      <MTabBar active="index" />
      {adding && <AddSourceDialog onClose={() => setAdding(false)} compact />}
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// Desktop Notebook screen
// ────────────────────────────────────────────────────────────
function DNotebook({ companyId, goPortfolio, goScore, goChat }) {
  const c = COMPANIES.find(x => x.id === companyId);
  const [selected, setSelected] = React.useState('a-flywheel');
  const [view, setView] = React.useState('graph'); // graph or list
  const [adding, setAdding] = React.useState(false);

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '340px 1fr 360px',
      gap: 0, minHeight: D_H - 56, position: 'relative' }}>

      {/* Left — Sources */}
      <aside style={{ background: 'var(--surface)', display: 'flex', flexDirection: 'column',
        borderRight: '1px solid var(--elev-2)' }}>
        <div style={{ padding: '28px 24px 14px', display: 'flex',
          justifyContent: 'space-between', alignItems: 'flex-end' }}>
          <div>
            <Kicker color="var(--ink-2)">Sources</Kicker>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--ink-3)',
              marginTop: 4, letterSpacing: '0.14em' }}>
              10 docs · 135 atoms
            </div>
          </div>
          <button onClick={() => setAdding(true)} style={{
            background: 'var(--gold)', border: 'none', color: 'var(--canvas)',
            padding: '8px 14px', fontFamily: 'var(--mono)', fontSize: 10,
            letterSpacing: '0.16em', textTransform: 'uppercase', cursor: 'pointer',
            fontWeight: 500,
          }}>+ Add</button>
        </div>
        <div style={{ flex: 1, overflow: 'auto' }} className="no-scrollbar">
          <SourcesList onSelect={setSelected} selectedId={selected} />
        </div>
        <div style={{ padding: '14px 24px', borderTop: '1px solid var(--elev-2)',
          fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
          textTransform: 'uppercase', color: 'var(--ink-3)' }}>
          Last ingest 11 May · streaming live
        </div>
      </aside>

      {/* Center — Graph or List */}
      <div style={{ background: 'var(--canvas)', display: 'flex', flexDirection: 'column' }}>
        <div style={{ padding: '28px 36px 12px', display: 'flex',
          justifyContent: 'space-between', alignItems: 'flex-end' }}>
          <div>
            <Kicker>{c.sector} · {c.stage}</Kicker>
            <div style={{ fontFamily: 'var(--serif)', fontSize: 42, lineHeight: 0.95,
              letterSpacing: '-0.025em', marginTop: 6 }}>
              {c.name}{' '}
              <span style={{ fontStyle: 'italic', color: 'var(--ink-3)' }}>Digital</span>
              <span style={{ marginLeft: 14, fontFamily: 'var(--mono)',
                fontSize: 12, letterSpacing: '0.14em', color: 'var(--ink-3)',
                textTransform: 'uppercase' }}>· Notebook</span>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 0 }}>
            {[
              { id: 'graph', label: 'Graph' },
              { id: 'list',  label: 'List' },
            ].map(t => (
              <button key={t.id} onClick={() => setView(t.id)} style={{
                padding: '10px 18px', border: 'none',
                background: view === t.id ? 'var(--ink)' : 'transparent',
                color: view === t.id ? 'var(--canvas)' : 'var(--ink-3)',
                fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em',
                textTransform: 'uppercase', cursor: 'pointer',
              }}>{t.label}</button>
            ))}
          </div>
        </div>

        <div style={{ padding: '0 28px 10px', display: 'flex', gap: 18,
          fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.14em',
          textTransform: 'uppercase', color: 'var(--ink-3)' }}>
          <span><span style={{ color: 'var(--green)' }}>●</span> {ATOMS.filter(a => a.rag==='green').length} green</span>
          <span><span style={{ color: 'var(--amber)' }}>●</span> {ATOMS.filter(a => a.rag==='amber').length} amber</span>
          <span><span style={{ color: 'var(--red)' }}>●</span> {ATOMS.filter(a => a.rag==='red').length} red</span>
          <span style={{ marginLeft: 'auto' }}>Hover to focus · click to pin</span>
        </div>

        {view === 'graph' && (
          <div style={{ flex: 1, position: 'relative' }}>
            <AtomGraph width={720} height={680} onSelect={setSelected}
              selected={selected} showLabels={true} />
          </div>
        )}
        {view === 'list' && (
          <div style={{ flex: 1, overflow: 'auto', background: 'var(--surface)' }}
            className="no-scrollbar">
            <div style={{ padding: '12px 18px', display: 'grid',
              gridTemplateColumns: 'auto 1fr 100px 60px', gap: 16,
              borderBottom: '1px solid var(--elev-2)',
              fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.14em',
              textTransform: 'uppercase', color: 'var(--ink-3)' }}>
              <span></span><span>Atom · Axis</span><span>Source</span><span style={{ textAlign: 'right' }}>Conf</span>
            </div>
            <AtomList onSelect={setSelected} selectedId={selected} max={60} />
          </div>
        )}
      </div>

      {/* Right — selected detail + provenance */}
      <aside style={{ background: 'var(--surface)', borderLeft: '1px solid var(--elev-2)',
        display: 'flex', flexDirection: 'column' }}>
        <div style={{ padding: '14px 22px', borderBottom: '1px solid var(--elev-2)',
          display: 'flex', justifyContent: 'space-between' }}>
          <Kicker>Inspector</Kicker>
          <Kicker>{selected ? 'pinned' : 'idle'}</Kicker>
        </div>
        <div style={{ flex: 1, overflow: 'auto' }} className="no-scrollbar">
          <AtomDetail atomId={selected} />

          {/* if atom, show its connections */}
          {selected && !selected.startsWith('src:') && ATOM_BY_ID[selected] && (
            <div style={{ padding: '0 22px 22px' }}>
              <Rule />
              <div style={{ marginTop: 18 }}>
                <Kicker>Connected atoms</Kicker>
                <div style={{ marginTop: 10 }}>
                  {(() => {
                    const a = ATOM_BY_ID[selected];
                    const direct = (a.edges || []).map(id => ATOM_BY_ID[id]).filter(Boolean);
                    // include back-edges
                    const backs = ATOMS.filter(x => (x.edges || []).includes(selected));
                    const all = [...direct, ...backs].filter((v, i, arr) => arr.indexOf(v) === i);
                    if (all.length === 0) return (
                      <div style={{ fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--ink-3)' }}>None</div>
                    );
                    return all.map(b => (
                      <button key={b.id} style={{
                        display: 'block', width: '100%', textAlign: 'left',
                        padding: '8px 0', background: 'transparent', border: 'none',
                        color: 'var(--ink)', cursor: 'pointer',
                      }} onClick={() => setSelected(b.id)}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                          <RagDot rag={b.rag} size={6} />
                          <span style={{ fontSize: 13 }}>{b.label || b.id}</span>
                        </div>
                      </button>
                    ));
                  })()}
                </div>
              </div>
            </div>
          )}
        </div>
      </aside>

      {adding && <AddSourceDialog onClose={() => setAdding(false)} />}
    </div>
  );
}

Object.assign(window, {
  SOURCES, ATOMS, SOURCE_BY_ID, ATOM_BY_ID, SOURCE_POS, KIND_LABEL,
  AtomGraph, AtomDetail, SourcesList, AtomList, AddSourceDialog,
  MNotebook, DNotebook, neighborsOf,
});
