/* Notes viewer · Obsidian + Notion · with AI actions (summarize / send to agent / promote / tag) */

const { useState, useMemo, useRef, useEffect } = React;

/* ---------- ICONS ---------- */
const ICONS = {
  obsidian: 'M12 2L4 7v10l8 5 8-5V7l-8-5zM12 2v20M4 7l16 10',
  notion:   'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5zM8 7v10M16 7v10M8 7l8 10',
  sparkle:  'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8L19 14zM5 16l.6 1.6L7 18l-1.4.4L5 20l-.6-1.6L3 18l1.4-.4L5 16z',
  send:     'M22 2L11 13M22 2l-7 20-4-9-9-4z',
  envelope: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6L12 13 2 6',
  tag:      'M20 12l-8 8-9-9V3h8l9 9z M7 7h.01',
  file:     'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 2v6h6',
  folder:   'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v11z',
  caret:    'M9 6l6 6-6 6',
  caretDown:'M6 9l6 6 6-6',
  brain:    'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2zM14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z',
  feather:  'M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5l6.74-6.76zM16 8l-9 9M2 22l5-5',
};
const Ic = ({ name, size = 14, color = 'currentColor' }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d={ICONS[name] || ICONS.file} />
  </svg>
);

/* ---------- Vault ----------
   Mirrors the actual material in artemis-city-blog and BRYPTOS folders. */
const VAULT = [
  { source: 'obsidian', folder: 'kb/', files: [
    { id: 'ob-1', title: 'Artemis City — Architecture',     tags: ['architecture', 'kernel'],   updated: '2026-04-12' },
    { id: 'ob-2', title: 'Hebbian routing benchmarks',      tags: ['research', 'routing'],      updated: '2026-04-22' },
    { id: 'ob-3', title: 'Why LLM Wrappers Fail',           tags: ['doctrine'],                 updated: '2026-03-30' },
    { id: 'ob-4', title: 'ATP Protocol Spec v0.4',          tags: ['protocol', 'spec'],         updated: '2026-04-18' },
  ]},
  { source: 'obsidian', folder: 'kb/Research', files: [
    { id: 'ob-5', title: 'Quantum security · physics not math', tags: ['research', 'security'], updated: '2026-04-08' },
    { id: 'ob-6', title: 'Trust decay model',               tags: ['governance'],              updated: '2026-03-26' },
  ]},
  { source: 'obsidian', folder: 'BRYPTOS', files: [
    { id: 'ob-7', title: 'Crypto options · Greeks digest',  tags: ['trading', 'options'],      updated: '2026-04-23' },
    { id: 'ob-8', title: 'Miners revenue spike · Apr',      tags: ['trading', 'on-chain'],     updated: '2026-04-15' },
    { id: 'ob-9', title: 'Glossary working draft',          tags: ['reference'],               updated: '2026-04-01' },
  ]},
  { source: 'notion', folder: 'Workspace · Artemis', files: [
    { id: 'nt-1', title: 'Q1 Artemis OKRs',                  tags: ['planning'],                 updated: '2026-04-20' },
    { id: 'nt-2', title: 'Audit log · governance violations',tags: ['governance', 'audit'],     updated: '2026-04-22' },
    { id: 'nt-3', title: 'Release notes · ATP v0.5',         tags: ['protocol', 'release'],     updated: '2026-04-24' },
  ]},
  { source: 'notion', folder: 'Workspace · BRYPTOS', files: [
    { id: 'nt-4', title: 'Desk update · weekly digest',      tags: ['trading', 'digest'],       updated: '2026-04-24' },
    { id: 'nt-5', title: 'FRED series · macro watchlist',    tags: ['macro'],                    updated: '2026-04-21' },
  ]},
  { source: 'notion', folder: 'Workspace · Lab', files: [
    { id: 'nt-6', title: 'Ramble On · feature backlog',      tags: ['product', 'voice'],        updated: '2026-04-19' },
  ]},
];

/* Note bodies (markdown — but rendered locally) */
const NOTES = {
  'ob-1': { body: `## Kernel-first architecture

**Artemis City** uses a deterministic kernel to route tasks. Agents register with capabilities. The kernel matches and ranks via Hebbian-weighted scoring; the elected agent runs inside a governance envelope.

### Why kernel-first

- LLMs are non-deterministic loop controllers — useless for long-running ops.
- The kernel decides; the LLM executes within bounded steps.
- Governance is enforced at the routing layer, not the prompt.

### Components
\`\`\`
[Kernel] ──route──▶ [Agent Registry] ──pick──▶ [Agent]
   │                                            │
   ▼                                            ▼
[Sandbox]                                    [Memory Bus]
\`\`\`

> The kernel decides, not the LLM.

See also: [[Hebbian routing benchmarks]], [[ATP Protocol Spec v0.4]].` },

  'ob-2': { body: `## Hebbian routing benchmarks

Across **12,400 routed tasks** against the static baseline:

- **+18.3%** mean success rate at iter ≥ 4
- **−42%** kernel-to-agent round trips
- **1.4** Sharpe of quality

Weights converged at \`~3.2k\` runs. Task mix skewed to summarization; planning underperforms below iter 8.

### Method
1. Sample 12k tasks from production traces.
2. Replay through static-ranked router (control) and Hebbian-weighted router (treatment).
3. Score outputs with a frozen judge model.

### Open questions
- Does adaptive weighting create echo chambers (same agents reinforced)? See [[Trust decay model]].
- Hot-loading new agents resets local weights — find a continuity strategy.` },

  'ob-3': { body: `## Why LLM Wrappers Fail

Wrapper-first systems are useful for **prototyping** but weak for **long-lived operations**.

### Common failure modes
- Non-deterministic tool routing
- Context / memory drift over long tasks
- Limited accountability for runtime decisions
- High cost from repeated full-inference planning loops

### Kernel-first alternative
Artemis City separates runtime control from inference:

- **Kernel** — governance, routing, state transitions.
- **Agents / LLMs** — task execution within boundaries.

This keeps behavior predictable while still using modern model capabilities.` },

  'ob-4': { body: `## ATP envelope · v0.4

The Artemis Transmission Protocol envelope carries every cross-agent message.

\`\`\`json
{
  "version": "0.4",
  "envelope_id": "env_a4f3b1c2",
  "from": "user/prinston",
  "to": "summarizer",
  "intent": "request",
  "payload": { "task": "TL;DR the Q1 OKR doc." },
  "governance": {
    "policy": "approval",
    "trust_min": 0.75,
    "tool_whitelist": ["memory.read", "kb.search"]
  },
  "trust_decay": 0.02,
  "signed_at": "2026-04-24T17:21:00Z"
}
\`\`\`

### Required fields
- **agent / from / to** — registry-scoped ids
- **intent** — one of \`request\`, \`propose\`, \`commit\`, \`reject\`
- **payload** — Pydantic-validated body
- **governance** — policy + trust_min + tool_whitelist
- **trust_decay** — deducted from sender on failure` },

  'ob-7': { body: `## Crypto options · Greeks digest

BTC at $71,248 · IV30 ATM 52.4% (down 1.8pp).

### Position book
- **+5 BTC-Jun-72k C** (Δ 2.10)
- **+3 BTC-Jun-75k C** (Δ 0.72)
- **−2 BTC-Jun-80k C** (Δ -0.20)
- **+4 BTC-Jun-67k P** (Δ -0.88)

### Sensitivities
| Greek | Net |
|---|---|
| Δ | +1.98 |
| Γ | 0.00026 |
| Θ | -41.20 |
| ν | +124 |

Vega is the dominant exposure. If realised vol prints below 40, the book bleeds.

> Glossary: [[Delta (Δ)]], [[Gamma (Γ)]], [[Theta (Θ)]], [[Vega (ν)]].` },

  'nt-1': { body: `## Q1 Artemis OKRs

### O1 — Ship the kernel governance layer
- **KR1** — 99% audit coverage for every routed task. _on track_
- **KR2** — ATP v0.5 envelope reaches GA. _at risk · waiting on signed envelope cert._
- **KR3** — Trust decay calibrated to <2% false-positive lockout. _done._

### O2 — Land the marketing surface
- **KR1** — Artemis-city.com hits 5k MAU. _on track_
- **KR2** — Three published whitepapers (architecture, governance, quantum). _2 of 3 done._

### O3 — Grow the partner pipeline
- **KR1** — Three design-partner deployments running on production tenants. _1 of 3 done._` },

  'nt-3': { body: `## Release notes · ATP v0.5

### New
- Signed envelopes via Descope JWTs.
- \`intent: defer\` for queued tasks.
- Tool whitelist supports glob patterns (\`memory.*\`).

### Changed
- Trust-decay default raised from 0.02 → 0.03.
- Governance policy \`auto\` now requires explicit opt-in per workspace.

### Migration
- Envelopes signed with the legacy HMAC mode remain accepted through Q3.` },

  'nt-4': { body: `## BRYPTOS desk update · weekly digest

### Spot
- BTC closed at $71,248 (+5.84% WoW)
- ETH at $3,812 (+3.12% WoW)

### Macro
- 10y yield ticked to 4.31% (+3bp), DXY 104.21.
- CPI prints next Tuesday — consensus 3.1%.

### Positioning
- We're net long Vega via the $72-75k call spread.
- Watching mempool drain into the Jun expiry.

### Action items
- [ ] Roll the $80k short into Sep.
- [ ] Add a Hebbian-routed validator to the macro signal feed.` },
};

// Default placeholder for notes not authored above
function defaultBody(item) {
  return `## ${item.title}

_(placeholder body — this note hasn't been authored yet in the prototype dataset)_

Tags: ${item.tags.join(', ')}. Last updated ${item.updated}.

The action panel on the right still works against this stub — try **AI summarize** or **send to agent**.`;
}

/* ---------- Markdown render (lightweight) ---------- */
function renderMd(src, onLinkClick) {
  const lines = src.split('\n');
  const out = [];
  let inCode = false; let codeBuf = []; let codeLang = '';
  let listBuf = []; let listType = null;
  let quoteBuf = [];

  function flushList(key) {
    if (!listBuf.length) return;
    const Tag = listType === 'ul' ? 'ul' : 'ol';
    out.push(<Tag key={key}>{listBuf.map((l, i) => <li key={i} dangerouslySetInnerHTML={{ __html: inline(l) }} />)}</Tag>);
    listBuf = []; listType = null;
  }
  function flushQuote(key) {
    if (!quoteBuf.length) return;
    out.push(<blockquote key={key} dangerouslySetInnerHTML={{ __html: inline(quoteBuf.join(' ')) }} />);
    quoteBuf = [];
  }
  function inline(s) {
    return s
      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
      .replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
      .replace(/`([^`]+)`/g, '<code>$1</code>')
      .replace(/\[\[([^\]]+)\]\]/g, '<span class="link" data-wikilink="$1">$1</span>');
  }

  lines.forEach((line, i) => {
    if (line.startsWith('```')) {
      if (!inCode) { inCode = true; codeLang = line.slice(3); codeBuf = []; }
      else { inCode = false; out.push(<pre key={i}><code>{codeBuf.join('\n')}</code></pre>); }
      return;
    }
    if (inCode) { codeBuf.push(line); return; }
    if (line.startsWith('### '))  { flushList(i); flushQuote(i); out.push(<h3 key={i} dangerouslySetInnerHTML={{ __html: inline(line.slice(4)) }} />); return; }
    if (line.startsWith('## '))   { flushList(i); flushQuote(i); out.push(<h2 key={i} dangerouslySetInnerHTML={{ __html: inline(line.slice(3)) }} />); return; }
    if (line.startsWith('# '))    { flushList(i); flushQuote(i); out.push(<h2 key={i} dangerouslySetInnerHTML={{ __html: inline(line.slice(2)) }} />); return; }
    if (line.startsWith('> '))    { flushList(i); quoteBuf.push(line.slice(2)); return; }
    if (line.startsWith('- ') || line.startsWith('* ')) {
      flushQuote(i);
      if (listType !== 'ul') flushList(i);
      listType = 'ul'; listBuf.push(line.slice(2)); return;
    }
    if (/^\d+\.\s/.test(line)) {
      flushQuote(i);
      if (listType !== 'ol') flushList(i);
      listType = 'ol'; listBuf.push(line.replace(/^\d+\.\s/, '')); return;
    }
    if (line.trim() === '') { flushList(i); flushQuote(i); return; }
    flushList(i); flushQuote(i);
    out.push(<p key={i} dangerouslySetInnerHTML={{ __html: inline(line) }} />);
  });
  flushList('end-l'); flushQuote('end-q');
  return out;
}

/* ---------- Search hits ---------- */
function searchVault(q) {
  if (!q) return [];
  const needle = q.toLowerCase();
  const hits = [];
  VAULT.forEach((g) => g.files.forEach((f) => {
    const body = NOTES[f.id]?.body || defaultBody(f);
    const all = (f.title + ' ' + f.tags.join(' ') + ' ' + body).toLowerCase();
    if (all.includes(needle)) {
      // grab snippet
      const idx = body.toLowerCase().indexOf(needle);
      let sni = body.slice(Math.max(0, idx - 40), Math.min(body.length, idx + 120)).replace(/\n+/g, ' · ');
      if (idx < 0) sni = body.slice(0, 140).replace(/\n+/g, ' · ');
      hits.push({ ...f, source: g.source, folder: g.folder, sni });
    }
  }));
  return hits.slice(0, 8);
}

/* ---------- Sidebar tree ---------- */
function Tree({ source, activeId, onPick }) {
  const groups = VAULT.filter((g) => source === 'all' || g.source === source);
  const [open, setOpen] = useState(() => {
    const o = {}; groups.forEach((g) => { o[g.source + ':' + g.folder] = true; }); return o;
  });
  return (
    <div className="tree">
      {groups.map((g, gi) => {
        const key = g.source + ':' + g.folder;
        const isOpen = open[key];
        return (
          <div key={gi} className="folder">
            <div className="folder-head" onClick={() => setOpen({ ...open, [key]: !isOpen })}>
              <span className="caret"><Ic name={isOpen ? 'caretDown' : 'caret'} size={11} /></span>
              <Ic name={g.source === 'obsidian' ? 'obsidian' : 'notion'} size={12} color={g.source === 'obsidian' ? '#a855f7' : '#67e8f9'} />
              <span>{g.folder}</span>
              <span style={{ marginLeft: 'auto', color: 'var(--fg-3)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>{g.files.length}</span>
            </div>
            {isOpen && (
              <div className="folder-children">
                {g.files.map((f) => (
                  <div key={f.id} className={'file ' + (f.id === activeId ? 'active' : '')} onClick={() => onPick(f.id)}>
                    <Ic name="file" size={11} color={g.source === 'obsidian' ? '#a855f7' : '#67e8f9'} />
                    <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.title}</span>
                    <span className={'badge ' + g.source}>{g.source === 'obsidian' ? 'md' : 'pg'}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

/* ---------- Action panel ---------- */
function ActionPanel({ activeNote, onToast }) {
  const [resultKind, setResultKind] = useState(null);
  const [result, setResult] = useState('');
  const [pending, setPending] = useState(false);
  const [agent, setAgent] = useState('artemis');

  async function run(kind) {
    if (pending) return;
    setResultKind(kind); setResult(''); setPending(true);
    const body = activeNote ? (NOTES[activeNote.id]?.body || defaultBody(activeNote)) : '';
    try {
      let prompt = '';
      if (kind === 'summarize') {
        prompt = `Summarize this note as Artemis Summarizer agent:\n\n1) One-line TL;DR\n2) 3-5 key bullets\n3) Recommended next action\n\nMax 120 words.\n\n=== NOTE ===\n${body}`;
      } else if (kind === 'medium') {
        prompt = `Rewrite this note as a publication-ready Medium post draft. Output: title, subtitle, 2-3 paragraphs, 3 tag suggestions. Keep the voice engineering-serious. Note:\n\n${body}`;
      } else if (kind === 'atp') {
        prompt = `Convert this note into an ATP envelope JSON. The envelope shape:\n{\n  version: "0.4",\n  envelope_id: "env_<rand>",\n  from: "notes/${activeNote?.source || 'obsidian'}",\n  to: "${agent}",\n  intent: "propose",\n  payload: { source_title, summary, action_items },\n  governance: { policy: "approval", trust_min: 0.75, tool_whitelist: ["memory.read","kb.search"] },\n  trust_decay: 0.02\n}\n\nGenerate only the JSON.\n\nNote:\n${body}`;
      } else if (kind === 'task') {
        prompt = `Convert this note into a single concrete task for the ${agent} agent. Output:\n- **Goal:** ...\n- **Steps:** 3-5 short steps\n- **Tools needed:** comma-separated\n- **Done means:** one sentence.\n\nNote:\n${body}`;
      } else if (kind === 'tag') {
        prompt = `Suggest 4-6 new tags for this note (terse, lowercase, hyphenated). Current tags: ${activeNote?.tags.join(', ')}. Output as a comma-separated list only.\n\nNote:\n${body}`;
      }
      const r = await window.claude.complete({ messages: [{ role: 'user', content: prompt }] });
      setResult(r);
      onToast?.(kind === 'task' ? 'Task dispatched to ' + agent : kind === 'medium' ? 'Medium draft ready' : kind === 'atp' ? 'Envelope sealed' : kind === 'tag' ? 'Tag suggestions ready' : 'Summary ready');
    } catch (err) {
      setResult('⚠ ' + (err?.message || 'request failed'));
    } finally {
      setPending(false);
    }
  }

  return (
    <aside className="actions">
      <div>
        <div className="a-head">● AI Actions</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
          <button className="action-btn" onClick={() => run('summarize')}>
            <span className="iconbox"><Ic name="sparkle" /></span>
            <span>Summarize note<small>TL;DR + bullets + next action</small></span>
          </button>
          <button className="action-btn purple" onClick={() => run('medium')}>
            <span className="iconbox"><Ic name="feather" /></span>
            <span>Promote to Medium<small>Title, subtitle, draft + tags</small></span>
          </button>
          <button className="action-btn amber" onClick={() => run('atp')}>
            <span className="iconbox"><Ic name="envelope" /></span>
            <span>Wrap as ATP envelope<small>v0.4 · ready to dispatch</small></span>
          </button>
        </div>
      </div>

      <div>
        <div className="a-head">● Kernel actions</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
          <div style={{ display: 'flex', gap: 6, alignItems: 'center', padding: '4px 2px', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-3)' }}>
            agent:
            <select value={agent} onChange={(e) => setAgent(e.target.value)} style={{ flex: 1, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.14)', borderRadius: 6, color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: 12, padding: '4px 8px' }}>
              <option value="artemis">artemis</option>
              <option value="planner">planner</option>
              <option value="research">research</option>
              <option value="summarizer">summarizer</option>
            </select>
          </div>
          <button className="action-btn green" onClick={() => run('task')}>
            <span className="iconbox"><Ic name="send" /></span>
            <span>Send to {agent} as task<small>Decomposes into goal + steps</small></span>
          </button>
          <button className="action-btn" onClick={() => run('tag')}>
            <span className="iconbox"><Ic name="tag" /></span>
            <span>Suggest tags<small>Categorize for retrieval</small></span>
          </button>
        </div>
      </div>

      {(pending || result) && (
        <div className={'action-result ' + (resultKind === 'medium' ? 'purple' : resultKind === 'atp' ? 'amber' : resultKind === 'task' ? 'green' : '')}>
          <div className="label">
            <span className="dot" style={{ background: pending ? '#fbbf24' : '#22c55e' }}></span>
            {resultKind === 'summarize' ? 'AI summary' :
             resultKind === 'medium'    ? 'Medium draft' :
             resultKind === 'atp'       ? 'ATP envelope' :
             resultKind === 'task'      ? 'Task dispatched' :
             resultKind === 'tag'       ? 'Tag suggestions' :
             'Output'}
          </div>
          {pending && !result ? <span className="typing"><span></span><span></span><span></span></span> : result}
        </div>
      )}

      <div style={{ marginTop: 'auto', padding: 10, background: 'rgba(255,255,255,0.03)', border: '1px dashed rgba(255,255,255,0.10)', borderRadius: 8, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', letterSpacing: '0.06em', lineHeight: 1.55 }}>
        ● linked · obsidian://open · notion://www.notion.so · output via window.claude.complete
      </div>
    </aside>
  );
}

/* ---------- Root ---------- */
function App() {
  const [source, setSource] = useState('all');
  const [activeId, setActiveId] = useState('ob-1');
  const [search, setSearch] = useState('');
  const [toastMsg, setToastMsg] = useState(null);

  // tags can be added per note locally
  const [localTags, setLocalTags] = useState({});
  const baseItem = useMemo(() => {
    for (const g of VAULT) {
      const f = g.files.find((x) => x.id === activeId);
      if (f) return { ...f, source: g.source, folder: g.folder };
    }
    return null;
  }, [activeId]);
  const item = baseItem ? { ...baseItem, tags: [...(baseItem.tags || []), ...(localTags[activeId] || [])] } : null;

  const hits = searchVault(search.trim());

  function addTag(noteId) {
    const t = prompt('Add tag (lowercase, hyphenated):');
    if (!t) return;
    setLocalTags((lt) => ({ ...lt, [noteId]: [...(lt[noteId] || []), t.trim().toLowerCase()] }));
    toast('tag added · #' + t.trim());
  }
  function toast(msg) {
    setToastMsg(msg);
    setTimeout(() => setToastMsg(null), 2400);
  }

  // wiki-link click handler
  function onMdClick(e) {
    if (e.target?.dataset?.wikilink) {
      const title = e.target.dataset.wikilink;
      // find by title
      for (const g of VAULT) {
        const f = g.files.find((x) => x.title.toLowerCase() === title.toLowerCase());
        if (f) { setActiveId(f.id); toast('navigated → ' + f.title); return; }
      }
      toast('no note found for [[' + title + ']]');
    }
  }

  return (
    <>
      <div className="aurora"><div className="blob b1"></div><div className="blob b2"></div><div className="blob b3"></div></div>
      <div className="app">
        <div className="topbar">
          <div className="brand">
            <span className="glyph">N</span>
            <div>
              <div className="title"><span className="grad">Notes</span> <span style={{ color: 'var(--fg-3)', fontWeight: 500, fontSize: 14 }}>· Obsidian + Notion</span></div>
              <div className="sub">UNIFIED MEMORY · LINKED TO ARTEMIS KERNEL</div>
            </div>
          </div>

          <input
            className="global-search"
            placeholder={`Search ${source === 'all' ? 'Obsidian + Notion' : source}…`}
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />

          <div className="right">
            <span className="chip"><span className="dot"></span> 6 vaults · 412 notes · synced</span>
          </div>
        </div>

        <div className="stage" style={{ position: 'relative' }}>
          <aside className="sidebar">
            <div className="source-toggle">
              <button className={'all ' + (source === 'all' ? 'on' : '')} onClick={() => setSource('all')}>All</button>
              <button className={'obsidian ' + (source === 'obsidian' ? 'on' : '')} onClick={() => setSource('obsidian')}>
                <span className="dot" style={{ background: '#a855f7' }}></span> Obsidian
              </button>
              <button className={'notion ' + (source === 'notion' ? 'on' : '')} onClick={() => setSource('notion')}>
                <span className="dot" style={{ background: '#67e8f9' }}></span> Notion
              </button>
            </div>
            <div className="nav-title">Vaults</div>
            <Tree source={source} activeId={activeId} onPick={(id) => { setActiveId(id); setSearch(''); }} />
          </aside>

          {search.trim() && hits.length > 0 && (
            <div className="search-results">
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 8 }}>
                {hits.length} hit{hits.length === 1 ? '' : 's'} across {[...new Set(hits.map((h) => h.source))].join(' + ')}
              </div>
              {hits.map((h) => {
                const re = new RegExp(`(${search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
                const ttl = h.title.replace(re, '<mark>$1</mark>');
                const sni = h.sni.replace(re, '<mark>$1</mark>');
                return (
                  <div key={h.id} className="hit" onClick={() => { setActiveId(h.id); setSearch(''); }}>
                    <div className="top">
                      <span className={'reader-source ' + h.source}><Ic name={h.source} size={9} /> {h.source}</span>
                      <span className="ttl" dangerouslySetInnerHTML={{ __html: ttl }} />
                      <span style={{ marginLeft: 'auto', color: 'var(--fg-3)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>{h.folder}</span>
                    </div>
                    <div className="sni" dangerouslySetInnerHTML={{ __html: '…' + sni + '…' }} />
                  </div>
                );
              })}
            </div>
          )}

          <main className="reader" onClick={onMdClick}>
            {item ? (
              <>
                <div className="reader-head">
                  <div className="reader-meta">
                    <span className={'reader-source ' + item.source}><Ic name={item.source} size={10} /> {item.source}</span>
                    <span>{item.folder}</span>
                    <span>·</span>
                    <span>updated {item.updated}</span>
                  </div>
                  <h1>{item.title}</h1>
                  <div className="tags">
                    {item.tags.map((t, i) => <span key={i} className="tag-pill">#{t}</span>)}
                    <span className="add-tag" onClick={() => addTag(item.id)}>+ add tag</span>
                  </div>
                </div>
                <div className="reader-body">
                  {renderMd(NOTES[item.id]?.body || defaultBody(item))}
                </div>
              </>
            ) : (
              <div style={{ padding: 60, color: 'var(--fg-3)', textAlign: 'center' }}>Pick a note from the vault.</div>
            )}
          </main>

          <ActionPanel activeNote={item} onToast={toast} />
        </div>
      </div>

      <div className="toast" hidden={!toastMsg}>
        <span style={{ color: '#86efac' }}>●</span> {toastMsg}
      </div>
    </>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
