<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OKF Bundle — Knowledge Visualization</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>◇</text></svg>" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3/dist/cytoscape.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
<style>
* { margin:0; padding:0; box-sizing:border-box; }
:root {
  --bg:#0a0a0f; --surface:#131319; --surface-2:#191922; --surface-3:#1f1f2b;
  --border:#26262f; --border-strong:#34343f;
  --text:#f2f2f5; --text-2:#a3a3b5; --text-3:#6f6f82;
  --accent:#7c6cff; --accent-strong:#9385ff; --accent-soft:rgba(124,108,255,.14);
  --danger:#f87171; --danger-soft:rgba(248,113,113,.12);
  --success:#4ade80; --warning:#fbbf24;
  --brand-400:#a855f7; --brand-500:#8b5cf6; --brand-600:#7c3aed; --brand-800:#5b21b6; --brand-900:#4c1d95;
  --t-class:#a78bfa; --t-function:#38bdf8; --t-module:#fbbf24; --t-dependency:#4ade80;
  --t-table:#fb7185; --t-view:#fb923c; --t-index:#f472b6; --t-method:#c084fc;
  --t-interface:#22d3ee; --t-type:#818cf8; --t-trigger:#f43f5e;
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;
  --r-sm:6px; --r-md:10px; --r-lg:14px; --r-xl:18px;
  --shadow-sm:0 1px 2px rgba(0,0,0,.3);
  --shadow-md:0 4px 16px rgba(0,0,0,.35);
  --shadow-lg:0 8px 32px rgba(0,0,0,.35);
  --shadow-glow:0 0 0 1px var(--border), 0 8px 24px rgba(0,0,0,.35);
  --ease:cubic-bezier(.2,.8,.2,1);

  /* Ambient glow for dark theme */
  --bg-image: none;
}

html[data-theme="light"] {
  --bg:#f7f7fb; --surface:#ffffff; --surface-2:#f4f4f9; --surface-3:#ececf4;
  --border:#e3e3ec; --border-strong:#d3d3e0;
  --text:#131320; --text-2:#5c5c72; --text-3:#8a8aa0;
  --accent:#6d5efc; --accent-strong:#5847e8; --accent-soft:rgba(109,94,252,.10);
  --danger:#dc2626; --danger-soft:rgba(220,38,38,.08);
  --success:#22c55e; --warning:#d97706;
  --brand-400:#a855f7; --brand-500:#8b5cf6; --brand-600:#7c3aed; --brand-800:#5b21b6; --brand-900:#4c1d95;
  --shadow-sm:0 1px 2px rgba(20,20,40,.06);
  --shadow-md:0 8px 24px rgba(20,20,40,.08);
  --shadow-lg:0 8px 32px rgba(20,20,40,.08);
  --shadow-glow:0 0 0 1px var(--border), 0 8px 24px rgba(20,20,40,.08);

  --bg-image: none;
}
html[data-theme="light"] .stat-card {
  background:rgba(255,255,255,.15); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(0,0,0,.08);
}
html[data-theme="light"] .stat-card::after {
  box-shadow:inset 0 1px 1px rgba(255,255,255,1);
}
html[data-theme="light"] .stat-card:hover {
  background:rgba(255,255,255,.9); border-color:rgba(0,0,0,.1);
  box-shadow:0 12px 32px rgba(20,20,40,.1), 0 0 0 1px var(--accent-soft);
}
html[data-theme="light"] .sec pre { background:rgba(0,0,0,.02); }
html[data-theme="light"] .chip { background:rgba(0,0,0,.03); border-color:rgba(0,0,0,.08); }

body {
  font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  color:var(--text); height:100vh; overflow:hidden;
  font-size:14px; line-height:1.5;

  background-color:var(--bg);
  background-image:var(--bg-image);
  background-size:cover;

  transition:background-color .18s var(--ease), color .18s var(--ease);
}


code, pre, .mono { font-family:'JetBrains Mono',ui-monospace,Menlo,monospace; }
::selection { background:var(--accent); color:#fff; }
:focus-visible { outline:2px solid var(--accent-strong); outline-offset:2px; border-radius:4px; }
a { color:inherit; }
button, select, input { font:inherit; }
::-webkit-scrollbar { width:10px; height:10px; }
::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:99px; border:2px solid var(--bg); }
::-webkit-scrollbar-track { background:transparent; }

#topbar {
  height:60px; display:flex; align-items:center; justify-content:center;
  position:sticky; top:0; z-index:50; padding:0;
}
#topbar-inner {
  width:92%; max-width:1200px; height:46px;
  display:flex; align-items:center; gap:var(--sp-4); padding:0 16px;
  border-radius:16px;
  background:rgba(255,255,255,.08); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(255,255,255,.12);
  box-shadow:0 4px 32px rgba(0,0,0,.3), 0 1px 4px rgba(0,0,0,.15);
  position:relative;
}

html[data-theme="light"] #topbar-inner {
  background:rgba(255,255,255,.1);
  backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border-color:rgba(0,0,0,.08);
  box-shadow:0 4px 32px rgba(20,20,40,.08), 0 1px 4px rgba(20,20,40,.04);
}
.nav-seg {
  display:flex; border-radius:10px; background:rgba(255,255,255,.06); padding:3px; gap:2px;
}
html[data-theme="light"] .nav-seg { background:rgba(0,0,0,.04); }
.nav-seg-item {
  font-size:13px; font-weight:500; color:var(--text-3); padding:4px 18px;
  border-radius:8px; cursor:pointer; transition:all .2s var(--ease); user-select:none;
  background:transparent; border:none; font-family:inherit;
}
.nav-seg-item:hover { color:var(--text-2); }
.nav-seg-item.active {
  color:var(--text); background:var(--surface);
  box-shadow:0 1px 3px rgba(0,0,0,.12);
}
#brand { display:flex; align-items:center; gap:10px; flex-shrink:0; }
.nav-logo-icon { display:flex; align-items:center; justify-content:center; width:30px; height:30px; flex-shrink:0; }
#brand-text .name { font-weight:700; font-size:15px; color:var(--text); white-space:nowrap; }
#topbar-spacer { flex:1; }

#breadcrumb {
  display:flex; align-items:center; gap:6px; padding:var(--sp-3) var(--sp-4);
  margin-bottom:var(--sp-3); margin-top:0;
  background:rgba(255,255,255,.04); border-radius:var(--r-md);
  border:1px solid rgba(255,255,255,.06); font-size:12px;
  position:sticky; top:0; z-index:10;
}
html[data-theme="light"] #breadcrumb {
  background:rgba(0,0,0,.03);
  border-color:rgba(0,0,0,.06);
}
#breadcrumb .crumb {
  cursor:pointer; color:var(--text-3); padding:2px 8px; border-radius:var(--r-sm);
  transition:all .15s var(--ease); font-weight:500;
}
#breadcrumb .crumb:hover { color:var(--accent-strong); background:var(--accent-soft); }
#breadcrumb .sep { color:var(--border-strong); font-size:10px; user-select:none; }
#breadcrumb .current { color:var(--text); background:var(--surface); font-weight:600; box-shadow:0 1px 2px rgba(0,0,0,.06); }
.icon-btn {
  width:34px; height:34px; border-radius:var(--r-sm); background:transparent; border:1px solid transparent;
  color:var(--text-2); display:flex; align-items:center; justify-content:center; cursor:pointer;
  transition:background .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);
}
.icon-btn:hover { background:var(--surface-2); color:var(--text); border-color:var(--border); }
.icon-btn svg { width:17px; height:17px; }

#layout { display:flex; height:calc(100vh - 60px); }

#sidebar {
  width:300px; border-radius:var(--r-xl);
  display:flex; flex-direction:column; flex:none; overflow:hidden;
  margin:var(--sp-3) 0 var(--sp-3) var(--sp-3);
  background:rgba(19,19,25,.72); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(255,255,255,.07);
  box-shadow:var(--shadow-md);
  position:relative;
}

html[data-theme="light"] #sidebar {
  background:rgba(255,255,255,.1);
  backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border-color:rgba(0,0,0,.08);
}
#main { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
#main.wide { padding:0; }
#browse-layout { display:flex; flex:1; min-height:0; padding:var(--sp-4); gap:0; }
#browse-content {
  flex:1; display:flex; flex-direction:column; min-height:0; overflow:auto;
  padding:var(--sp-4); margin:var(--sp-3) var(--sp-3) var(--sp-3) 0;
  background:rgba(19,19,25,.6); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(255,255,255,.06); border-radius:var(--r-xl);
  box-shadow:var(--shadow-md);
}
html[data-theme="light"] #browse-content {
  background:rgba(255,255,255,.1);
  backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border-color:rgba(0,0,0,.08);
}
#sidebar-resize {
  width:4px; cursor:col-resize; background:var(--border); flex:none;
  transition:background .12s var(--ease); position:relative; z-index:10;
  margin:100px 0;
  border-radius:4px; background-clip:content-box;
  background:linear-gradient(to bottom, transparent 0, var(--border) 20%, var(--border) 80%, transparent 100%);
}
#sidebar-resize:hover, #sidebar-resize.active { background:var(--accent); background-clip:content-box; }
.field-wrap { padding:var(--sp-3) var(--sp-3) 0; }
.search-box { position:relative; }
.search-box svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); width:14px; height:14px; color:var(--text-3); pointer-events:none; }
#search {
  width:100%; background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r-sm);
  padding:8px 10px 8px 30px; color:var(--text); font-size:13px; outline:none;
  transition:border-color .15s var(--ease), background .15s var(--ease);
}
#search:focus { border-color:var(--accent); background:var(--surface); }
#search-no-results code { font-size:12px; }
#search::placeholder { color:var(--text-3); }
.filter-row { display:flex; flex-direction:column; gap:4px; margin-top:var(--sp-2); }
.filter-row select {
  width:100%; background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r-sm);
  color:var(--text-2); padding:6px 8px; font-size:12px; outline:none; cursor:pointer;
}
.filter-row select:focus { border-color:var(--accent); }
.filter-row select option { background:var(--surface); }



#tree { flex:1; overflow-y:auto; padding:var(--sp-2) var(--sp-2) var(--sp-2); margin-top:var(--sp-2); }
.tree-folder { user-select:none; }
.tree-folder-label {
  display:flex; align-items:center; gap:6px; padding:6px 8px; border-radius:var(--r-sm); cursor:pointer;
  font-size:12.5px; font-weight:500; color:var(--text-2); transition:background .12s var(--ease);
}
.tree-folder-label:hover { background:var(--surface-2); color:var(--text); }
.tree-folder-label .caret { width:10px; height:10px; flex:none; transition:transform .15s var(--ease); color:var(--text-3); }
.tree-folder.collapsed > .tree-folder-children { display:none; }
.tree-folder.collapsed > .tree-folder-label .caret { transform:rotate(-90deg); }
.tree-folder-children { padding-left:12px; border-left:1px solid var(--border); margin-left:13px; }
.tree-leaf {
  display:flex; align-items:center; gap:9px; padding:6px 8px 6px 11px; border-radius:var(--r-sm); cursor:pointer;
  font-size:13px; color:var(--text); position:relative; transition:background .12s var(--ease);
}
.tree-leaf:hover { background:var(--surface-2); }
.tree-leaf.active { background:var(--accent-soft); color:var(--accent-strong); font-weight:600; }
.tree-leaf.active::before { content:''; position:absolute; left:-13px; top:0; bottom:0; width:2px; background:var(--accent); border-radius:2px; }
.tree-leaf .dot { width:7px; height:7px; border-radius:50%; flex:none; }
.tree-leaf .name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.hidden { display:none !important; }
.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; }

#main.wide { padding:0; }
.tab-content { display:none; min-height:0; }
#tab-browse.active { display:flex; flex-direction:column; flex:1; overflow:hidden; }
#tab-graph.active { display:flex; flex-direction:column; flex:1; overflow:hidden; }
.fade-in { animation:fadeIn .18s var(--ease); }
@keyframes fadeIn { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:none; } }

.tab-content { display:none; }
.tab-content.active { display:block; }

.eyebrow { font-size:11px; font-weight:600; color:var(--accent-strong); text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px; }
#landing h1 { font-size:24px; font-weight:700; letter-spacing:-.02em; margin-bottom:8px; }
#landing p.sub { color:var(--text-2); font-size:14px; margin-bottom:var(--sp-6); }
.stat-grid {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
  gap:var(--sp-4); margin-bottom:var(--sp-6);
}
.stat-card {
  background:rgba(19,19,25,.6); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(255,255,255,.06); border-radius:var(--r-lg);
  padding:var(--sp-5); position:relative;
  transition:all .2s var(--ease);
}
.stat-card::after {
  content:''; position:absolute; inset:0; border-radius:inherit;
  box-shadow:inset 0 1px 1px rgba(255,255,255,.08);
  pointer-events:none;
}
.stat-card:hover {
  border-color:rgba(255,255,255,.12);
  background:rgba(25,25,34,.7);
  box-shadow:0 0 0 1px var(--accent-soft);
}
.stat-card .n { font-size:24px; font-weight:700; letter-spacing:-.03em; }
.stat-card .l { font-size:11px; color:var(--text-3); margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; }
#landing .domains { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-2); align-items:start; }
#landing .domains { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-2) var(--sp-5); align-items:start; max-height:300px; overflow-y:auto; }
#landing { padding-bottom:100px; }
#landing .domains h3 { font-size:12px; color:var(--text-3); margin-bottom:var(--sp-3); text-transform:uppercase; letter-spacing:.06em; font-weight:600; grid-column:1/-1; }
.domain-row {
  display:flex; align-items:center; padding:8px 12px; border-radius:var(--r-sm); cursor:pointer;
  transition:background .12s var(--ease), border-color .12s var(--ease); border:1px solid transparent;
}
.domain-row:hover { background:var(--surface); border-color:var(--border); }
.domain-row .name { font-size:13px; font-weight:500; }
.domain-row .count { font-size:12px; color:var(--text-3); }
.domain-row .bar-wrap { flex:1; margin:0 var(--sp-4); height:6px; background:var(--surface-2); border-radius:99px; overflow:hidden; }
.domain-row .bar { height:100%; background:linear-gradient(90deg, var(--accent), var(--accent-strong)); border-radius:99px; }

#detail { max-width:1360px; display:flex; flex-direction:column; flex:1; min-height:0; }
.detail-tab-content { display:none; flex:1; min-height:0; overflow:auto; }
.detail-tab-content.active { display:flex; flex-direction:column; }
#detail-header {
  position:sticky; top:0; z-index:9;
  padding:var(--sp-5) var(--sp-5) var(--sp-4);
  margin:calc(-1 * var(--sp-4)) calc(-1 * var(--sp-4)) var(--sp-3);
  border-bottom:1px solid rgba(255,255,255,.06);
  position:relative;
}
html[data-theme="light"] #detail-header {
  border-bottom-color:rgba(0,0,0,.06);
}
#detail-header.hidden { display:none; }
#detail-topbar { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3); flex-wrap:wrap; min-height:28px; }
#detail-topbar + #detail-topbar { margin-top:2px; }
#detail-topbar-left { display:flex; flex-direction:column; align-items:flex-start; }
#detail-topbar-right { display:flex; align-items:center; gap:var(--sp-2); flex-shrink:0; }
#detail-topbar .resource { font-size:11px; color:var(--text-3); }
.vs-btn {
  display:inline-flex; align-items:center; gap:5px; padding:4px 10px; border-radius:99px; font-size:10.5px; font-weight:700;
  border:1px solid var(--border); cursor:pointer; color:var(--accent-strong); background:var(--accent-soft);
  transition:background .12s var(--ease), border-color .12s var(--ease);
}
.vs-btn:hover { background:var(--accent-strong); color:#fff; border-color:var(--accent-strong); }
#detail .desc { color:var(--text-2); font-size:14px; margin:var(--sp-2) 0 0; max-width:760px; }
.type-badge {
  display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:99px; font-size:11px; font-weight:700;
  text-transform:uppercase; letter-spacing:.04em; margin-bottom:var(--sp-3);
}
.type-badge .dot { width:6px; height:6px; border-radius:50%; }
#detail h2 { font-size:24px; font-weight:700; letter-spacing:-.02em; margin-bottom:6px; }
#detail p.desc { color:var(--text-2); font-size:14.5px; margin:6px 0; max-width:760px; }
#detail p.resource {
  font-size:12px; color:var(--text-3); margin-top:var(--sp-2); display:inline-flex; align-items:center; gap:6px;
  background:var(--surface-2); border:1px solid var(--border); padding:4px 10px; border-radius:var(--r-sm);
}
#detail-meta { display:flex; flex-wrap:wrap; gap:6px; margin:var(--sp-3) 0 var(--sp-5); }
.meta-tag {
  display:inline-flex; align-items:center; gap:4px; padding:3px 8px; border-radius:99px;
  font-size:10.5px; font-weight:600; text-transform:uppercase; letter-spacing:.03em;
  background:var(--surface-2); border:1px solid var(--border); color:var(--text-2);
}
.meta-tag .label { color:var(--text-3); font-weight:400; text-transform:none; }
.meta-tag-vis { background:var(--accent-soft); border-color:var(--accent); color:var(--accent-strong); }

/* ---- Cosmos Explore Tab ---- */
#graph-main { display:flex; flex:1; min-height:0; gap:var(--sp-3); padding:var(--sp-3); }
#cosmos-stage { position:relative; flex:0 0 70%; border-radius:var(--r-xl); overflow:hidden; background:var(--bg); border:1px solid rgba(255,255,255,.07); }
#cosmos-canvas { position:absolute; inset:0; z-index:9; pointer-events:none; width:100%; height:100%; }
#cluster-labels { position:absolute; inset:0; z-index:15; pointer-events:none; }
#graph-sidebar { flex:0 0 calc(30% - var(--sp-3)); display:flex; flex-direction:column; background:rgba(19,19,25,.72); backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px); border:1px solid rgba(255,255,255,.07); border-radius:var(--r-xl); box-shadow:var(--shadow-md); overflow:hidden; }
html[data-theme="light"] #graph-sidebar { background:rgba(255,255,255,.1); border-color:rgba(0,0,0,.08); }
#gs-header { display:flex; flex-direction:column; justify-content:start; padding:var(--sp-4) var(--sp-4) var(--sp-3); border-bottom:1px solid var(--border); flex:0 0 auto; gap:var(--sp-1); }
#gs-header .top { display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
#gs-title { font-size:13px; font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#gs-tabs { display:flex; flex:0 0 auto; border-bottom:1px solid var(--border); padding:0 var(--sp-2); gap:0; }
.gs-tab { flex:1; background:transparent; border:none; color:var(--text-3); font-size:12px; font-weight:600; padding:var(--sp-2) var(--sp-2); cursor:pointer; transition:all .12s var(--ease); font-family:inherit; border-bottom:2px solid transparent; }
.gs-tab:hover { color:var(--text-2); }
.gs-tab.active { color:var(--accent-strong); border-bottom-color:var(--accent); }
#gs-body { flex:1; min-height:0; overflow:auto; }
.gs-pane { display:none; padding:var(--sp-4); }
.gs-pane.active { display:block; }
.gs-pane .gs-section { margin-bottom:var(--sp-3); padding-bottom:var(--sp-3); border-bottom:1px solid var(--border); }
.gs-pane .gs-section:last-child { border-bottom:none; margin-bottom:0; padding-bottom:0; }
.gs-pane .gs-label { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-3); margin-bottom:var(--sp-2); }
.gs-pane .gs-desc { font-size:13px; color:var(--text-2); line-height:1.6; }
.gs-pane .gs-meta { display:flex; flex-wrap:wrap; gap:4px; }
.gs-pane .gs-tag { font-size:10px; font-weight:600; padding:2px 8px; border-radius:99px; background:var(--surface-2); border:1px solid var(--border); color:var(--text-2); }
.gs-pane .gs-chip { display:inline-flex; align-items:center; gap:5px; padding:5px 10px; border-radius:var(--r-md); font-size:11px; font-family:'JetBrains Mono',monospace; background:rgba(255,255,255,.03); border:1px solid var(--border); color:var(--text); cursor:pointer; transition:all .15s var(--ease); margin:0 4px 4px 0; }
.gs-pane .gs-chip:hover { border-color:var(--accent); background:var(--accent-soft); color:var(--accent-strong); }
.gs-pane .gs-chip .dot { width:5px; height:5px; border-radius:50%; flex:none; }
.gs-pane .gs-code { font-size:12px; line-height:1.6; white-space:pre-wrap; font-family:'JetBrains Mono',monospace; color:var(--text-2); background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r-md); padding:var(--sp-3); overflow-x:auto; }
.gs-empty { color:var(--text-3); font-size:12px; padding:var(--sp-6); text-align:center; }
.gs-type-badge { display:inline-flex; align-items:center; gap:5px; padding:3px 9px; border-radius:99px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
.gs-type-badge .dot { width:5px; height:5px; border-radius:50%; }

.cluster-label {
  position:absolute; transform:translate(-50%,-100%); font-size:11px; font-weight:700;
  color:rgba(255,255,255,.55); text-transform:uppercase; letter-spacing:.08em;
  text-shadow:0 1px 6px rgba(0,0,0,.8),0 0 20px rgba(0,0,0,.5); white-space:nowrap;
}
#cosmos-loading { position:absolute; inset:0; z-index:50; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; background:var(--bg); transition:opacity .3s var(--ease); }
#cosmos-loading.hide { opacity:0; pointer-events:none; }
#cosmos-loading.error .spinner { display:none; }
#cosmos-loading.error .lbl { color:#f87171; }
#cosmos-toolbar {
  position:absolute; top:10px; left:50%; transform:translateX(-55%); z-index:20;
  display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:14px;
  background:rgba(19,19,25,.72); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(255,255,255,.08); box-shadow:var(--shadow-md); max-width:600px;
}
html[data-theme="light"] #cosmos-toolbar { background:rgba(255,255,255,.75); border-color:rgba(0,0,0,.08); }
#cosmos-toolbar .search-box { position:relative; flex:0 0 auto; }
#cosmos-toolbar .search-box svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); width:14px; height:14px; color:var(--text-3); pointer-events:none; }
#cosmos-search { width:160px; background:var(--surface-2); border:1px solid var(--border); border-radius:8px; padding:6px 8px 6px 28px; color:var(--text); font-size:12px; outline:none; transition:border-color .15s var(--ease); }
#cosmos-search:focus { border-color:var(--accent); }
#cosmos-search::placeholder { color:var(--text-3); }

.dropdown-btn {
  background:transparent; border:1px solid transparent; color:var(--text-2); border-radius:6px;
  padding:5px 10px; font-size:12px; font-weight:500; cursor:pointer; font-family:inherit;
  display:flex; align-items:center; gap:6px; white-space:nowrap; transition:all .15s var(--ease);
}
.dropdown-btn:hover { background:var(--surface-2); }
.dropdown-btn.active { background:var(--accent-soft); color:var(--accent-strong); border-color:var(--accent); }
.dropdown-btn .badge { background:var(--accent); color:#fff; font-size:10px; font-weight:700; padding:1px 6px; border-radius:99px; }
.dropdown-wrapper { position:relative; }
.dropdown-menu {
  position:absolute; top:calc(100% + 6px); left:0; z-index:100; min-width:180px; max-height:300px;
  background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--r-lg);
  box-shadow:var(--shadow-lg); overflow:hidden; display:none; flex-direction:column;
}
.dropdown-menu.open { display:flex; }
.dropdown-list { max-height:260px; overflow-y:auto; padding:6px; display:flex; flex-direction:column; gap:2px; }
.dropdown-list::-webkit-scrollbar { width:5px; }
.dropdown-list::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:99px; }
.dropdown-item {
  display:flex; align-items:center; justify-content:space-between; padding:6px 10px; border-radius:6px;
  color:var(--text); font-size:12px; cursor:pointer; transition:background .1s; user-select:none;
}
.dropdown-item:hover { background:var(--surface-2); }
.item-left { display:flex; align-items:center; gap:8px; }
.item-cb {
  width:14px; height:14px; border-radius:4px; border:2px solid var(--border-strong); flex:none;
  display:flex; align-items:center; justify-content:center; transition:all .12s var(--ease);
}
.dropdown-item.on .item-cb { background:var(--accent); border-color:var(--accent); }
.dropdown-item.on .item-cb::after { content:'✓'; color:#fff; font-size:9px; font-weight:700; }
.item-count { font-size:10px; color:var(--text-3); background:var(--surface-2); padding:1px 6px; border-radius:99px; font-weight:600; }
.dropdown-item .dot { width:7px; height:7px; border-radius:50%; flex:none; }

.toolbar-sep { width:1px; align-self:stretch; background:var(--border); margin:2px 0; flex:0 0 auto; }
#status-pill {
  position:absolute; top:16px; left:16px; z-index:20; font-size:11px; color:var(--text-3);
  background:rgba(19,19,25,.72); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(255,255,255,.08); border-radius:99px; padding:7px 12px; box-shadow:var(--shadow-md);
}
html[data-theme="light"] #status-pill { background:rgba(255,255,255,.75); border-color:rgba(0,0,0,.08); }
#status-pill .n { color:var(--text); font-weight:700; }
#hover-tag {
  position:absolute; z-index:25; pointer-events:none; font-size:11.5px; font-weight:600;
  background:rgba(19,19,25,.9); color:var(--text); border:1px solid rgba(255,255,255,.1);
  padding:5px 10px; border-radius:8px; box-shadow:var(--shadow-md);
  opacity:0; transition:opacity .1s; transform:translate(-50%,-130%);
}
#hover-tag.show { opacity:1; }
#hover-tag .type { color:var(--text-3); font-weight:500; margin-left:5px; }
#cosmos-legend {
  position:absolute; bottom:16px; left:16px; z-index:20; border-radius:12px;
  background:rgba(19,19,25,.72); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(255,255,255,.08); box-shadow:var(--shadow-md); overflow:hidden;
}
html[data-theme="light"] #cosmos-legend { background:rgba(255,255,255,.75); border-color:rgba(0,0,0,.08); }
#cosmos-legend-head { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:10px 12px; cursor:pointer; }
#cosmos-legend-head span.t { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-3); }
#cosmos-legend-head svg { width:12px; height:12px; color:var(--text-3); transition:transform .15s var(--ease); }
#cosmos-legend.collapsed #cosmos-legend-body { display:none; }
#cosmos-legend.collapsed #cosmos-legend-body { display:none; }
#cosmos-legend.collapsed #cosmos-legend-head svg { transform:rotate(-90deg); }
#cosmos-legend-body { padding:0 12px 12px; display:flex; flex-direction:column; gap:4px; }
.legend-item { display:flex; align-items:center; gap:8px; padding:3px 6px; border-radius:6px; font-size:11px; font-weight:500; color:var(--text-2); }
.legend-item .dot { width:8px; height:8px; border-radius:50%; flex:none; }
.legend-item .count { color:var(--text-3); margin-left:auto; font-size:10px; font-weight:600; }
#zoomctl {
  position:absolute; bottom:16px; right:16px; z-index:20; display:flex; flex-direction:column; gap:2px;
  border-radius:12px; background:rgba(19,19,25,.72); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(255,255,255,.08); box-shadow:var(--shadow-md); overflow:hidden;
}
html[data-theme="light"] #zoomctl { background:rgba(255,255,255,.75); border-color:rgba(0,0,0,.08); }
#zoomctl button {
  width:38px; height:38px; background:transparent; border:none; color:var(--text-2); cursor:pointer;
  display:flex; align-items:center; justify-content:center; transition:background .12s var(--ease),color .12s var(--ease);
}
#zoomctl button:hover { background:var(--surface-2); color:var(--text); }
#zoomctl button:not(:last-child) { border-bottom:1px solid var(--border); }
#zoomctl svg { width:16px; height:16px; }
.detail-btn:hover { background:var(--accent); color:#fff; }
.detail-btn svg { width:13px; height:13px; }

/* ---- Parse Tree ---- */
#code-split-parse .parse-node {
  padding:2px 0 2px 20px; cursor:default; white-space:nowrap;
  border-left:1px solid var(--border); margin-left:10px;
}
.parse-node.root { margin-left:0; border-left:none; padding-left:0; }
.parse-node .node-toggle {
  display:inline-block; width:14px; text-align:center; cursor:pointer; user-select:none;
  color:var(--text-3); font-size:10px; margin-left:-18px;
}
.parse-node .node-type { color:var(--accent-strong); font-weight:500; }
.parse-node .node-text { color:var(--text-2); }
.parse-node .node-text.str { color:#fbbf24; }
.parse-node .node-text.num { color:#fb923c; }
.parse-node .node-text.kw { color:#f472b6; font-weight:500; }
.parse-node .node-text.id { color:#38bdf8; }
.parse-node.collapsed > .parse-children { display:none; }
.parse-node.collapsed > .node-toggle { transform:rotate(0deg); }
.parse-node:not(.collapsed) > .node-toggle { transform:rotate(90deg); }
.parse-node .parse-range { font-size:10px; color:var(--text-3); margin-left:6px; }
.close-btn {
  width:30px; height:30px; border-radius:var(--r-sm); background:transparent; border:1px solid transparent;
  color:var(--text-2); display:flex; align-items:center; justify-content:center; cursor:pointer;
  transition:background .12s var(--ease);
}
#code-split-source pre {
  font-size:12px; line-height:1.6; white-space:pre-wrap; tab-size:2; overflow-x:auto;
  color:var(--text); font-family:'JetBrains Mono',ui-monospace,Menlo,monospace;
  background:transparent !important; margin:0;
}
#code-split-source pre code { background:transparent !important; }
html[data-theme="light"] pre[class*="language-"],
html[data-theme="light"] code[class*="language-"] { color:#000 !important; background:transparent !important; }
html[data-theme="light"] .token.operator,
html[data-theme="light"] .token.punctuation,
html[data-theme="light"] .token.plain-text { color:#333 !important; }

#detail-tab-bar { display:flex; gap:0; margin:0 0 var(--sp-4); padding:var(--sp-1); background:rgba(255,255,255,.04); border-radius:var(--r-md); border:1px solid rgba(255,255,255,.06); width:fit-content; }
html[data-theme="light"] #detail-tab-bar { background:rgba(0,0,0,.03); border-color:rgba(0,0,0,.06); }
.detail-tab {
  background:transparent; border:none; color:var(--text-3); padding:6px 18px;
  font-size:12px; font-weight:500; cursor:pointer; transition:all .15s var(--ease);
  border-radius:6px; font-family:inherit;
}
.detail-tab:hover { color:var(--text); background:rgba(255,255,255,.06); }
html[data-theme="light"] .detail-tab:hover { background:rgba(0,0,0,.04); }
.detail-tab.active { color:var(--text); background:var(--surface); box-shadow:0 1px 3px rgba(0,0,0,.12); }

#detail-grid { display:grid; grid-template-columns:1fr minmax(260px,340px); gap:var(--sp-5); align-items:start; }
@media (max-width:1080px) { #detail-grid { grid-template-columns:1fr; } }
@media (max-width:720px) { #detail { padding:0; } }
#detail-right { position:sticky; top:var(--sp-4); display:flex; flex-direction:column; gap:var(--sp-4); }
@media (max-width:1080px) { #detail-right { position:static; } }
#code-split { display:flex; min-height:400px; flex:1; gap:0; border:1px solid var(--border); border-radius:var(--r-md); overflow:hidden; }
#code-split-source { flex:1; overflow:auto; padding:var(--sp-4); background:var(--surface); }
#code-split-source pre { margin:0; font-size:13px; white-space:pre-wrap; word-break:break-word; }
#code-split-divider { width:4px; cursor:col-resize; background:var(--border); flex:none; }
#code-split-divider:hover { background:var(--accent); }
#code-split-parse { flex:1; overflow:auto; padding:var(--sp-4); }
.code-placeholder { color:var(--text-3); font-size:13px; text-align:center; padding:var(--sp-8); }

.sec {
  background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg);
  margin-bottom:var(--sp-4); margin-top:var(--sp-4); overflow:hidden;
  box-shadow:0 2px 8px rgba(0,0,0,.15);
  transition:border-color .2s var(--ease);
}
.sec::before { display:none; }
.sec:hover { border-color:var(--border-strong); }
.sec h3 {
  font-size:11px; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; font-weight:600;
  padding:var(--sp-3) var(--sp-4); border-bottom:1px dashed var(--border-strong);
  background:transparent; margin:0;
}
.sec pre {
  padding:var(--sp-4); font-size:13px; overflow-x:auto; color:var(--text-2);
  white-space:pre-wrap; word-break:break-word; margin:0;
  background:rgba(0,0,0,.2);
}
.sec-inline { padding:var(--sp-3); font-size:13px; color:var(--text-2); }
.sec .empty-note { color:var(--text-3); font-size:12px; padding:var(--sp-3); }
#detail-left { display:flex; flex-direction:column; gap:0; }
#detail-mid-grid { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-3); margin-bottom:var(--sp-4); }
@media (max-width:900px) { #detail-mid-grid { grid-template-columns:1fr; } }
.param-row { display:flex; font-size:12.5px; padding:4px 0; border-bottom:1px solid var(--border); }
.param-row:last-child { border-bottom:none; }
.param-name { color:var(--accent-strong); font-weight:500; min-width:100px; flex:none; }
.param-type { color:var(--text-2); }
.param-default { color:var(--text-3); margin-left:auto; font-size:11px; }
.returns-val { font-size:13px; color:var(--text-2); padding:var(--sp-3); }
.chip-list { display:flex; flex-wrap:wrap; gap:6px; }
.chip {
  display:inline-flex; align-items:center; gap:8px; padding:6px 12px; border-radius:var(--r-md);
  font-size:12px; font-family:'JetBrains Mono',ui-monospace,monospace;
  background:rgba(255,255,255,.03); border:1px solid rgba(255,255,255,.08);
  color:var(--text); transition:all .2s var(--ease); cursor:pointer;
}
.chip:hover { border-color:var(--accent); background:var(--accent-soft); color:var(--accent-strong); transform:translateY(-1px); }
.chip .dot { width:6px; height:6px; border-radius:50%; flex:none; }
.markdown-body { font-size:13.5px; color:var(--text-2); }
.markdown-body h1, .markdown-body h2 { font-size:14px; color:var(--text); margin:10px 0 4px; font-weight:600; }
.markdown-body code { background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:12px; }
.markdown-body table { border-collapse:collapse; width:100%; font-size:12.5px; margin:6px 0; }
.markdown-body th, .markdown-body td { border:1px solid var(--border); padding:5px 8px; }

#ego-graph { height:280px; width:100%; background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r-md); }
#ego-hint { font-size:11px; color:var(--text-3); margin-top:var(--sp-2); }

#global-graph-view { flex:1; min-height:300px; background:var(--bg); border:1px solid var(--border); border-radius:var(--r-md); }

.loading { display:flex; align-items:center; justify-content:center; height:100%; color:var(--text-muted); font-size:13px; gap:8px; color:var(--text-3); }
.spinner { width:18px; height:18px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.6s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce) { *,*::before,*::after { animation-duration:.01ms!important; transition-duration:.01ms!important; } }
</style>
</head>
<body>
<div id="topbar">
  <div id="topbar-inner">
    <div id="brand">
      <div class="nav-logo-icon" aria-hidden="true">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220" width="30" height="30">
          <defs>
            <linearGradient id="icon_grad" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stop-color="#5b21b6" />
              <stop offset="50%" stop-color="#7c3aed" />
              <stop offset="100%" stop-color="#a78bfa" />
            </linearGradient>
          </defs>
          <g transform="translate(5, 10)">
            <rect x="20" y="30" width="30" height="12" rx="6" fill="#c4b5fd" />
            <rect x="60" y="30" width="50" height="12" rx="6" fill="url(#icon_grad)" />
            <path d="M 120 36 L 160 36 L 175 60" fill="none" stroke="url(#icon_grad)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />
            <rect x="10" y="65" width="25" height="14" rx="7" fill="#7c3aed" />
            <rect x="45" y="65" width="75" height="14" rx="7" fill="url(#icon_grad)" />
            <path d="M 130 72 L 180 72 L 195 90" fill="none" stroke="url(#icon_grad)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
            <rect x="30" y="100" width="40" height="14" rx="7" fill="#a78bfa" />
            <path d="M 80 107 L 150 107" fill="none" stroke="url(#icon_grad)" stroke-width="10" stroke-linecap="round" />
            <circle cx="195" cy="107" r="16" fill="url(#icon_grad)" />
            <circle cx="195" cy="107" r="7" fill="#ffffff" />
            <circle cx="195" cy="107" r="3" fill="#4c1d95" />
            <rect x="15" y="135" width="60" height="14" rx="7" fill="url(#icon_grad)" />
            <path d="M 85 142 L 165 142 L 195 125" fill="none" stroke="url(#icon_grad)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
            <rect x="35" y="170" width="45" height="12" rx="6" fill="#c4b5fd" />
            <path d="M 90 176 L 140 176 L 175 150" fill="none" stroke="url(#icon_grad)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />
          </g>
        </svg>
      </div>
      <div id="brand-text">
        <span class="name">OKF Generator</span>
      </div>
    </div>
    <div id="topbar-spacer"></div>
    <div class="nav-seg">
      <button class="nav-seg-item" onclick="switchTab('browse',this)">Browse</button>
      <button class="nav-seg-item active" onclick="switchTab('graph',this)">Explore</button>
    </div>
    <div id="topbar-spacer"></div>
    <a href="https://github.com/UmairBaig8/okf-generator" target="_blank" class="icon-btn" style="text-decoration:none" title="GitHub">
      <svg viewBox="0 0 24 24" fill="currentColor" width="17" height="17"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
    </a>
    <button class="icon-btn" id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark theme" title="Toggle theme">
      <svg id="theme-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="17" height="17"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path></svg>
    </button>
  </div>
</div>
<div id="layout">
  <div id="main">
    <div id="tab-browse" class="tab-content">
      <div id="browse-layout">
        <div id="sidebar">
          <div class="field-wrap">
            <div class="search-box">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path></svg>
              <label for="search" class="visually-hidden">Search concepts</label>
              <input id="search" type="text" placeholder="Search concepts..." aria-label="Search concepts">
            </div>
            <div class="filter-row">
              <select id="bundle-select" aria-label="Filter by bundle"><option value="">All Bundles</option></select>
              <select id="type-filter" aria-label="Filter by type"><option value="">All types</option></select>
              <select id="lang-filter" aria-label="Filter by language"><option value="">All langs</option></select>
            </div>
          </div>
          <div id="tree"></div>
        </div>
        <div id="sidebar-resize"></div>
        <div id="browse-content">
          <div id="breadcrumb"><span class="crumb current" onclick="showLanding()">Home</span></div>
          <div id="detail-header"></div>
          <div id="landing"></div>
          <div id="detail" class="hidden">
            <div id="detail-tab-bar">
              <button class="detail-tab active" data-tab="details" onclick="switchDetailTab('details',this)">Details</button>
              <button class="detail-tab" data-tab="code" onclick="switchDetailTab('code',this)">Code</button>
            </div>
            <div id="detail-tab-details" class="detail-tab-content active">
              <div id="detail-grid">
                <div id="detail-left"></div>
                <div id="detail-right"></div>
              </div>
            </div>
            <div id="detail-tab-code" class="detail-tab-content">
              <div id="code-split">
                <div id="code-split-source"></div>
                <div id="code-split-divider"></div>
                <div id="code-split-parse"><div class="code-placeholder">Parse tree appears here</div></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div id="tab-graph" class="tab-content active">
      <div id="graph-main">
      <div id="cosmos-stage">
        <canvas id="cosmos-canvas"></canvas>
        <div id="cluster-labels"></div>

        <div id="cosmos-loading">
          <div class="spinner"></div>
          <div class="lbl" id="cosmos-loading-lbl">Loading graph…</div>
        </div>

        <div id="hover-tag"></div>

        <div id="status-pill"><span class="n" id="status-n">0</span><span id="status-of"></span><span> concepts </span><span style="opacity:.4">·</span><span> </span><span class="n" id="status-e">0</span><span> edges</span><span id="graph-max-nodes" style="display:none">{GRAPH_MAX_NODES}</span></div>

         <div id="cosmos-toolbar">
           <div class="search-box">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path></svg>
             <input id="cosmos-search" type="text" placeholder="Search concepts…" style="width:140px">
           </div>
           <div class="toolbar-sep"></div>
           <div class="filter-group" style="display:flex;gap:4px">
             <div class="dropdown-wrapper">
               <button class="dropdown-btn" id="type-btn" onclick="toggleDropdown('type')">Type <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg></button>
               <div class="dropdown-menu" id="type-menu"><div class="dropdown-list" id="type-list"></div></div>
             </div>
             <div class="dropdown-wrapper">
               <button class="dropdown-btn" id="bundle-btn" onclick="toggleDropdown('bundle')">Bundle <span class="badge" id="bundle-badge">All</span> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg></button>
               <div class="dropdown-menu" id="bundle-menu"><div class="dropdown-list" id="bundle-list"></div></div>
             </div>
           </div>
         </div>

        <div id="cosmos-legend">
          <div id="cosmos-legend-head" onclick="document.getElementById('cosmos-legend').classList.toggle('collapsed')">
            <span class="t">Legend</span>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M6 9l6 6 6-6"></path></svg>
          </div>
          <div id="cosmos-legend-body"></div>
        </div>

        <div id="zoomctl">
          <button onclick="cosmosZoom(1.4)" title="Zoom in"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
          <button onclick="cosmosZoom(0.71)" title="Zoom out"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/></svg></button>
          <button onclick="cosmosReset()" title="Fit to screen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg></button>
        </div>
      </div>
      <div id="graph-sidebar">
          <div id="gs-header">
            <div class="top">
              <span id="gs-header-badge" style="display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:99px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;display:none"></span>
              <span id="gs-title">Concept Details</span>
            </div>
            <div id="gs-header-desc" style="display:none;font-size:12px;color:var(--text-2);line-height:1.5;margin-top:2px"></div>
          </div>
        <div id="gs-tabs">
          <button class="gs-tab active" data-gs="details" onclick="gsSwitchTab('details',this)">Details</button>
          <button class="gs-tab" data-gs="source" onclick="gsSwitchTab('source',this)">Source</button>
        </div>
        <div id="gs-body">
          <div class="gs-pane active" id="gs-details">
            <div class="gs-section" id="gs-details-default" style="padding:var(--sp-6) 0;text-align:center">
              <div style="font-size:15px;font-weight:700;color:var(--text);margin-bottom:var(--sp-2)">Knowledge Bundle Explorer</div>
              <div style="font-size:12px;color:var(--text-3);line-height:1.6;max-width:240px;margin:0 auto">Click any node in the graph to inspect its details, relationships, and source code.</div>
              <div style="margin-top:var(--sp-5);display:flex;flex-wrap:wrap;gap:var(--sp-2);justify-content:center" id="gs-default-stats"></div>
            </div>
            <div id="gs-details-body"></div>
          </div>
          <div class="gs-pane" id="gs-source">
            <pre class="gs-code" id="gs-source-content">Click a concept to view source.</pre>
          </div>
        </div>
      </div>
      </div>
    </div>
  </div>
</div>



<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-tree-sitter@0.20.8/tree-sitter.js"></script>
<script>
var DYNAMIC = {DYNAMIC_FLAG};
var BUNDLE_DATA = {BUNDLE_DATA};

var TYPE_COLORS = {
  Class:"#a78bfa", Dependency:"#4ade80", Function:"#38bdf8",
  Index:"#f472b6", Interface:"#22d3ee", Method:"#c084fc",
  Module:"#fbbf24", Table:"#fb7185", Trigger:"#f43f5e",
  Type:"#818cf8", View:"#fb923c",
};

var __nodeById = {};
var __adj = {};

function esc(s) {
  if (s === null || s === undefined) return '';
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function qs(id) { return document.getElementById(id); }

// ---- Data Layer ----
async function api(path) { var r = await fetch(path); return r.json(); }

function buildStaticIndex(data) {
  (data.nodes || []).forEach(function(n) { __nodeById[n.id] = n; });
  (data.nodes || []).forEach(function(n) { __adj[n.id] = new Set(); });
  (data.links || []).forEach(function(l) {
    if (__adj[l.source]) __adj[l.source].add(l.target);
    if (__adj[l.target]) __adj[l.target].add(l.source);
  });
}

async function getData() {
  if (!DYNAMIC && BUNDLE_DATA && BUNDLE_DATA.nodes) {
    if (Object.keys(__nodeById).length === 0) buildStaticIndex(BUNDLE_DATA);
    return { type: 'static', data: BUNDLE_DATA, nodeById: __nodeById, adj: __adj };
  }
  if (DYNAMIC) return { type: 'dynamic' };
  return null;
}

async function searchConcepts(q, typeFilter, langFilter, limit) {
  limit = limit || 200;
  if (DYNAMIC) {
    var url = '/api/search?q=' + encodeURIComponent(q) + '&type=' + encodeURIComponent(typeFilter) + '&limit=' + limit;
    if (langFilter) url += '&tag=' + encodeURIComponent('lang:' + langFilter);
    return await api(url);
  }
  if (!BUNDLE_DATA || !BUNDLE_DATA.nodes) return [];
  var res = BUNDLE_DATA.nodes;
  if (q) {
    var tokens = q.toLowerCase().split(/\s+/).filter(Boolean);
    res = res.filter(function(n) {
      var text = (n.title + ' ' + (n.id || '') + ' ' + (n.description || '')).toLowerCase();
      return tokens.every(function(t) { return text.includes(t); });
    });
  }
  if (typeFilter) res = res.filter(function(n) { return n.type === typeFilter; });
  if (langFilter) res = res.filter(function(n) { return (n.tags || []).indexOf('lang:' + langFilter) >= 0; });
  return res.map(function(n) { return { concept_id: n.id, title: n.title, type: n.type, resource: n.resource, description: n.description, tags: n.tags || [] }; });
}

async function getConcept(id) {
  if (DYNAMIC) return await api('/api/concept/' + encodeURIComponent(id));
  return __nodeById[id] || null;
}

async function getInfo() {
  if (DYNAMIC) return await api('/api/info');
  if (!BUNDLE_DATA || !BUNDLE_DATA.nodes) return { total: 0, types: {}, languages: {} };
  var types = {}, langs = {};
  BUNDLE_DATA.nodes.forEach(function(n) {
    types[n.type] = (types[n.type] || 0) + 1;
    (n.tags || []).forEach(function(t) {
      if (t.indexOf('lang:') === 0 && t !== 'lang:manifest') langs[t.slice(5)] = (langs[t.slice(5)] || 0) + 1;
    });
  });
  return { total: BUNDLE_DATA.total_concepts || BUNDLE_DATA.nodes.length, types: types, languages: langs, name: BUNDLE_DATA.bundle_name || 'okf_bundle' };
}

async function getTypes() {
  if (DYNAMIC) return await api('/api/types');
  if (!BUNDLE_DATA || !BUNDLE_DATA.nodes) return [];
  var counts = {};
  BUNDLE_DATA.nodes.forEach(function(n) { counts[n.type] = (counts[n.type] || 0) + 1; });
  return Object.keys(counts).sort().map(function(k) { return { type: k, count: counts[k] }; });
}

async function getLanguages() {
  if (DYNAMIC) return await api('/api/languages');
  if (!BUNDLE_DATA || !BUNDLE_DATA.nodes) return [];
  var counts = {};
  BUNDLE_DATA.nodes.forEach(function(n) {
    (n.tags || []).forEach(function(t) {
      if (t.indexOf('lang:') === 0 && t !== 'lang:manifest') counts[t.slice(5)] = (counts[t.slice(5)] || 0) + 1;
    });
  });
  return Object.keys(counts).sort().map(function(k) { return { language: k, count: counts[k] }; });
}

async function getGraph(maxNodes) {
  if (DYNAMIC) return await api('/api/graph?max_nodes=' + (maxNodes || 120));
  if (!BUNDLE_DATA) return { nodes: [], edges: [] };
  var nodes = (BUNDLE_DATA.nodes || []).slice(0, maxNodes || 120).map(function(n) {
    return { id: n.id, label: n.title, title: n.type + ': ' + n.title, group: n.type };
  });
  return { nodes: nodes, edges: (BUNDLE_DATA.links || []).slice(0, 300), total: BUNDLE_DATA.nodes.length, shown: nodes.length };
}

function getEgoElements(conceptId, concept) {
  if (!DYNAMIC && __nodeById[conceptId]) {
    var neighbors = Array.from(__adj[conceptId] || []);
    var nodes = [{ data: { id: conceptId, label: __nodeById[conceptId].title, type: __nodeById[conceptId].type } }];
    neighbors.forEach(function(nid) {
      var n = __nodeById[nid];
      if (n) nodes.push({ data: { id: nid, label: n.title, type: n.type } });
    });
    var edges = [];
    (BUNDLE_DATA.links || []).forEach(function(l) {
      if (l.source === conceptId || l.target === conceptId)
        edges.push({ data: { source: l.source, target: l.target } });
    });
    return { elements: { nodes: nodes, edges: edges } };
  }
  var els = [];
  var added = {};
  els.push({ data: { id: conceptId, label: concept.title, type: concept.type } });
  added[conceptId] = true;
  function addRel(items, dir) {
    (items || []).forEach(function(r) {
      if (!added[r.concept_id]) {
        els.push({ data: { id: r.concept_id, label: r.title, type: 'Unknown' } });
        added[r.concept_id] = true;
      }
      if (dir === 'in') els.push({ data: { source: r.concept_id, target: conceptId } });
      else els.push({ data: { source: conceptId, target: r.concept_id } });
    });
  }
  addRel(concept.related, 'out');
  addRel(concept.calls, 'out');
  addRel(concept.called_by, 'in');
  addRel(concept.used_by, 'in');
  return { elements: els };
}

// ---- Init ----
async function init() {
  if (!DYNAMIC && BUNDLE_DATA && BUNDLE_DATA.nodes) buildStaticIndex(BUNDLE_DATA);

  await getInfo();

  var types = await getTypes();
  var sel = qs('type-filter');
  types.forEach(function(t) {
    var o = document.createElement('option');
    o.value = t.type; o.textContent = t.type + ' (' + t.count + ')';
    sel.appendChild(o);
  });

  var langs = await getLanguages();
  var lsel = qs('lang-filter');
  langs.forEach(function(l) {
    var o = document.createElement('option');
    o.value = l.language; o.textContent = l.language + ' (' + l.count + ')';
    lsel.appendChild(o);
  });

  await doSearch();

  // Populate bundle dropdown with counts from search results
  var bundleCounts = {};
  currentResults.forEach(function(c) {
    var seg = (c.concept_id || '').split('/')[0];
    if (seg) bundleCounts[seg] = (bundleCounts[seg] || 0) + 1;
  });
  // Also add explicitly listed bundles (static)
  if (!DYNAMIC && BUNDLE_DATA && BUNDLE_DATA.bundles) {
    BUNDLE_DATA.bundles.forEach(function(b) {
      if (!bundleCounts[b]) bundleCounts[b] = 0;
    });
  }
  var bsel = qs('bundle-select');
  Object.keys(bundleCounts).sort().forEach(function(b) {
    var o = document.createElement('option');
    o.value = b; o.textContent = b + ' (' + bundleCounts[b] + ')';
    bsel.appendChild(o);
  });
  bsel.addEventListener('change', doSearch);

  // Activate default tab (Explore)
  switchTab('graph', document.querySelector('.nav-seg-item.active'));

  // Pre-load cosmos in background (dynamic mode)
  if (DYNAMIC) loadCosmosExplore();
}
document.addEventListener('DOMContentLoaded', init);

// ---- Search ----
var currentResults = [];

async function doSearch() {
  var q = qs('search').value;
  var t = qs('type-filter').value;
  var l = qs('lang-filter').value;
  var b = qs('bundle-select').value;
  currentResults = await searchConcepts(q, t, l, DYNAMIC ? 999999 : 0);
  if (b) currentResults = currentResults.filter(function(c) { return c.concept_id && c.concept_id.indexOf(b + '/') === 0; });
  renderTree(currentResults);
  if (qs('detail').classList.contains('hidden')) {
    if (DYNAMIC) {
      var infoData = await api('/api/info');
      renderLandingStats(currentResults, infoData.total);
    } else {
      renderLandingStats(currentResults);
    }
  }
  var noRes = qs('search-no-results');
  if (!DYNAMIC && currentResults.length === 0 && (q || t || l || b)) {
    if (!noRes) { noRes = document.createElement('div'); noRes.id = 'search-no-results'; var bc = qs('browse-content'); if (bc) bc.appendChild(noRes); else qs('main').appendChild(noRes); }
    noRes.innerHTML = '<div style="padding:16px 24px;margin:8px 16px;text-align:center;color:var(--text-3);font-size:13px;line-height:1.6;background:var(--surface-2);border-radius:var(--r-md);border:1px solid var(--border)">No concepts match — only <span class="n" id="status-n2">0</span> of <span id="status-of2"></span> concepts loaded in this view.<br>Run <code style="background:var(--surface);padding:2px 6px;border-radius:4px">okf dashboard</code> for full-bundle search across all concepts.</div>';
    var sn = qs('status-n'); var so = qs('status-of');
    if (sn) qs('status-n2').textContent = sn.textContent;
    if (so) qs('status-of2').textContent = so.textContent;
  } else if (noRes) { noRes.remove(); }
}
qs('search').addEventListener('input', doSearch);
qs('type-filter').addEventListener('change', doSearch);
qs('lang-filter').addEventListener('change', doSearch);

// ---- Tree ----
function renderTree(results) {
  var root = { __children: {}, __concepts: [] };
  results.forEach(function(c) {
    var id = c.concept_id || c.id;
    if (!id) return;
    var parts = id.split('/');
    var folderParts = parts.slice(0, -1);
    var cur = root;
    folderParts.forEach(function(f) {
      cur.__children[f] = cur.__children[f] || { __children: {}, __concepts: [] };
      cur = cur.__children[f];
    });
    cur.__concepts = cur.__concepts || [];
    cur.__concepts.push(c);
  });

  var tree = qs('tree');
  tree.innerHTML = '';
  tree.appendChild(renderFolder(root, ''));
}

function renderFolder(node, path) {
  var wrap = document.createElement('div');
  wrap.className = 'tree-folder';
  var children = node.__children || {};
  var concepts = node.__concepts || [];
  var folderNames = Object.keys(children).sort();

  if (path) {
    var label = document.createElement('div');
    label.className = 'tree-folder-label';
    label.tabIndex = 0;
    label.setAttribute('role', 'button');
    label.setAttribute('aria-expanded', 'true');
    label.innerHTML = '<svg class="caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M6 9l6 6 6-6"></path></svg><span>' + esc(path.split('/').pop()) + '</span>';
    var toggle = function() {
      wrap.classList.toggle('collapsed');
      label.setAttribute('aria-expanded', String(!wrap.classList.contains('collapsed')));
    };
    label.onclick = toggle;
    label.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } };
    wrap.appendChild(label);
  }

  var childWrap = document.createElement('div');
  childWrap.className = 'tree-folder-children';
  folderNames.forEach(function(fname) {
    childWrap.appendChild(renderFolder(children[fname], path ? path + '/' + fname : fname));
  });
  concepts.sort(function(a, b) { return (a.title || '').localeCompare(b.title || ''); }).forEach(function(c) {
    var leaf = document.createElement('div');
    leaf.className = 'tree-leaf';
    leaf.dataset.id = c.concept_id || c.id;
    leaf.dataset.type = c.type;
    leaf.dataset.title = (c.title || '').toLowerCase();
    leaf.tabIndex = 0;
    leaf.setAttribute('role', 'button');
    leaf.setAttribute('aria-label', c.type + ': ' + c.title);
    var color = TYPE_COLORS[c.type] || '#888';
    leaf.innerHTML = '<span class="dot" style="background:' + color + '" aria-hidden="true"></span><span class="name">' + esc(c.title) + '</span>';
    leaf.onclick = function() { selectConcept(leaf.dataset.id); };
    leaf.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectConcept(leaf.dataset.id); } };
    childWrap.appendChild(leaf);
  });
  wrap.appendChild(childWrap);
  return wrap;
}

// ---- Landing ----
function renderLandingStats(nodes, totalOverride) {
  var types = {}, doms = {}, total = totalOverride || nodes.length;
  nodes.forEach(function(n) {
    types[n.type] = (types[n.type] || 0) + 1;
    var parts = (n.concept_id || n.id || '').split('/');
    var dom = parts.length > 1 ? parts[0] : '(root)';
    doms[dom] = (doms[dom] || 0) + 1;
  });
  var maxDom = Math.max.apply(null, Object.keys(doms).map(function(k) { return doms[k]; })) || 1;
  var sorted = Object.keys(types).sort(function(a, b) { return types[b] - types[a]; });

  var statHtml = '<div class="stat-grid">';
  sorted.forEach(function(t) {
    var color = TYPE_COLORS[t] || '#888';
    statHtml += '<div class="stat-card"><div class="n" style="color:' + color + '">' + types[t] + '</div><div class="l">' + esc(t) + '</div></div>';
  });
  statHtml += '</div>';

  var domHtml = '';
  if (Object.keys(doms).length) {
    domHtml = '<div class="domains"><h3>Domains</h3>';
    var domKeys = Object.keys(doms).sort(function(a, b) { return doms[b] - doms[a]; });
    var totalDomains = domKeys.length;
    if (totalDomains > 12) domKeys = domKeys.slice(0, 12);
    domKeys.forEach(function(d) {
      var pct = Math.round((doms[d] / maxDom) * 100);
      domHtml += '<div class="domain-row" tabindex="0" role="button" onclick="filterToDomain(\'' + d.replace(/'/g,"\\'") + '\')"><span class="name">' + esc(d) + '</span><div class="bar-wrap"><div class="bar" style="width:' + pct + '%"></div></div><span class="count">' + doms[d] + '</span></div>';
    });
    if (totalDomains > 12) domHtml += '<div class="domain-row" style="cursor:default;opacity:.5"><span class="name">+' + (totalDomains - 12) + ' more</span></div>';
    domHtml += '</div>';
  }

  var b = qs('bundle-select').value;
  var label = b || 'All Bundles';
  var heading = total + ' concepts' + (b ? ' in ' + b : '');
  var nDomains = Object.keys(doms).length;
  qs('landing').innerHTML = '<div class="eyebrow">' + esc(label) + '</div><h1>' + esc(heading) + '</h1><p class="sub">' + total + ' concepts across ' + nDomains + ' domains.</p>' + statHtml + domHtml;
}

function breadcrumbNav(path) {
  // Try direct path first, then path/index for module concepts
  if (__nodeById[path]) { selectConcept(path); return; }
  if (__nodeById[path + '/index']) { selectConcept(path + '/index'); return; }
  // Fallback: filter by domain
  filterToDomain(path.split('/')[0]);
}

function showLanding() {
  qs('detail').classList.add('hidden');
  qs('detail-header').classList.add('hidden');
  var landing = qs('landing');
  landing.classList.remove('hidden');
  landing.classList.add('fade-in');
  qs('breadcrumb').innerHTML = '<span class="crumb current" onclick="showLanding()">Home</span>';
  document.querySelectorAll('.tree-leaf.active').forEach(function(el) { el.classList.remove('active'); });
  renderLandingStats(currentResults);
}

function filterToDomain(d) {
  qs('search').value = d;
  doSearch();
}

// ---- Detail ----
var currentId = null;
var conceptCache = {};
var egoCy = null;
var globalCy = null;

async function selectConcept(id) {
  qs('landing').classList.add('hidden');
  qs('detail-header').classList.remove('hidden');
  qs('detail').classList.remove('hidden');
  qs('detail').classList.remove('fade-in'); void qs('detail').offsetWidth; qs('detail').classList.add('fade-in');

  document.querySelectorAll('.tree-leaf.active').forEach(function(el) { el.classList.remove('active'); });
  var leaf = document.querySelector('.tree-leaf[data-id="' + id.replace(/"/g,'\\"') + '"]');
  if (leaf) leaf.classList.add('active');

  var c;
  if (conceptCache[id]) { c = conceptCache[id]; }
  else { c = await getConcept(id); conceptCache[id] = c; }
  if (!c) { qs('detail-left').innerHTML = '<p>Concept not found.</p>'; return; }
  currentId = id;
  renderDetail(c);

  // Pre-populate code tab in background
  var _ext = (c.resource || '').split('.').pop();
  var _lang = {js:'javascript',ts:'typescript',py:'python',rb:'ruby',rs:'rust',go:'go',java:'java',kt:'kotlin',cs:'csharp',cpp:'cpp',c:'c',sql:'sql',sh:'bash',yaml:'yaml',json:'json',html:'html',css:'css',toml:'ini',swift:'swift',php:'php',scala:'scala',dart:'dart'}[_ext] || _ext || '';
  var _code = c.code || c.source || '';
  if (_code && !/^Lines \d/.test(_code.trim())) {
    qs('code-split-source').innerHTML = '<pre><code class="language-' + _lang + '">' + esc(_code) + '</code></pre>';
    var _ce = qs('code-split-source').querySelector('code');
    if (_ce && window.Prism) Prism.highlightElement(_ce);
  } else if (c.source && /^Lines \d/.test(c.source)) {
    qs('code-split-source').innerHTML = '<div class="code-placeholder">Source code not embedded in this bundle view. Run <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px">okf dashboard</code> for source lookup, or open <code>' + esc(c.resource || '') + '</code> directly.</div>';
  } else {
    qs('code-split-source').innerHTML = '<div class="code-placeholder">No source code available for this concept.</div>';
  }
  showParseTreePrompt(id);

  // Build breadcrumb from concept path segments
  var parts = id.split('/');
  var crumbHtml = '<span class="crumb" onclick="showLanding()">Home</span>';
  if (parts.length > 1) {
    var path = '';
    for (var i = 0; i < parts.length - 1; i++) {
      var seg = parts[i];
      path = path ? path + '/' + seg : seg;
      crumbHtml += '<span class="sep">›</span><span class="crumb" onclick="breadcrumbNav(\'' + esc(path) + '\')">' + esc(seg) + '</span>';
    }
  }
  crumbHtml += '<span class="sep">›</span><span class="crumb current">' + esc(c.title || parts[parts.length - 1]) + '</span>';
  qs('breadcrumb').innerHTML = crumbHtml;
}

function _parseRel(text) {
  if (!text) return [];
  if (Array.isArray(text)) return text;
  var out = [];
  text.split('\n').forEach(function(line) {
    var m = line.match(/\[([^\]]+)\]\(([^)]+)\.md\)/);
    if (m) out.push({ title: m[1], concept_id: m[2] });
  });
  return out;
}

function _parseRelationships(text, type) {
  if (!text) return [];
  var out = [];
  text.split('\n').forEach(function(line) {
    if (line.indexOf('|') !== 0) return;
    var cells = line.split('|').filter(function(c) { return c.trim(); });
    if (cells.length < 2) return;
    var relType = cells[0].trim().toLowerCase();
    if (relType !== type && relType !== type.replace('_', ' ')) return;
    var m = cells[1].match(/\[([^\]]+)\]\(([^)]+)\)/);
    if (m) out.push({ title: m[1], concept_id: m[2].replace(/\.md$/, '') });
  });
  return out;
}

function _parseBullets(text) {
  if (!text) return [];
  if (Array.isArray(text)) return text;
  return text.split('\n').filter(function(l) { return l.trim().indexOf('- ') === 0; }).map(function(l) { return l.trim().slice(2).replace(/`+/g, '').trim(); });
}

function renderDetail(c) {
  var color = TYPE_COLORS[c.type] || '#888';
  var sections = c.sections || {};

  // Normalise concept fields — handle both static (sections as strings) and dynamic (pre-parsed arrays)
  var d = {
    type: c.type || '',
    title: c.title || '',
    description: c.description || '',
    resource: c.resource || '',
    id: c.concept_id || c.id || '',
    signature: c.signature || sections.signature || '',
    docstring: c.docstring || sections.docstring || '',
    parameters: c.parameters || sections.parameters || '',
    returns: c.returns || sections.returns || '',
    source: c.source || c.raw || c.code || c.body || '',
    related: (function(){ var r = _parseRel(c.related || sections.related); return r.length ? r : _parseRelationships(sections.relationships, 'related'); })(),
    calls: (function(){ var r = _parseRel(c.calls || sections.calls); return r.length ? r : _parseRelationships(sections.relationships, 'calls'); })(),
    called_by: (function(){ var r = _parseRel(c.called_by || sections['called by']); return r.length ? r : _parseRelationships(sections.relationships, 'called_by'); })(),
    used_by: _parseRel(c.used_by || sections['used by']),
    tags: c.tags || [],
    visibility: c.visibility || _parseBullets(sections.visibility),
    decorators: c.decorators || _parseBullets(sections.decorators),
    inheritance: c.inheritance || _parseBullets(sections.inheritance),
    type_params: c.type_params || _parseBullets(sections.type_parameters || sections['type parameters']),
    fields: c.fields || [],
    deptable: c.deptable || {},
    body: c.body || '',
    sections: sections,
  };

  var visDec = [];
  if (d.visibility && d.visibility.length) visDec.push('<span class="meta-tag meta-tag-vis"><span class="label">visibility:</span> ' + esc(d.visibility.join(', ')) + '</span>');
  if (d.decorators && d.decorators.length) visDec.push('<span class="meta-tag meta-tag-vis"><span class="label">decorators:</span> ' + esc(d.decorators.join(', ')) + '</span>');

  var top = '<div id="detail-topbar"><div id="detail-topbar-left">';
  top += '<span class="type-badge" style="background:' + color + '18;color:' + color + '"><span class="dot" style="background:' + color + '"></span>' + esc(d.type) + '</span>';
  top += '</div><div id="detail-topbar-right">';
  if (visDec.length) top += '<div style="display:flex;flex-wrap:wrap;gap:4px">' + visDec.join('') + '</div>';
  top += '</div></div>';

  top += '<div id="detail-topbar"><div id="detail-topbar-left">';
  top += '<h2>' + esc(d.title) + '</h2>';
  top += '</div></div>';

  top += '<div id="detail-topbar"><div id="detail-topbar-left">';
  if (d.description) top += '<p class="desc" style="margin:0">' + esc(d.description) + '</p>';
  top += '</div><div id="detail-topbar-right">';
  if (d.resource) top += '<span class="resource">' + esc(d.resource) + '</span>';
  top += '</div></div>';

  var md = [];
  if (d.inheritance && d.inheritance.length) md.push({l:'inherits', v:d.inheritance.join(', ')});
  if (d.type_params && d.type_params.length) md.push({l:'generics', v:d.type_params.join(', ')});
  if (md.length) {
    top += '<div id="detail-meta">' + md.map(function(m) { return '<span class="meta-tag' + (m.c||'') + '"><span class="label">' + m.l + ':</span> ' + esc(m.v) + '</span>'; }).join('') + '</div>';
  }
  qs('detail-header').innerHTML = top;

  var left = '';
  if (Object.keys(d.deptable).length) {
    left += '<div class="sec"><table style="width:100%;font-size:12.5px;border-collapse:collapse">';
    for (var k in d.deptable) {
      if (d.deptable.hasOwnProperty(k))
        left += '<tr><td style="padding:6px 10px;color:var(--text-3);border-bottom:1px solid var(--border);font-weight:500">' + esc(k) + '</td><td style="padding:6px 10px;border-bottom:1px solid var(--border)">' + esc(d.deptable[k]) + '</td></tr>';
    }
    left += '</table></div>';
  }

  var mid = [];
  if (d.signature) {
    var sigText = d.signature.replace(/```\w*\n?/g, '').trim();
    var sigLang = (d.signature.match(/```(\w+)/) || [])[1] || '';
    mid.push({id:'sig', label:'Signature', h: '<pre><code class="language-' + sigLang + '">' + esc(sigText) + '</code></pre>'});
  }
  if (d.parameters) {
    var lines = d.parameters.split('\n');
    var ph = '<table style="width:100%;border-collapse:collapse;font-size:12.5px">';
    ph += '<tr style="border-bottom:1px solid var(--border)"><th style="padding:6px 8px;text-align:left;color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase">Name</th><th style="padding:6px 8px;text-align:left;color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase">Type</th><th style="padding:6px 8px;text-align:left;color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase">Description</th></tr>';
    var inHeader = true;
    lines.forEach(function(l) {
      if (l.trim().indexOf('|') === 0) {
        var cells = l.split('|').filter(function(_,i,a) { return i > 0 && i < a.length-1; }).map(function(c) { return c.trim().replace(/^`|`$/g,''); });
        if (inHeader && cells[0] === 'Name') { inHeader = false; return; }
        if (inHeader && /^[:\-]+$/.test(cells[0])) { inHeader = false; return; }
        if (cells.length >= 2 && !inHeader) {
          ph += '<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 8px;font-weight:500;color:var(--accent-strong)">' + esc(cells[0]) + '</td><td style="padding:6px 8px;color:var(--text-2)">' + esc(cells[1] || '') + '</td><td style="padding:6px 8px;color:var(--text-3)">' + esc(cells[2] || '') + '</td></tr>';
        }
      }
    });
    ph += '</table>';
    mid.push({id:'params', label:'Parameters', h: ph});
  }
  if (d.docstring) mid.push({id:'doc', label:'Docstring', h: '<pre><code class="language-md">' + esc(d.docstring.slice(0,600)) + '</code></pre>'});
  if (d.returns) mid.push({id:'ret', label:'Returns', h: '<div class="returns-val">' + esc(d.returns) + '</div>'});
  if (d.fields && d.fields.length) {
    var fh = '';
    d.fields.forEach(function(f) {
      fh += '<div class="param-row"><span class="param-name">' + esc(f.name || '') + '</span>';
      if (f.type) fh += '<span class="param-type">: ' + esc(f.type) + '</span>';
      if (f.visibility) fh += '<span class="param-default"> (' + esc(f.visibility) + ')</span>';
      fh += '</div>';
    });
    mid.push({id:'fields', label:'Fields', h: fh});
  }
  if (mid.length) {
    left += '<div id="detail-mid-grid">';
    mid.slice(0,4).forEach(function(s) {
      left += '<div class="sec"><h3>' + s.label + '</h3>' + s.h + '</div>';
    });
    left += '</div>';
  }

  if (d.body) {
    var bp = d.body.split('---');
    var mb = bp.length >= 3 ? bp.slice(2).join('---').trim() : d.body;
    var skipHeaders = ['Signature','Docstring','Parameters','Returns','Methods','Source','Related','Calls','Called By','Used By','Fields','Visibility','Decorators','Type Parameters','Inheritance','Type_parameters'];
    var hr = new RegExp('^##\\s+(' + skipHeaders.join('|') + ')\\s*$', 'i');
    var ln = mb.split('\n');
    var kp = [];
    var sk = false;
    ln.forEach(function(l) {
      if (/^##\s+/.test(l)) { sk = hr.test(l); if (sk) return; }
      if (!sk) kp.push(l);
    });
    mb = kp.join('\n').replace(/^#\s+.*\n/, '').trim();
    var fp = (mb.split(/\n\s*\n/)[0] || '').trim();
    if (d.description && fp.replace(/\s+/g,' ') === d.description.trim().replace(/\s+/g,' ')) mb = mb.slice(fp.length).trim();
    if (mb) {
      try { left += '<div class="sec"><h3>Notes</h3><div class="markdown-body sec-inline">' + marked.parse(mb) + '</div></div>'; }
      catch (e) { left += '<div class="sec"><h3>Notes</h3><pre>' + esc(mb.slice(0,600)) + '</pre></div>'; }
    }
  }
  if (!left) left = '<p class="empty-note" style="color:var(--text-3);font-size:13px">No additional info.</p>';
  qs('detail-left').innerHTML = left;

  var right = '';
  var anyRel = false;
  [['related','Related'],['calls','Calls'],['called_by','Called By'],['used_by','Used By']].forEach(function(pair) {
    var key = pair[0], label = pair[1];
    var items = d[key];
    if (!items || !items.length) return;
    anyRel = true;
    right += '<div class="sec"><h3>' + label + '</h3><div class="chip-list" style="padding:var(--sp-3)">';
    items.forEach(function(r) {
      var cid = r.concept_id || (typeof r === 'string' ? r : null);
      var rtitle = r.title || cid || '';
      if (!cid) return;
      var tc = TYPE_COLORS[r.type || ''] || '#888';
      right += '<span class="chip" onclick="selectConcept(\'' + cid.replace(/'/g,"\\'") + '\')"><span class="dot" style="background:' + tc + '"></span>' + esc(rtitle) + '</span>';
    });
    right += '</div></div>';
  });
  if (!anyRel) right += '<div class="sec"><div class="sec-inline empty-note">No related concepts, calls, or dependents.</div></div>';
  right += '<div class="sec"><h3>Local Graph</h3><div id="ego-graph"></div><div id="ego-hint" style="font-size:11px;color:var(--text-3);padding:0 var(--sp-3) var(--sp-3)">Click a neighbor to jump to it.</div></div>';
  qs('detail-right').innerHTML = right;
  renderEgoGraph(d.id, d);
}

// ---- Code Panel ----
// ---- Code Tab Split ----
var _parseTreeLoaded = {};
function showParseTreePrompt(id) {
  if (_parseTreeLoaded[id]) return;
  var container = qs('code-split-parse');
  container.innerHTML = '<div class="loading"><div class="spinner"></div> Parsing…</div>';
  renderParseTree(id);
}

function switchDetailTab(name, btn) {
  document.querySelectorAll('.detail-tab').forEach(function(t) { t.classList.remove('active'); });
  if (btn) btn.classList.add('active');
  document.querySelectorAll('.detail-tab-content').forEach(function(t) { t.classList.remove('active'); });
  qs('detail-tab-' + name).classList.add('active');
}

// ---- Tree-sitter WASM Parser ----
var _tsInitialized = false;
var _tsParsers = {};

// Only languages that publish WASM files on npm + CDN
var _tsLangMap = {
  py:'python', js:'javascript', ts:'typescript', rb:'ruby', rs:'rust',
  go:'go', java:'java', cpp:'cpp', c:'c', php:'php',
  // kt: kotlin, cs: csharp, scala, dart, jl: julia, sql — no WASM builds on npm
};
var _tsWasmUrls = {
  python:'https://cdn.jsdelivr.net/npm/tree-sitter-python@0.20.4/tree-sitter-python.wasm',
  javascript:'https://cdn.jsdelivr.net/npm/tree-sitter-javascript@0.20.4/tree-sitter-javascript.wasm',
  typescript:'https://cdn.jsdelivr.net/npm/tree-sitter-typescript@0.20.5/tree-sitter-typescript.wasm',
  ruby:'https://cdn.jsdelivr.net/npm/tree-sitter-ruby@0.20.1/tree-sitter-ruby.wasm',
  rust:'https://cdn.jsdelivr.net/npm/tree-sitter-rust@0.20.4/tree-sitter-rust.wasm',
  go:'https://cdn.jsdelivr.net/npm/tree-sitter-go@0.20.0/tree-sitter-go.wasm',
  java:'https://cdn.jsdelivr.net/npm/tree-sitter-java@0.20.2/tree-sitter-java.wasm',
  cpp:'https://cdn.jsdelivr.net/npm/tree-sitter-cpp@0.20.5/tree-sitter-cpp.wasm',
  c:'https://cdn.jsdelivr.net/npm/tree-sitter-c@0.20.8/tree-sitter-c.wasm',
  php:'https://cdn.jsdelivr.net/npm/tree-sitter-php@0.20.0/tree-sitter-php.wasm',
};

async function getTsParser(langName) {
  if (_tsParsers[langName]) return _tsParsers[langName];
  if (!_tsInitialized) {
    await TreeSitter.init({
      locateFile: function(p) { return 'https://cdn.jsdelivr.net/npm/web-tree-sitter@0.20.8/' + p; }
    });
    _tsInitialized = true;
  }
  var url = _tsWasmUrls[langName];
  if (!url) return null;
  try {
    var lang = await TreeSitter.Language.load(url);
    var parser = new TreeSitter();
    parser.setLanguage(lang);
    _tsParsers[langName] = parser;
    return parser;
  } catch(e) {
    console.warn('Failed to load WASM parser for ' + langName + ': ' + e.message);
    return null;
  }
}

function _tsNodeName(n) {
  return n.type;
}

function _tsNodeText(n, src) {
  try { return src.slice(n.startIndex, n.endIndex); } catch(e) { return ''; }
}

function _renderTsTree(node, src, depth, isLast) {
  var nn = _tsNodeName(node);
  var txt = _tsNodeText(node, src).replace(/\s+/g, ' ').trim();
  if (txt.length > 60) txt = txt.slice(0, 60) + '…';
  var prefix = '';
  for (var i = 0; i < depth; i++) prefix += '  ';
  var hasChildren = node.childCount > 0;
  var line = '';
  if (hasChildren) {
    line += '<div class="parse-node' + (depth === 0 ? ' root' : '') + '" data-depth="' + depth + '">';
    line += '<span class="node-toggle" onclick="toggleParseNode(this)">▶</span>';
    line += '<span class="node-type">' + nn + '</span>';
    if (txt && nn !== 'program' && nn !== 'module' && nn !== 'block' && nn !== 'statement_block' && nn !== 'body' && nn !== 'declaration') {
      line += ' <span class="node-text">' + esc(txt) + '</span>';
    }
    line += '<span class="parse-range">[' + node.startIndex + '-' + node.endIndex + ']</span>';
    line += '<div class="parse-children">';
    for (var j = 0; j < node.childCount; j++) {
      line += _renderTsTree(node.child(j), src, depth + 1, j === node.childCount - 1);
    }
    line += '</div></div>';
  } else {
    var cls = 'node-text';
    if (/^['"]/.test(txt)) cls += ' str';
    else if (/^\d/.test(txt)) cls += ' num';
    else if (/^(return|if|else|for|while|def|class|import|from|let|const|var|function|fn|pub|use|mod|struct|enum|trait|impl|interface|package|val|var|fun)$/.test(txt)) cls += ' kw';
    else if (/^[A-Z]/.test(txt) && txt.length < 40) cls += ' id';
    line += '<div class="parse-node" data-depth="' + depth + '">';
    line += '<span class="node-type">' + nn + '</span>';
    if (txt) line += ' <span class="' + cls + '">' + esc(txt) + '</span>';
    line += '<span class="parse-range">[' + node.startIndex + '-' + node.endIndex + ']</span>';
    line += '</div>';
  }
  return line;
}

function toggleParseNode(el) {
  var node = el.closest('.parse-node');
  if (node) node.classList.toggle('collapsed');
}

async function renderParseTree(id) {
  var container = qs('code-split-parse');
  if (!id) return;
  if (_parseTreeLoaded[id]) return;

  var c = conceptCache[id] || __nodeById[id];
  if (!c) { container.innerHTML = '<div class="empty-note">No concept data.</div>'; return; }

  var src = c.code || c.body || '';
  if (!src && c.source) {
    if (/^Lines \d/.test(c.source)) {
      container.innerHTML = '<div class="empty-note">Source code not embedded in this view. Use <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px">okf dashboard</code> for source lookup.</div>';
      return;
    }
    src = c.source;
  }
  if (!src) { container.innerHTML = '<div class="empty-note">No source code available.</div>'; return; }

  var ext = (c.resource || '').split('.').pop();
  var langName = _tsLangMap[ext];
  if (!langName || !_tsWasmUrls[langName]) {
    container.innerHTML = '<div class="empty-note">Parse tree is not available for <strong>.' + esc(ext) + '</strong> files. Use the <strong>Source</strong> tab for code view.</div>';
    return;
  }

  if (src.length > 5000) {
    container.innerHTML = '<div class="empty-note">Code too long for parse tree (' + src.length + ' chars). Switch to <strong>Source</strong> tab for code view.</div>';
    return;
  }
  container.innerHTML = '<div class="loading">Parsing…</div>';
  var parser = await getTsParser(langName);
  if (!parser) {
    container.innerHTML = '<div class="empty-note">Parse tree could not be loaded for <strong>' + esc(langName) + '</strong>. Switch to <strong>Source</strong> tab for code view.</div>';
    return;
  }

  try {
    var tree = parser.parse(src);
    var root = tree.rootNode;

    // Named-only tree for readability
    function namedChildren(n) {
      var kids = [];
      for (var i = 0; i < n.childCount; i++) {
        var ch = n.child(i);
        if (ch.isNamed) kids.push(ch);
      }
      return kids;
    }

    function renderNamed(node, depth) {
      var nn = node.type;
      var txt = _tsNodeText(node, src).replace(/\s+/g, ' ').trim();
      if (txt.length > 60) txt = txt.slice(0, 60) + '…';
      var kids = namedChildren(node);
      var line = '<div class="parse-node' + (depth === 0 ? ' root' : '') + '">';
      if (kids.length > 0) {
        line += '<span class="node-toggle" onclick="toggleParseNode(this)">▶</span>';
      } else {
        line += '<span class="node-toggle" style="visibility:hidden">▶</span>';
      }
      line += '<span class="node-type">' + nn + '</span>';
      if (txt && kids.length === 0) {
        var cls = 'node-text';
        if (/^['"]/.test(txt)) cls += ' str';
        else if (/^\d/.test(txt)) cls += ' num';
        else if (/^(return|if|else|for|while|def|class|import|from|let|const|var|function|fn|pub|use|mod|struct|enum|trait|impl|interface|package|val|var|fun)$/.test(txt)) cls += ' kw';
        else if (nn === 'identifier' || nn === 'type_identifier') cls += ' id';
        line += ' <span class="' + cls + '">' + esc(txt) + '</span>';
      }
      line += '<span class="parse-range">[' + node.startIndex + '-' + node.endIndex + ']</span>';
      if (kids.length > 0) {
        line += '<div class="parse-children">';
        kids.forEach(function(k) { line += renderNamed(k, depth + 1); });
        line += '</div>';
      }
      line += '</div>';
      return line;
    }

    container.innerHTML = renderNamed(root, 0) + '<div style="margin-top:var(--sp-3);text-align:center"><button onclick="delete _parseTreeLoaded[\'' + id.replace(/'/g,"\\'") + '\'];showParseTreePrompt(\'' + id.replace(/'/g,"\\'") + '\')" style="font-size:11px;padding:4px 12px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border);border-radius:var(--r-sm);cursor:pointer">Re-parse</button></div>';
  } catch(e) {
    container.innerHTML = '<div class="empty-note">Parse error: ' + esc(e.message) + '</div>';
  }
}



// ---- Ego Graph (Cytoscape) ----
function renderEgoGraph(centerId, concept) {
  var result = getEgoElements(centerId, concept);
  var elements = result.elements;
  if (!elements || (Array.isArray(elements) && elements.length === 0) || (elements.nodes && elements.nodes.length < 2)) {
    var cont = qs('ego-graph');
    if (cont) cont.innerHTML = '<div class="loading">No connections</div>';
    return;
  }
  if (egoCy) egoCy.destroy();
  var container = qs('ego-graph');
  if (!container) return;
  container.innerHTML = '';
  var tv = getComputedStyle(document.documentElement);
  var textColor = tv.getPropertyValue('--text').trim();
  var borderColor = tv.getPropertyValue('--border-strong').trim();
  var surf2 = tv.getPropertyValue('--surface-2').trim();

  egoCy = cytoscape({
    container: container,
    elements: elements,
    style: [
      { selector: 'node', style: {
        'label': 'data(label)', 'color': textColor, 'font-size':'10px',
        'text-valign':'bottom', 'text-margin-y':'6px',
        'width': 22, 'height': 22, 'border-width':2, 'border-color': surf2,
        'background-color': function(ele) { return TYPE_COLORS[ele.data('type')] || '#888'; },
      } },
      { selector: 'node[id = "' + centerId + '"]', style: { 'width':32, 'height':32, 'border-width':3, 'border-color': textColor } },
      { selector: 'edge', style: { 'width':1.5, 'line-color': borderColor, 'target-arrow-color': borderColor, 'target-arrow-shape':'triangle', 'curve-style':'bezier', 'arrow-scale':0.6, 'opacity':.7 } },
    ],
    layout: { name: 'concentric', concentric: function(n) { return n.id() === centerId ? 2 : 1; }, levelWidth: function() { return 1; }, minNodeSpacing: 44 },
    userZoomingEnabled: true,
    userPanningEnabled: true,
  });
  egoCy.nodes().forEach(function(n) { n.style('cursor', 'pointer'); });
  egoCy.on('tap', 'node', function(evt) {
    var id = evt.target.id();
    if (id !== centerId) selectConcept(id);
  });
}

// ---- Cosmos Explore Tab (pure Canvas 2D) ----
var _canvasVars = null;
var _cosmosFilterDirty = 0;
function _cosmosFilterChanged() { _cosmosFilterDirty++; }
var _cosmosLoaded = false;
var _cosmosLoading = false;
var _cosmosClickedIdx = null;
var _cosmosHoverIdx = null;
var _cosmosPtrX = 0, _cosmosPtrY = 0;
var _cosmosActiveTypes = null;
var _cosmosActiveBundles = null;
var _cosmosMatchSet = null;
var _cosmosFilterDirty = 0;
var _cosmosNodes = [];
var _cosmosEdges = [];
var _cosmosDomainNames = [];
var _cosmosN = 0;
var _cosmosDomIdx = {};
var _cosmosPointColors = null;
var _edgeRelType = [];
var REL_VERB_MAP = {"calls":"calls","called_by":"called by","imports":"imports","related":"relates to"};

function _cosmosHexToRgb(h) {
  var x = h.replace('#','');
  return [parseInt(x.slice(0,2),16)/255, parseInt(x.slice(2,4),16)/255, parseInt(x.slice(4,6),16)/255];
}

function _cosmosInitData() {
  var nodes = (BUNDLE_DATA && BUNDLE_DATA.nodes) || [];
  var links = (BUNDLE_DATA && BUNDLE_DATA.links) || [];
  _cosmosNodes = nodes;
  _cosmosEdges = links;
  _cosmosN = nodes.length;

  var doms = {};
  nodes.forEach(function(n) {
    var parts = (n.id || n.concept_id || '').split('/');
    var d = parts[0] || 'root';
    if (!doms[d]) doms[d] = [];
    doms[d].push(n);
  });
  _cosmosDomainNames = Object.keys(doms).sort();
  _cosmosDomainNames.forEach(function(d, i) {
    (doms[d] || []).forEach(function(n) {
      _cosmosDomIdx[n.id || n.concept_id] = i;
    });
  });

  var deg = new Array(_cosmosN).fill(0);
  links.forEach(function(l) {
    var si = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.source; });
    var ti = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.target; });
    if (si >= 0) deg[si]++;
    if (ti >= 0) deg[ti]++;
  });
  _edgeRelType = new Array(links.length);
  links.forEach(function(l, i) {
    _edgeRelType[i] = REL_VERB_MAP[l.type] || l.type || '';
  });
  return deg;
}

// ---- Pure Canvas 2D Graph Engine ----
function _canvas2DInit() {
  if (_canvasVars && _canvasVars.raf) cancelAnimationFrame(_canvasVars.raf);
  var canvas = qs('cosmos-canvas');
  if (!canvas) return;
  var stage = qs('cosmos-stage');
  var rect = stage.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) {
    setTimeout(_canvas2DInit, 100);
    return;
  }
  var dpr = window.devicePixelRatio || 1;
  var ctx = canvas.getContext('2d');
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  var nodes = _cosmosNodes || [];
  var edges = _cosmosEdges || [];
  var deg = new Array(nodes.length).fill(0);
  edges.forEach(function(l) {
    var si = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.source; });
    var ti = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.target; });
    if (si >= 0) deg[si]++; if (ti >= 0) deg[ti]++;
  });
  var ps = nodes.map(function(n, i) {
    var a = (i / nodes.length) * Math.PI * 2;
    return { x: Math.cos(a) * 280, y: Math.sin(a) * 280, vx: 0, vy: 0, r: 8 + Math.sqrt(deg[i] || 0) * 2.8 };
  });
  var view = { x: rect.width / 2, y: rect.height / 2, k: 1 };
  var alpha = 1;
  var _W = rect.width, _H = rect.height;
  function resize() {
    var r = stage.getBoundingClientRect();
    _W = r.width; _H = r.height;
    canvas.width = _W * dpr; canvas.height = _H * dpr;
    canvas.style.width = _W + 'px'; canvas.style.height = _H + 'px';
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  function tick() {
    if (alpha < 0.005) { alpha = 0; } else { alpha *= 0.985; }
    var k = alpha;
    for (var i = 0; i < ps.length; i++) {
      for (var j = i + 1; j < ps.length; j++) {
        var dx = ps[i].x - ps[j].x, dy = ps[i].y - ps[j].y;
        var d2 = dx * dx + dy * dy || 1;
        var f = 2800 / d2; var d = Math.sqrt(d2); dx /= d; dy /= d;
        ps[i].vx += dx * f * k; ps[i].vy += dy * f * k;
        ps[j].vx -= dx * f * k; ps[j].vy -= dy * f * k;
      }
    }
    for (var ei = 0; ei < edges.length; ei++) {
      var l = edges[ei];
      var si = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.source; });
      var ti = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.target; });
      if (si < 0 || ti < 0) continue;
      var dx = ps[ti].x - ps[si].x, dy = ps[ti].y - ps[si].y;
      var d = Math.sqrt(dx * dx + dy * dy) || 1;
      var f = (d - 90) * 0.04 * k; dx /= d; dy /= d;
      ps[si].vx += dx * f; ps[si].vy += dy * f;
      ps[ti].vx -= dx * f; ps[ti].vy -= dy * f;
    }
    for (var ni = 0; ni < ps.length; ni++) {
      ps[ni].vx += (-ps[ni].x) * 0.002 * k;
      ps[ni].vy += (-ps[ni].y) * 0.002 * k;
      ps[ni].vx *= 0.86; ps[ni].vy *= 0.86;
      ps[ni].x += ps[ni].vx; ps[ni].y += ps[ni].vy;
    }
  }
  function toWorld(px, py) { return { x: (px - view.x) / view.k, y: (py - view.y) / view.k }; }
  function pick(px, py) {
    var w = toWorld(px, py); var best = -1, bd = 1e9;
    for (var i = 0; i < ps.length; i++) {
      var dx = ps[i].x - w.x, dy = ps[i].y - w.y, d = dx * dx + dy * dy;
      var r = ps[i].r + 4;
      if (d < r * r && d < bd) { bd = d; best = i; }
    }
    return best;
  }
  function _nodeBundle(n) { var p = (n.id || n.concept_id || '').split('/'); return p[0] || 'root'; }
  function _nodeVisible(ni) {
    var n = nodes[ni];
    if (!n) return false;
    if (_cosmosActiveTypes && !_cosmosActiveTypes.has(n.type)) return false;
    if (_cosmosActiveBundles && !_cosmosActiveBundles.has(_nodeBundle(n))) return false;
    if (_cosmosMatchSet !== null && !_cosmosMatchSet.has(ni)) return false;
    return true;
  }
  function draw() {
    ctx.clearRect(0, 0, _W, _H);
    ctx.save(); ctx.translate(view.x, view.y); ctx.scale(view.k, view.k);
    var focusIdx = _cosmosClickedIdx >= 0 ? _cosmosClickedIdx : (_cosmosHoverIdx >= 0 ? _cosmosHoverIdx : -1);
    var neighSet = new Set();
    if (focusIdx >= 0) {
      neighSet.add(focusIdx);
      edges.forEach(function(l) {
        var si = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.source; });
        var ti = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.target; });
        if (si === focusIdx) neighSet.add(ti);
        if (ti === focusIdx) neighSet.add(si);
      });
    }
    var hiEdges = [];
    var cs = getComputedStyle(document.documentElement);
    var textColor = cs.getPropertyValue('--text').trim() || '#f2f2f5';
    var textMuted = cs.getPropertyValue('--text-2').trim() || '#8b949e';
    var accentColor = cs.getPropertyValue('--accent').trim() || '#7c6cff';
    var bgColor = cs.getPropertyValue('--bg').trim() || '#0a0a0f';
    for (var ei = 0; ei < edges.length; ei++) {
      var l = edges[ei];
      var si = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.source; });
      var ti = nodes.findIndex(function(n) { return (n.id || n.concept_id) === l.target; });
      if (si < 0 || ti < 0) continue;
      var isHi = focusIdx >= 0 && (si === focusIdx || ti === focusIdx);
      var edgeOk = _nodeVisible(si) && _nodeVisible(ti);
      if (focusIdx >= 0) {
        if (!edgeOk && !isHi) continue;
        if (isHi) hiEdges.push({ ei: ei, si: si, ti: ti, s: ps[si], t: ps[ti], l: l });
      } else if (!edgeOk) {
        ctx.globalAlpha = 0.08; ctx.beginPath(); ctx.moveTo(ps[si].x, ps[si].y); ctx.lineTo(ps[ti].x, ps[ti].y);
        ctx.strokeStyle = textMuted; ctx.lineWidth = 0.6 / view.k; ctx.stroke(); ctx.globalAlpha = 1;
        continue;
      }
      ctx.beginPath(); ctx.moveTo(ps[si].x, ps[si].y); ctx.lineTo(ps[ti].x, ps[ti].y);
      ctx.globalAlpha = isHi ? 1 : (focusIdx >= 0 ? 0.12 : 1);
      ctx.strokeStyle = isHi ? textMuted : textMuted;
      ctx.lineWidth = isHi ? 2 / view.k : 0.8 / view.k;
      ctx.stroke(); ctx.globalAlpha = 1;
    }
    for (var hi = 0; hi < hiEdges.length; hi++) {
      var h = hiEdges[hi];
      var dx = h.t.x - h.s.x, dy = h.t.y - h.s.y;
      var d = Math.sqrt(dx * dx + dy * dy) || 1;
      var ang = Math.atan2(dy, dx);
      var aSize = 10 / view.k;
      ctx.beginPath(); ctx.moveTo(h.t.x, h.t.y);
      ctx.lineTo(h.t.x - Math.cos(ang - 0.45) * aSize, h.t.y - Math.sin(ang - 0.45) * aSize);
      ctx.lineTo(h.t.x - Math.cos(ang + 0.45) * aSize, h.t.y - Math.sin(ang + 0.45) * aSize);
      ctx.closePath();
      ctx.fillStyle = accentColor; ctx.globalAlpha = 0.85; ctx.fill(); ctx.globalAlpha = 1;
      var rel = _edgeRelType[h.ei];
      if (rel) {
        var mx = (h.s.x + h.t.x) / 2, my = (h.s.y + h.t.y) / 2;
        var fontSize = 11 / view.k;
        ctx.font = '600 ' + fontSize + 'px Inter,-apple-system,sans-serif';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        var tw = ctx.measureText(rel).width;
        var pad = 7 / view.k, rh = fontSize + 6 / view.k, rw = tw + pad * 2;
        var rx = mx - rw / 2, ry = my - rh / 2;
        ctx.fillStyle = bgColor; ctx.globalAlpha = 0.82;
        var rr = 5 / view.k;
        ctx.beginPath(); ctx.moveTo(rx + rr, ry); ctx.lineTo(rx + rw - rr, ry);
        ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, rr); ctx.lineTo(rx + rw, ry + rh);
        ctx.arcTo(rx + rw, ry + rh, rx + rw - rr, ry + rh, rr);
        ctx.lineTo(rx + rr, ry + rh); ctx.arcTo(rx, ry + rh, rx, ry + rh - rr, rr);
        ctx.lineTo(rx, ry + rr); ctx.arcTo(rx, ry, rx + rr, ry, rr);
        ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
        ctx.fillStyle = textColor; ctx.fillText(rel, mx, my);
      }
    }
    for (var ni = 0; ni < ps.length; ni++) {
      var n = nodes[ni]; var p = ps[ni];
      if (!n) continue;
      var col = TYPE_COLORS[n.type] || '#888';
      var vis = _nodeVisible(ni);
      if (focusIdx >= 0) {
        if (!vis && !neighSet.has(ni)) continue;
        var dim = !neighSet.has(ni);
        ctx.globalAlpha = dim ? 0.2 : 1;
      } else {
        ctx.globalAlpha = vis ? 1 : 0.15;
      }
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 7);
      ctx.fillStyle = col; ctx.fill();
      if (ni === focusIdx) { ctx.lineWidth = 2 / view.k; ctx.strokeStyle = textColor; ctx.stroke(); }
      ctx.globalAlpha = 1;
      if (ni === focusIdx || deg[ni] >= 6 || (focusIdx < 0 && view.k > 1.2)) {
        ctx.font = (10 / view.k) + 'px Inter,-apple-system,sans-serif';
        ctx.textAlign = 'center'; ctx.fillStyle = textColor;
        ctx.fillText(n.title || '', p.x, p.y + p.r + 12 / view.k);
      }
    }
    ctx.restore();
  }
  function loop() {
    try { tick(); draw(); _canvas2DUpdateClusterLabels(ps, view); }
    catch(e) { console.error('Canvas render error:', e); if(e.stack) console.error(e.stack); }
    _canvasVars.raf = requestAnimationFrame(loop);
  }
  var dragNode = -1, dragMoved = false, panning = false, lastX = 0, lastY = 0;
  canvas.onmousedown = function(e) {
    var i = pick(e.offsetX, e.offsetY);
    if (i >= 0) { dragNode = i; dragMoved = false; alpha = Math.max(alpha, 0.3); }
    else { panning = true; canvas.style.cursor = 'grabbing'; }
    lastX = e.offsetX; lastY = e.offsetY;
  };
  canvas.onmousemove = function(e) {
    _cosmosPtrX = e.offsetX; _cosmosPtrY = e.offsetY;
    if (dragNode >= 0) {
      var w = toWorld(e.offsetX, e.offsetY);
      ps[dragNode].x = w.x; ps[dragNode].y = w.y;
      ps[dragNode].vx = 0; ps[dragNode].vy = 0;
      dragMoved = true; alpha = Math.max(alpha, 0.2);
    } else if (panning) {
      view.x += e.offsetX - lastX; view.y += e.offsetY - lastY;
      lastX = e.offsetX; lastY = e.offsetY;
    } else {
      var i = pick(e.offsetX, e.offsetY);
      if (i !== _cosmosHoverIdx) {
        _cosmosHoverIdx = i;
        if (i >= 0) { _cosmosShowHoverTag(i); } else { _cosmosHideHoverTag(); }
      } else if (_cosmosHoverIdx >= 0) {
        _cosmosPosHoverTag();
      }
      canvas.style.cursor = i >= 0 ? 'pointer' : 'grab';
    }
  };
  canvas.onmouseup = function(e) {
    if (dragNode >= 0 && !dragMoved) {
      var n = nodes[dragNode];
      if (n) {
        if (_cosmosClickedIdx === dragNode) { graphSidebarClose(); }
        else { _cosmosClickedIdx = dragNode; graphSidebarOpen(dragNode); }
      }
    }
    dragNode = -1; panning = false;
    canvas.style.cursor = 'grab';
  };
  canvas.onwheel = function(e) {
    e.preventDefault();
    var s = Math.exp(-e.deltaY * 0.0012);
    view.x = e.offsetX - (e.offsetX - view.x) * s;
    view.y = e.offsetY - (e.offsetY - view.y) * s;
    view.k = Math.max(0.15, Math.min(4, view.k * s));
  };
  _canvasVars = { raf: null, running: true, view: view, nodes: nodes, W: _W, H: _H };
  new ResizeObserver(function() {
    var r = stage.getBoundingClientRect();
    if (r.width > 0 && r.height > 0 && (r.width !== _W || r.height !== _H)) {
      _W = r.width; _H = r.height;
      canvas.width = _W * dpr; canvas.height = _H * dpr;
      canvas.style.width = _W + 'px'; canvas.style.height = _H + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
  }).observe(stage);
  _canvasVars.raf = requestAnimationFrame(loop);
}
function _canvas2DStop() {
  if (_canvasVars && _canvasVars.raf) {
    cancelAnimationFrame(_canvasVars.raf);
    _canvasVars = null;
  }
}
function _canvas2DSwitch() {
  _canvas2DStop();
  var canvas = qs('cosmos-canvas');
  canvas.style.display = 'block';
  canvas.style.pointerEvents = 'auto';
  _canvas2DInit();
  qs('status-n').textContent = _cosmosN;
  qs('status-of').textContent = BUNDLE_DATA && BUNDLE_DATA.total_concepts > _cosmosN ? ' of ' + BUNDLE_DATA.total_concepts : '';
  qs('status-e').textContent = _cosmosEdges.length;
}
function _canvas2DUpdateClusterLabels(ps, view) {
  if (!_cosmosDomainNames || !_cosmosDomainNames.length) return;
  var els = qs('cluster-labels').children;
  if (!els || !els.length) return;
  for (var di = 0; di < _cosmosDomainNames.length && di < els.length; di++) {
    var dname = _cosmosDomainNames[di];
    var cx = 0, cy = 0, count = 0;
    for (var ni = 0; ni < ps.length; ni++) {
      var n = _cosmosNodes[ni];
      if (!n) continue;
      var parts = (n.id || n.concept_id || '').split('/');
      if (parts[0] === dname || (parts.length > 1 && parts[0] === dname)) {
        var nVisible = true;
        if (_cosmosActiveTypes && !_cosmosActiveTypes.has(n.type)) nVisible = false;
        if (_cosmosActiveBundles) {
          var nb = (n.id || n.concept_id || '').split('/')[0] || 'root';
          if (!_cosmosActiveBundles.has(nb)) nVisible = false;
        }
        if (_cosmosMatchSet !== null && !_cosmosMatchSet.has(ni)) nVisible = false;
        if (!nVisible) continue;
        cx += ps[ni].x; cy += ps[ni].y; count++;
      }
    }
    if (count === 0) { els[di].style.display = 'none'; continue; }
    els[di].style.display = '';
    cx /= count; cy /= count;
    var sx = cx * view.k + view.x;
    var sy = cy * view.k + view.y;
    els[di].style.left = sx + 'px';
    els[di].style.top = (sy - 40) + 'px';
  }
}

// ---- Graph Sidebar ----
var _gsIdx = null;
async function graphSidebarOpen(idx) {
  _gsIdx = idx;
  var n = _cosmosNodes[idx];
  if (!n) return;
  // In dynamic mode, fetch full concept details from API
  if (DYNAMIC && (!n.description || !n.sections)) {
    try {
      var full = await getConcept(n.id || n.concept_id);
      if (full) {
        n.description = full.description || n.description;
        n.sections = full.sections || {};
        n.code = full.code || n.code;
        n.resource = full.resource || n.resource;
        n.tags = full.tags || n.tags || [];
      }
    } catch(e) {}
  }
  // Hide default
  var def = qs('gs-details-default');
  if (def) def.style.display = 'none';
  // Description in header
  var hd = qs('gs-header-desc');
  if (hd) { hd.style.display = n.description ? 'block' : 'none'; hd.textContent = n.description || ''; }
  var col = TYPE_COLORS[n.type] || '#888';
  // Badge in header
  var hb = qs('gs-header-badge');
  if (hb) { hb.style.display = 'inline-flex'; hb.innerHTML = '<span class="dot" style="width:6px;height:6px;border-radius:50%;background:' + col + ';flex:none"></span>' + esc(n.type); hb.style.background = col + '22'; hb.style.color = col; hb.style.border = '1px solid ' + col + '44'; }
  qs('gs-title').textContent = n.title || '';

  // Build details body: Signature → Relationships → Resource → Tags → Docstring
  var detailsHtml = '';
  var rsrc = n.resource || '';
  // 1. Signature (with syntax highlight)
  if (n.sections && n.sections.signature) {
    var sigText = n.sections.signature.replace(/```\w*\n?/g, '').trim();
    var sigLang = (n.sections.signature.match(/```(\w+)/) || [])[1] || '';
    if (!sigLang && rsrc) {
      var extMap = {js:'javascript',ts:'typescript',py:'python',rb:'ruby',rs:'rust',go:'go',java:'java',kt:'kotlin',cs:'csharp',cpp:'cpp',c:'c',sql:'sql',sh:'bash',yaml:'yaml',json:'json',html:'html',css:'css'};
      sigLang = extMap[(rsrc || '').split('.').pop()] || '';
    }
    detailsHtml += '<div class="gs-section"><div class="gs-label">Signature</div><pre class="gs-code' + (sigLang ? ' language-' + sigLang : '') + '"><code' + (sigLang ? ' class="language-' + sigLang + '"' : '') + '>' + esc(sigText) + '</code></pre></div>';
  }
  // 2. Relationships
  var sid = n.id || n.concept_id;
  var relsHtml = '';
  if (sid) {
    var calls = [], calledBy = [];
    _cosmosEdges.forEach(function(l) {
      if (l.source === sid) { var tn = _cosmosNodes.find(function(x) { return (x.id || x.concept_id) === l.target; }); if (tn) calls.push({ title: tn.title, id: tn.id || tn.concept_id, type: tn.type, rel: l.type }); }
      if (l.target === sid) { var sn = _cosmosNodes.find(function(x) { return (x.id || x.concept_id) === l.source; }); if (sn) calledBy.push({ title: sn.title, id: sn.id || sn.concept_id, type: sn.type, rel: l.type }); }
    });
    var relTypes = { 'related': 'relates to', 'calls': 'calls', 'called_by': 'called by', 'imports': 'imports' };
    var anyRel = false;
    [['Outgoing', calls], ['Incoming', calledBy]].forEach(function(pair) {
      var label = pair[0], items = pair[1];
      if (!items.length) return;
      anyRel = true;
      relsHtml += '<div class="gs-section"><div class="gs-label">' + label + '</div>';
      items.forEach(function(r) {
        var rc = TYPE_COLORS[r.type || ''] || '#888';
        var verb = relTypes[r.rel] || r.rel || '';
        relsHtml += '<span class="gs-chip" onclick="graphSidebarJump(\'' + r.id.replace(/'/g,"\\'") + '\')"><span class="dot" style="background:' + rc + '"></span>' + esc(r.title) + '<span style="font-size:9px;color:var(--text-3);margin-left:4px">' + esc(verb) + '</span></span>';
      });
      relsHtml += '</div>';
    });
    if (!anyRel) relsHtml = '<div class="gs-section"><div class="gs-desc" style="color:var(--text-3)">No relationships.</div></div>';
  }
  if (relsHtml) detailsHtml += relsHtml;
  // 3. Resource
  if (rsrc) detailsHtml += '<div class="gs-section"><div class="gs-label">Resource</div><div style="font-size:12px;color:var(--text-3);font-family:JetBrains Mono,monospace">' + esc(rsrc) + '</div></div>';
  // 4. Tags
  var tagsHtml = (n.tags || []).map(function(t) { return '<span class="gs-tag">' + esc(t) + '</span>'; }).join('');
  if (tagsHtml) detailsHtml += '<div class="gs-section"><div class="gs-label">Tags</div><div class="gs-meta">' + tagsHtml + '</div></div>';
  // 5. Docstring (as markdown code block, matching Browse tab style)
  if (n.sections && n.sections.docstring) {
    var md = n.sections.docstring.slice(0, 400).trim();
    detailsHtml += '<div class="gs-section"><div class="gs-label">Docstring</div><pre class="gs-code"><code class="language-md">' + esc(md) + '</code></pre></div>';
  }
  // Set details content atomically
  var detailsBody = qs('gs-details-body');
  if (detailsBody) detailsBody.innerHTML = detailsHtml;
  // Highlight signature code if Prism is available
  if (window.Prism) {
    var sigCode = detailsBody && detailsBody.querySelector('pre.gs-code code');
    if (sigCode) Prism.highlightElement(sigCode);
  }

  // Source
  var src = n.code || n.body || n.source || '';
  var ext = (rsrc || '').split('.').pop();
  if (src) {
    qs('gs-source-content').textContent = src;
    var langMap = {js:'javascript',ts:'typescript',py:'python',rb:'ruby',rs:'rust',go:'go',java:'java',kt:'kotlin',cs:'csharp',cpp:'cpp',c:'c',sql:'sql',sh:'bash',yaml:'yaml',json:'json',html:'html',css:'css'};
    var lang = langMap[ext] || '';
    if (lang && window.Prism) {
      qs('gs-source-content').className = 'gs-code language-' + lang;
      Prism.highlightElement(qs('gs-source-content'));
    }
  } else {
    qs('gs-source-content').textContent = 'No source code available for this concept.';
  }

  // Stay on current tab — don't switch to details
  var curTab = document.querySelector('.gs-tab.active');
  if (curTab) gsSwitchTab(curTab.dataset.gs, curTab);
  else gsSwitchTab('details', document.querySelector('.gs-tab'));
}
function graphSidebarClose() {
  _gsIdx = null;
  _cosmosClickedIdx = null;
  var def = qs('gs-details-default');
  if (def) def.style.display = '';
  var body = qs('gs-details-body');
  if (body) body.innerHTML = '';
  qs('gs-title').textContent = 'Concept Details';
  var hb = qs('gs-header-badge');
  if (hb) hb.style.display = 'none';
  var hd = qs('gs-header-desc');
  if (hd) hd.style.display = 'none';
}
function graphSidebarJump(id) {
  var idx = _cosmosNodes.findIndex(function(n) { return (n.id || n.concept_id) === id; });
  if (idx >= 0) {
    _cosmosClickedIdx = idx;
    graphSidebarOpen(idx);
  }
}
function gsSwitchTab(name, btn) {
  document.querySelectorAll('.gs-tab').forEach(function(t) { t.classList.remove('active'); });
  if (btn) btn.classList.add('active');
  document.querySelectorAll('.gs-pane').forEach(function(p) { p.classList.remove('active'); });
  var pane = qs('gs-' + name);
  if (pane) pane.classList.add('active');
}

async function loadCosmosExplore() {
  if (_cosmosLoaded) return;
  if (_cosmosLoading) return;
  _cosmosLoading = true;
  var loadingEl = qs('cosmos-loading');

  // Dynamic mode: fetch graph + bundle info
  var _cosmosFullTypeCounts = {};
  var _cosmosBundleNames = {};
  if (DYNAMIC) {
    try {
      var gmn = parseInt((qs('graph-max-nodes') || {}).textContent, 10) || 2000;
      var [gd, infoData] = await Promise.all([
        api('/api/graph?max_nodes=' + gmn),
        api('/api/info')
      ]);
      _cosmosNodes = (gd.nodes || []).map(function(n) { return { id: n.id, title: n.label || n.title, type: n.group || n.type, resource: n.resource || '', tags: n.tags || [] }; });
      _cosmosEdges = (gd.edges || []).map(function(e) { return { source: e.from || e.source, target: e.to || e.target, type: e.type || 'related' }; });
      _cosmosFullTypeCounts = infoData.types || {};
      _cosmosFullBundleNames = infoData.name || '';
    } catch(e) {
      _cosmosNodes = []; _cosmosEdges = [];
    }
    _cosmosN = _cosmosNodes.length;
    // Build domain map from fetched nodes
    var doms = {};
    _cosmosNodes.forEach(function(n) {
      var parts = (n.id || '').split('/');
      var d = parts[0] || 'root';
      if (!doms[d]) doms[d] = [];
      doms[d].push(n);
    });
    _cosmosDomainNames = Object.keys(doms).sort();
  } else {
    var deg = _cosmosInitData();
    _cosmosN = _cosmosNodes.length;
  }

  _cosmosPointColors = new Float32Array(_cosmosN * 4);
  _cosmosNodes.forEach(function(n, i) {
    var c = TYPE_COLORS[n.type] || '#888';
    var rgb = _cosmosHexToRgb(c);
    _cosmosPointColors[i*4] = rgb[0]; _cosmosPointColors[i*4+1] = rgb[1];
    _cosmosPointColors[i*4+2] = rgb[2]; _cosmosPointColors[i*4+3] = 0.92;
  });

  qs('status-n').textContent = _cosmosN;
  qs('status-of').textContent = '';
  if (DYNAMIC && _cosmosFullTypeCounts) {
    var fullTotal = 0; for (var k in _cosmosFullTypeCounts) fullTotal += _cosmosFullTypeCounts[k];
    if (fullTotal > _cosmosN) qs('status-of').textContent = ' of ' + fullTotal;
  } else if (BUNDLE_DATA && BUNDLE_DATA.total_concepts > _cosmosN) {
    qs('status-of').textContent = ' of ' + BUNDLE_DATA.total_concepts;
  }
  qs('status-e').textContent = _cosmosEdges.length;

  var legendBody = qs('cosmos-legend-body');
  legendBody.innerHTML = '';
  var typeCounts = {};
  _cosmosNodes.forEach(function(n) { typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; });
  Object.keys(TYPE_COLORS).forEach(function(t) {
    var row = document.createElement('div');
    row.className = 'legend-item';
    var cnt = typeCounts[t] || 0;
    var full = DYNAMIC && _cosmosFullTypeCounts ? _cosmosFullTypeCounts[t] || 0 : cnt;
    var label = DYNAMIC ? cnt + ' / ' + full : '' + cnt;
    row.innerHTML = '<span class="dot" style="background:' + TYPE_COLORS[t] + '"></span><span>' + t + '</span><span class="count">' + label + '</span>';
    row.title = cnt + ' visible of ' + full + ' total';
    legendBody.appendChild(row);
  });

  _cosmosActiveTypes = new Set(Object.keys(TYPE_COLORS));
  
  // Type dropdown
  var typeList = qs('type-list');
  typeList.innerHTML = '';
  Object.keys(TYPE_COLORS).forEach(function(t) {
    var cnt = typeCounts[t] || 0;
    var full = DYNAMIC && _cosmosFullTypeCounts ? _cosmosFullTypeCounts[t] || 0 : cnt;
    var label = DYNAMIC ? cnt + ' / ' + full : '' + cnt;
    var item = document.createElement('div');
    item.className = 'dropdown-item on';
    item.innerHTML = '<div class="item-left"><span class="dot" style="background:' + TYPE_COLORS[t] + '"></span><span class="item-cb"></span><span>' + t + '</span></div><span class="item-count">' + label + '</span>';
    item.title = cnt + ' visible of ' + full + ' total';
    item.onclick = function() {
      if (_cosmosActiveTypes.has(t)) { _cosmosActiveTypes.delete(t); item.classList.remove('on'); }
      else { _cosmosActiveTypes.add(t); item.classList.add('on'); }
      _cosmosFilterChanged();
    };
    typeList.appendChild(item);
  });

  // Bundle dropdown
  var bundleNames = {};
  _cosmosNodes.forEach(function(n) { var b = (n.id || n.concept_id || '').split('/')[0] || 'root'; bundleNames[b] = (bundleNames[b] || 0) + 1; });
  var bundleKeys = Object.keys(bundleNames).sort();
  _cosmosActiveBundles = new Set(bundleKeys);
  var bundleList = qs('bundle-list');
  bundleList.innerHTML = '';
  bundleKeys.forEach(function(b) {
    var item = document.createElement('div');
    item.className = 'dropdown-item on';
    item.innerHTML = '<div class="item-left"><span class="item-cb"></span><span>' + esc(b) + '</span></div><span class="item-count">' + bundleNames[b] + '</span>';
    item.onclick = function() {
      if (_cosmosActiveBundles.has(b)) { _cosmosActiveBundles.delete(b); item.classList.remove('on'); }
      else { _cosmosActiveBundles.add(b); item.classList.add('on'); }
      _cosmosFilterChanged();
      updateBundleBadge();
    };
    bundleList.appendChild(item);
  });
  updateBundleBadge();

  var labelRoot = qs('cluster-labels');

  var labelRoot = qs('cluster-labels');
  labelRoot.innerHTML = '';
  _cosmosDomainNames.forEach(function(d) {
    var el = document.createElement('div');
    el.className = 'cluster-label'; el.textContent = d;
    labelRoot.appendChild(el);
  });

  _canvas2DSwitch();
  loadingEl.classList.add('hide');
  _cosmosLoaded = true;
  _cosmosLoading = false;

  // Populate default sidebar stats
  var statsHtml = '';
  var typeCounts = {};
  _cosmosNodes.forEach(function(n) { typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; });
  Object.keys(typeCounts).sort().forEach(function(t) {
    var c = TYPE_COLORS[t] || '#888';
    statsHtml += '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-md);padding:8px 12px;min-width:70px"><div style="font-size:16px;font-weight:700;color:' + c + '">' + typeCounts[t] + '</div><div style="font-size:9px;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em;margin-top:2px">' + t + '</div></div>';
  });
  var defStats = qs('gs-default-stats');
  if (defStats) defStats.innerHTML = statsHtml;
}

var _cosmosSearchTimer;
(function() {
  var searchEl = qs('cosmos-search');
  if (!searchEl) return;
  searchEl.addEventListener('input', function(e) {
    clearTimeout(_cosmosSearchTimer);
    var q = e.target.value.trim().toLowerCase();
    _cosmosSearchTimer = setTimeout(function() {
      _cosmosMatchSet = q ? new Set(_cosmosNodes.map(function(n,i){ return [n,i]; }).filter(function(p){ return (p[0].title||'').toLowerCase().includes(q); }).map(function(p){ return p[1]; })) : null;
      _cosmosFilterChanged();
    }, 120);
  });
})();

// Hover tag
var _cosmosHoverEl = qs('hover-tag');
function _cosmosShowHoverTag(idx) {
  var n = _cosmosNodes[idx];
  if (!n) return;
  _cosmosHoverEl.innerHTML = (n.title || '') + '<span class="type">' + (n.type || '') + '</span>';
  _cosmosHoverEl.classList.add('show');
  _cosmosPosHoverTag();
}
function _cosmosHideHoverTag() { _cosmosHoverEl.classList.remove('show'); }
function _cosmosPosHoverTag() { _cosmosHoverEl.style.left = _cosmosPtrX + 'px'; _cosmosHoverEl.style.top = _cosmosPtrY + 'px'; }

function cosmosZoom(f) {}
function cosmosTogglePhysics() {}
function cosmosReset() {}

// ---- Dropdown Toggle ----
var _openDropdown = null;
function toggleDropdown(name) {
  var menu = qs(name + '-menu');
  if (!menu) return;
  if (_openDropdown && _openDropdown !== menu) _openDropdown.classList.remove('open');
  menu.classList.toggle('open');
  _openDropdown = menu.classList.contains('open') ? menu : null;
}
function updateBundleBadge() {
  var active = _cosmosActiveBundles ? _cosmosActiveBundles.size : 0;
  var total = _cosmosActiveBundles ? _cosmosNodes.reduce(function(s,n) { var b=(n.id||n.concept_id||'').split('/')[0]||'root'; s.add(b); return s; }, new Set()).size : 0;
  var badge = qs('bundle-badge');
  if (badge) badge.textContent = active < total ? '' + active : 'All';
}
document.addEventListener('click', function(e) {
  if (_openDropdown && !e.target.closest('.dropdown-wrapper')) {
    _openDropdown.classList.remove('open');
    _openDropdown = null;
  }
});
window.cosmosZoom = cosmosZoom;
window.cosmosReset = cosmosReset;

// ---- Tab Switching ----
function switchTab(name, btn) {
  document.querySelectorAll('.nav-seg-item').forEach(function(t) { t.classList.remove('active'); });
  if (btn) btn.classList.add('active');
  document.querySelectorAll('.tab-content').forEach(function(t) { t.classList.remove('active'); });
  qs('tab-' + name).classList.add('active');
  if (name === 'graph') {
    qs('main').classList.add('wide');
    loadCosmosExplore();
  } else {
    qs('main').classList.remove('wide');
  }
}

// ---- Theme ----
(function() {
  var t = localStorage.getItem('okf-theme') || 'dark';
  document.documentElement.setAttribute('data-theme', t);
})();
function toggleTheme() {
  var html = document.documentElement;
  var cur = html.getAttribute('data-theme');
  var next = cur === 'dark' ? 'light' : 'dark';
  html.setAttribute('data-theme', next);
  localStorage.setItem('okf-theme', next);
  if (egoCy) { var c = conceptCache[currentId]; if (c) renderEgoGraph(currentId, c); }
}

// ---- Sidebar resize ----
(function() {
  var sidebar = qs('sidebar');
  var handle = qs('sidebar-resize');
  if (!sidebar || !handle) return;
  var isResizing = false;
  handle.addEventListener('mousedown', function(e) {
    isResizing = true;
    handle.classList.add('active');
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
  });
  document.addEventListener('mousemove', function(e) {
    if (!isResizing) return;
    var newWidth = Math.max(180, Math.min(600, e.clientX));
    sidebar.style.width = newWidth + 'px';
    sidebar.style.flex = 'none';
  });
  document.addEventListener('mouseup', function() {
    if (isResizing) {
      isResizing = false;
      handle.classList.remove('active');
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    }
  });
})();

// ---- Keyboard shortcuts ----
document.addEventListener('keydown', function(e) {
  if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement !== qs('search')) { qs('search').focus(); e.preventDefault(); }
  if (e.key === ' ' && e.target === document.body) { e.preventDefault(); }
});
</script>
</body>
</html>
