UNPKG

voyageai-cli

Version:

CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search

24,881 lines 968 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VAI Playground</title>
<link rel="icon" type="image/png" sizes="32x32" href="/icons/dark/32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icons/dark/16.png">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* Vai Dark Mode Palette (default) — Teal/Cyan brand */
  --bg: #001E2B;            /* MDB Black */
  --bg-surface: #112733;    /* Gray Dark 4 */
  --bg-card: #1C2D38;       /* Gray Dark 3 */
  --bg-input: #112733;      /* Gray Dark 4 */
  --accent: #00D4AA;        /* Teal — interactive elements */
  --accent-text: #FFFFFF;   /* Bright white — headings/labels in dark mode */
  --accent-dim: #009E80;    /* Teal Dark */
  --accent-glow: rgba(0, 212, 170, 0.10);
  --text: #E8EDEB;          /* Gray Light 2 */
  --text-dim: #C1C7C6;      /* Gray Light 1 */
  --text-muted: #889397;    /* Gray Base */
  --border: #3D4F58;        /* Gray Dark 2 */
  --error: #FF6960;         /* Red Light 1 */
  --warning: #FFC010;       /* Yellow Base */
  --success: #00D4AA;       /* Teal */
  --red: #FF6960;           /* Red Light 1 */
  --yellow: #FFC010;        /* Yellow Base */
  --green: #00D4AA;         /* Teal */
  --blue: #40E0FF;          /* Cyan (links) */
  --purple: #B45AF2;        /* Purple Base */
  --green-dark: #00241E;    /* Teal Dark 3 */
  --radius: 8px;
  --font: 'Euclid Circular A', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  --mono: 'Source Code Pro', 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
}

/* Vai Light Mode Palette — Teal/Cyan brand */
[data-theme="light"] {
  --bg: #FFFFFF;             /* White */
  --bg-surface: #F9FBFA;    /* Gray Light 3 */
  --bg-card: #FFFFFF;        /* White */
  --bg-input: #F9FBFA;      /* Gray Light 3 */
  --accent: #009E80;         /* Teal Dark — interactive elements */
  --accent-text: #001E2B;   /* MDB Black — headings/labels in light mode */
  --accent-dim: #007A63;     /* Teal Darker */
  --accent-glow: rgba(0, 158, 128, 0.08);
  --text: #001E2B;           /* MDB Black */
  --text-dim: #5C6C75;       /* Gray Dark 1 */
  --text-muted: #889397;     /* Gray Base */
  --border: #E8EDEB;         /* Gray Light 2 */
  --error: #DB3030;          /* Red Base */
  --warning: #944F01;        /* Yellow Dark 2 */
  --success: #009E80;        /* Teal Dark */
  --red: #DB3030;            /* Red Base */
  --yellow: #944F01;         /* Yellow Dark 2 */
  --green: #009E80;          /* Teal Dark */
  --blue: #0088CC;           /* Cyan Dark (links) */
  --purple: #5E0C9E;         /* Purple Dark 2 */
  --green-dark: #00241E;     /* Teal Dark 3 */
}
/* Light mode shadow + card adjustments */
[data-theme="light"] .explore-card,
[data-theme="light"] .card,
[data-theme="light"] .cost-strategy,
[data-theme="light"] .cost-summary-card {
  box-shadow: 0 1px 4px rgba(0, 30, 43, 0.08);
}
[data-theme="light"] .explore-card {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.86)),
    radial-gradient(circle at top right, rgba(0, 136, 204, 0.10), transparent 42%),
    var(--bg-card);
}
[data-theme="light"] .explore-card::before {
  opacity: 0.7;
}
[data-theme="light"] .explore-card::after {
  opacity: 0.45;
}
[data-theme="light"] .explore-card:hover {
  box-shadow: 0 10px 24px rgba(0, 158, 128, 0.14);
}
[data-theme="light"] .cost-modal,
[data-theme="light"] .explore-modal {
  box-shadow: 0 20px 60px rgba(0, 30, 43, 0.2);
}
[data-theme="light"] .cost-modal-overlay,
[data-theme="light"] .explore-modal-overlay {
  background: rgba(0, 30, 43, 0.4);
}
[data-theme="light"] .sidebar {
  box-shadow: 1px 0 3px rgba(0, 30, 43, 0.06);
}
/* Light mode gradient overrides */
[data-theme="light"] .quant-bar-fill.storage { background: linear-gradient(90deg, #009E80, #00D4AA); }
[data-theme="light"] .quant-bar-fill.latency { background: linear-gradient(90deg, #0088CC, #40E0FF); }
[data-theme="light"] .quant-meter-fill.perfect { background: linear-gradient(90deg, #009E80, #00D4AA); }
[data-theme="light"] .quant-meter-fill.good { background: linear-gradient(90deg, #944F01, #FFC010); }
[data-theme="light"] .quant-meter-fill.degraded { background: linear-gradient(90deg, #DB3030, #FF6960); }
/* Light mode button text */
[data-theme="light"] .btn { color: #FFFFFF; }
[data-theme="light"] .btn:hover { background: #007A63; }

html, body { height: 100%; }

body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  overflow-x: hidden;
}

/* ── Update banner ── */
.update-banner {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  background: linear-gradient(135deg, rgba(0,158,128,0.12), rgba(4,152,236,0.12));
  border-bottom: 1px solid rgba(0,212,170,0.2);
  font-size: 13px;
  color: var(--text);
  flex-shrink: 0;
}
.update-banner.show { display: flex; }
.update-banner-icon { font-size: 16px; }
.update-banner-text { flex: 1; }
.update-banner-text strong { color: var(--accent); }
.update-banner-btn {
  background: var(--accent);
  color: var(--bg);
  border: none;
  border-radius: 4px;
  padding: 5px 14px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  font-family: var(--font);
  transition: opacity 0.15s;
}
.update-banner-btn:hover { opacity: 0.85; }
.update-banner-dismiss {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  padding: 0 4px;
}
.update-banner-dismiss:hover { color: var(--text); }
.update-progress { display: flex; align-items: center; gap: 8px; flex: 1; }
.update-progress-bar { flex: 1; height: 6px; background: rgba(255,255,255,0.15); border-radius: 3px; overflow: hidden; }
[data-theme="light"] .update-progress-bar { background: rgba(0,0,0,0.1); }
.update-progress-fill { height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s ease; width: 0%; }
.update-progress-label { font-size: 12px; font-family: var(--mono); color: var(--accent); min-width: 40px; text-align: right; }

/* ── App Shell: sidebar + content layout ── */
.app-shell {
  display: flex;
  height: 100vh;
  overflow: hidden;
}

/* ── Sidebar ── */
.sidebar {
  width: 316px;
  min-width: 316px;
  background: var(--bg-surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 100;
  transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed {
  width: 72px;
  min-width: 72px;
}
.sidebar.collapsed .sidebar-brand-copy,
.sidebar.collapsed .sidebar-panel,
.sidebar.collapsed .status-label,
.sidebar.collapsed .sidebar-bug-label,
.sidebar.collapsed .sidebar-docs-link span,
.sidebar.collapsed #appVersionLabel,
.sidebar.collapsed .theme-toggle,
.sidebar.collapsed .sidebar-search-btn,
.sidebar.collapsed .sidebar-footer > div:last-child { display: none; }
.sidebar.collapsed .sidebar-footer { overflow: hidden; padding: 12px 0 56px; align-items: center; border-top: none; }
.sidebar.collapsed .sidebar-footer > div:first-child { justify-content: center; width: 100%; }
.sidebar.collapsed .sidebar-drag-region { justify-content: center; padding: 0; padding-top: 52px; }
body:not(.is-electron) .sidebar.collapsed .sidebar-drag-region { padding-top: 16px; padding-bottom: 12px; }

/* Sidebar collapse toggle */
.sidebar-collapse-btn,
.sidebar-search-btn {
  -webkit-app-region: no-drag;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 4px;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s;
  flex-shrink: 0;
  font-size: 14px;
  line-height: 1;
}
.sidebar-collapse-btn:hover,
.sidebar-search-btn:hover,
.sidebar-brand:hover { color: var(--text); background: rgba(255,255,255,0.06); }
[data-theme="light"] .sidebar-collapse-btn:hover,
[data-theme="light"] .sidebar-search-btn:hover,
[data-theme="light"] .sidebar-brand:hover { background: rgba(0,0,0,0.04); }
.sidebar.collapsed .sidebar-collapse-btn { transform: rotate(180deg); }

/* Sidebar tooltip for collapsed mode */
.sidebar.collapsed .sidebar-rail-btn {
  position: relative;
}
.sidebar.collapsed .sidebar-rail-btn::after {
  content: attr(data-tooltip);
  position: absolute;
  left: calc(100% + 8px);
  top: 50%;
  transform: translateY(-50%);
  background: #2d333b;
  color: #e6edf3;
  font-size: 12px;
  padding: 4px 8px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s 0.15s;
  z-index: 1000;
}
.sidebar.collapsed .sidebar-rail-btn:hover::after {
  opacity: 1;
}

.sidebar-drag-region {
  -webkit-app-region: drag;
  min-height: 82px;
  padding: 0 12px;
  padding-top: 52px; /* Clear macOS traffic lights fully */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
body:not(.is-electron) .sidebar-drag-region {
  height: auto;
  min-height: auto;
  padding-top: 16px;
  padding-bottom: 12px;
}

.sidebar-logo {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  flex-shrink: 0;
  object-fit: contain;
}

.sidebar-brand {
  -webkit-app-region: no-drag;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  padding: 8px 10px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  transition: background 0.15s ease, color 0.15s ease;
}

.sidebar-brand-copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  align-items: flex-start;
}

.sidebar-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--accent-text);
  white-space: nowrap;
  letter-spacing: -0.2px;
}

.sidebar-subtitle {
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar-header-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  -webkit-app-region: no-drag;
}

.sidebar-nav-shell {
  flex: 1;
  min-height: 0;
  display: flex;
  overflow: hidden;
}

.sidebar-rail {
  width: 84px;
  min-width: 84px;
  border-right: 1px solid var(--border);
  padding: 10px 8px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow-y: auto;
}

.sidebar.collapsed .sidebar-rail {
  width: 100%;
  min-width: 0;
  border-right: none;
  padding-inline: 6px;
}

.sidebar-rail-btn {
  background: none;
  border: 1px solid transparent;
  border-radius: 12px;
  color: var(--text-muted);
  padding: 10px 6px 8px;
  min-height: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  transition: all 0.15s ease;
  font-family: var(--font);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.sidebar-rail-btn:hover {
  color: var(--text);
  background: rgba(255,255,255,0.04);
}

.sidebar-rail-btn.active {
  color: var(--accent-text);
  background: var(--accent-glow);
  border-color: rgba(0,212,170,0.14);
}

[data-theme="light"] .sidebar-rail-btn:hover { background: rgba(0,30,43,0.04); }
[data-theme="light"] .sidebar-rail-btn.active { background: rgba(0,158,128,0.08); }

.sidebar-rail-btn-icon {
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sidebar-rail-btn-icon svg {
  width: 16px;
  height: 16px;
}

.sidebar-rail-btn-label {
  display: block;
  width: 100%;
  text-align: center;
  line-height: 1.1;
  white-space: nowrap;
}

.sidebar.collapsed .sidebar-rail-btn {
  padding: 10px 0;
  min-height: 44px;
}

.sidebar.collapsed .sidebar-rail-btn-label {
  display: none;
}

.sidebar-panel {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.sidebar-panel-header {
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--border);
}

.sidebar-panel-eyebrow {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.sidebar-panel-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.02em;
}

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: 10px 8px 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.sidebar-nav-group {
  display: none;
  flex-direction: column;
  gap: 2px;
}

.sidebar-nav-group.active {
  display: flex;
}

.sidebar-nav-section {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  padding: 0 8px 8px;
}

.tab-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: none;
  border: 1px solid transparent;
  border-radius: 10px;
  color: var(--text-dim);
  padding: 10px 12px;
  font-size: 13px;
  font-weight: 500;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
  text-align: left;
  position: relative;
}
.tab-btn:hover {
  color: var(--text);
  background: rgba(255,255,255,0.05);
  border-color: rgba(255,255,255,0.05);
}
.tab-btn:hover .tab-btn-icon {
  transform: scale(1.1);
}
.tab-btn.active {
  color: var(--accent);
  background: var(--accent-glow);
  border-color: rgba(0,212,170,0.18);
  font-weight: 600;
}
.tab-btn-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  transition: transform 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tab-btn-icon svg {
  width: 16px;
  height: 16px;
}
[data-theme="light"] .tab-btn:hover { background: rgba(0,30,43,0.04); border-color: rgba(0,30,43,0.06); }
[data-theme="light"] .tab-btn.active { background: rgba(0,158,128,0.08); }

.sidebar-footer {
  padding: 12px 16px;
  padding-bottom: 56px; /* Clear the fixed cost status bar */
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sidebar-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.theme-toggle {
  background: none;
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 4px 8px;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  gap: 4px;
}
.theme-toggle:hover {
  border-color: var(--accent);
  background: var(--accent-glow);
}

.status-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  transition: background 0.3s;
}
.status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--accent-glow); }
.status-dot.error { background: var(--error); }

.status-label {
  font-size: 11px;
  color: var(--text-dim);
}

/* ── Content area ── */
.content-area {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  position: relative;
}

.content-area::after {
  content: '';
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  width: 200px;
  height: 200px;
  background-image: url('/icons/watermark.png');
  background-size: contain;
  background-repeat: no-repeat;
  opacity: 0.05;
  pointer-events: none;
  z-index: -1;
  filter: invert(1);
}

[data-theme="light"] .content-area::after {
  filter: none;
}

.content-drag-region {
  -webkit-app-region: drag;
  height: 38px;
  min-height: 38px;
  flex-shrink: 0;
}

/* Web mode: hide Electron-only elements */
body:not(.is-electron) .content-drag-region { display: none; }
body:not(.is-electron) .update-banner { display: none !important; }

/* Main */
.main { padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%; flex: 1; overflow-y: auto; min-height: 0; }

/* Legacy compat — hide old horizontal bar (replaced by sidebar) */
.nav { display: none; }
.tab-bar-horizontal { display: none; }

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

/* Shared Components */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  margin-bottom: 16px;
}

.card-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--accent-text);
  margin-bottom: 12px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

textarea, input[type="text"], input[type="number"] {
  width: 100%;
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 13px;
  resize: vertical;
  transition: border-color 0.2s;
}
textarea:focus, input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
}

select {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  border-radius: var(--radius);
  font-size: 13px;
  font-family: var(--font);
  cursor: pointer;
}
select:focus { outline: none; border-color: var(--accent); }

.btn {
  background: var(--accent);
  color: var(--green-dark);
  border: none;
  padding: 10px 24px;
  border-radius: var(--radius);
  font-size: 14px;
  font-weight: 600;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.2s;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.btn:hover { background: #5CE8CC; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }

.btn-secondary {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent-dim);
}
.btn-secondary:hover { background: var(--accent-glow); }

.btn-small {
  padding: 6px 14px;
  font-size: 12px;
}

/* Subtabs - segmented control style */
.subtabs {
  display: flex;
  background: var(--card);
  border-radius: var(--radius);
  padding: 4px;
  gap: 4px;
  margin-bottom: 20px;
  border: 1px solid var(--accent-dim);
}
.subtab {
  flex: 1;
  background: transparent;
  color: var(--text-secondary);
  border: none;
  padding: 10px 20px;
  border-radius: calc(var(--radius) - 2px);
  font-size: 14px;
  font-weight: 500;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.subtab:hover {
  color: var(--text);
  background: rgba(0, 212, 170, 0.05);
}
.subtab.active {
  background: var(--accent);
  color: var(--green-dark);
  font-weight: 600;
}
.subtab svg {
  width: 16px;
  height: 16px;
}
[data-theme="light"] .subtab.active { color: #FFFFFF; }

.options-row {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin: 12px 0;
}

.option-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.option-label {
  font-size: 11px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.error-msg {
  background: rgba(255, 107, 107, 0.1);
  border: 1px solid rgba(255, 107, 107, 0.3);
  color: var(--error);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: 13px;
  margin-top: 12px;
  display: none;
}
.error-msg.visible { display: block; }

.spinner {
  display: inline-block;
  width: 16px; height: 16px;
  border: 2px solid transparent;
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.result-section {
  margin-top: 16px;
  display: none;
}
.result-section.visible { display: block; }

.stat {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--bg-input);
  padding: 6px 12px;
  border-radius: var(--radius);
  font-size: 13px;
  margin-right: 8px;
  margin-bottom: 8px;
}
.stat-label { color: var(--text-dim); }
.stat-value { color: var(--accent-text); font-weight: 600; font-family: var(--mono); }

.vector-preview {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text-dim);
  background: var(--bg-input);
  padding: 12px;
  border-radius: var(--radius);
  overflow-x: auto;
  white-space: nowrap;
  margin-top: 8px;
}

.heatmap {
  height: 24px;
  border-radius: 4px;
  margin-top: 8px;
  overflow: hidden;
  display: flex;
}
.heatmap-bar {
  flex: 1;
  min-width: 1px;
}

/* Compare tab */
.compare-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.similarity-display {
  text-align: center;
  padding: 32px 0;
}
.similarity-score {
  font-size: 72px;
  font-weight: 800;
  font-family: var(--mono);
  line-height: 1;
}
.similarity-label {
  font-size: 14px;
  color: var(--text-dim);
  margin-top: 8px;
}
.similarity-bar-outer {
  width: 100%;
  max-width: 400px;
  height: 8px;
  background: var(--bg-input);
  border-radius: 4px;
  margin: 16px auto 0;
  overflow: hidden;
}
.similarity-bar-inner {
  height: 100%;
  border-radius: 4px;
  transition: width 0.6s ease, background 0.6s ease;
}
.metrics-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-top: 24px;
}
.metric-card {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px 16px;
  text-align: center;
  transition: border-color 0.2s;
}
.metric-card.primary {
  border-color: var(--accent);
  background: var(--accent-glow);
}
.metric-card-value {
  font-family: var(--mono);
  font-size: 28px;
  font-weight: 700;
  line-height: 1;
}
.metric-card-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-top: 10px;
}
.metric-card-desc {
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 4px;
  line-height: 1.4;
}
.metric-bar {
  width: 100%;
  height: 6px;
  background: var(--bg);
  border-radius: 3px;
  margin-top: 12px;
  overflow: hidden;
}
.metric-bar-fill {
  height: 100%;
  border-radius: 3px;
  transition: width 0.6s ease, background 0.6s ease;
}
.metric-note {
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 16px;
  padding: 10px 16px;
  background: var(--bg-input);
  border-radius: 8px;
  line-height: 1.6;
}

/* Search tab */
.search-results {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.search-results.single-col {
  grid-template-columns: 1fr;
}

.result-item {
  display: flex;
  gap: 12px;
  padding: 12px;
  background: var(--bg-input);
  border-radius: var(--radius);
  margin-bottom: 8px;
  border-left: 3px solid var(--border);
  transition: border-color 0.3s;
}
.result-item.moved-up { border-left-color: var(--green); }
.result-item.moved-down { border-left-color: var(--red); }

.result-rank {
  font-size: 20px;
  font-weight: 700;
  color: var(--accent-text);
  font-family: var(--mono);
  min-width: 30px;
}
.result-body { flex: 1; min-width: 0; }
.result-text {
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.result-score-bar {
  height: 4px;
  background: var(--bg-surface);
  border-radius: 2px;
  margin-top: 6px;
  overflow: hidden;
}
.result-score-fill {
  height: 100%;
  border-radius: 2px;
  background: var(--accent);
}
.result-score-text {
  font-size: 11px;
  font-family: var(--mono);
  color: var(--text-dim);
  margin-top: 4px;
}
.result-movement {
  font-size: 11px;
  font-family: var(--mono);
  margin-top: 2px;
}

/* Explore tab */
.explore-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

.explore-card {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)),
    radial-gradient(circle at top right, rgba(64, 224, 255, 0.09), transparent 42%),
    var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  cursor: pointer;
  transition: all 0.2s;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.explore-card::before {
  content: '';
  position: absolute;
  top: -36px;
  right: -28px;
  width: 140px;
  height: 140px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(0, 212, 170, 0.16) 0%, rgba(64, 224, 255, 0.10) 38%, rgba(64, 224, 255, 0) 72%);
  opacity: 0.9;
  pointer-events: none;
  transform: translate3d(0, 0, 0);
  transition: transform 0.2s ease, opacity 0.2s ease;
  z-index: 0;
}
.explore-card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(64, 224, 255, 0.08);
  opacity: 0.8;
  pointer-events: none;
  z-index: 0;
}
.explore-card > * {
  position: relative;
  z-index: 1;
}
.explore-card:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(0, 212, 170, 0.12);
}
.explore-card:hover::before {
  transform: translate3d(-6px, 4px, 0) scale(1.05);
  opacity: 1;
}
.explore-card:hover::after {
  border-color: rgba(64, 224, 255, 0.16);
}
.explore-card.expanded {
  border-color: var(--accent);
}

.explore-card-icon {
  margin-bottom: 8px;
  color: var(--accent, #00D4AA);
  opacity: 0.85;
}
.explore-card-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 4px;
}
.explore-card-summary {
  font-size: 13px;
  color: var(--text-dim);
}
.explore-card-content {
  display: none;
}
.explore-card-actions {
  display: none;
}

/* ── Models Tab ── */
.models-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.models-filter-pills {
  display: flex;
  gap: 6px;
}
.models-filter-pill {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 4px 12px;
  border-radius: 20px;
  font-size: 12px;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.15s;
}
.models-filter-pill:hover {
  border-color: var(--accent);
  color: var(--text);
}
.models-filter-pill.active {
  background: rgba(0, 212, 170, 0.12);
  border-color: var(--accent);
  color: var(--accent);
}
.models-hero {
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)),
    radial-gradient(circle at top right, rgba(64, 224, 255, 0.10), transparent 34%),
    linear-gradient(135deg, rgba(0, 212, 170, 0.06), rgba(64, 224, 255, 0.05)),
    var(--bg-card);
  border: 1px solid rgba(0, 212, 170, 0.2);
  margin-bottom: 24px;
  isolation: isolate;
}
.models-hero::before {
  content: '';
  position: absolute;
  top: -30px;
  right: -30px;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(0, 212, 170, 0.18) 0%, rgba(64, 224, 255, 0.10) 38%, rgba(64, 224, 255, 0) 72%);
  pointer-events: none;
  z-index: 0;
}
.models-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(64, 224, 255, 0.08);
  pointer-events: none;
  z-index: 0;
}
.models-hero-content {
  position: relative;
  z-index: 1;
}
.models-hero-badge {
  display: inline-block;
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: #001E2B;
  font-size: 11px;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 12px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 10px;
}
.models-hero-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 8px;
}
.models-hero-desc {
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.6;
  max-width: 700px;
  margin-bottom: 14px;
}
.models-hero-models {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.models-hero-spotlight {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px;
  align-items: center;
  margin-bottom: 16px;
  padding: 16px 18px;
  border-radius: 14px;
  border: 1px solid rgba(64, 224, 255, 0.16);
  background:
    linear-gradient(135deg, rgba(64, 224, 255, 0.10), rgba(0, 212, 170, 0.06)),
    rgba(0, 30, 43, 0.24);
}
.models-hero-spotlight-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.models-hero-spotlight-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 6px;
}
.models-hero-spotlight-copy {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-dim);
  max-width: 720px;
}
.models-hero-spotlight-note {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 8px;
}
.models-hero-spotlight-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.models-hero-spotlight-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.02);
  color: var(--text);
  text-decoration: none;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
}
.models-hero-spotlight-link:hover {
  border-color: var(--accent);
  color: var(--accent-text);
  background: rgba(0, 212, 170, 0.08);
}
.models-hero-spotlight-link.primary {
  background: linear-gradient(135deg, rgba(0, 212, 170, 0.20), rgba(64, 224, 255, 0.12));
  border-color: rgba(0, 212, 170, 0.28);
}
[data-theme="light"] .models-hero-spotlight {
  background:
    linear-gradient(135deg, rgba(0, 136, 204, 0.08), rgba(0, 158, 128, 0.05)),
    rgba(255, 255, 255, 0.85);
  border-color: rgba(0, 136, 204, 0.16);
}
[data-theme="light"] .models-hero-spotlight-link {
  background: rgba(255, 255, 255, 0.7);
}
@media (max-width: 860px) {
  .models-hero-spotlight {
    grid-template-columns: 1fr;
  }
  .models-hero-spotlight-actions {
    justify-content: flex-start;
  }
}
.model-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  cursor: pointer;
  transition: all 0.15s;
}
.model-chip:hover {
  border-color: var(--accent);
  background: rgba(0, 212, 170, 0.06);
}
.model-chip-price {
  color: var(--text-muted);
  font-size: 11px;
}
.models-group {
  margin-bottom: 24px;
}
.models-group-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
.models-group-icon {
  color: var(--accent);
  opacity: 0.8;
}
.models-group-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}
.models-group-count {
  font-size: 11px;
  color: var(--text-muted);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1px 8px;
}
.models-group-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px;
}
.model-card {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)),
    radial-gradient(circle at top right, rgba(64, 224, 255, 0.08), transparent 38%),
    var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  cursor: default;
  transition: all 0.2s;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.model-card::before {
  content: '';
  position: absolute;
  top: -34px;
  right: -24px;
  width: 132px;
  height: 132px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(0, 212, 170, 0.14) 0%, rgba(64, 224, 255, 0.08) 40%, rgba(64, 224, 255, 0) 72%);
  opacity: 0.9;
  pointer-events: none;
  transition: transform 0.2s ease, opacity 0.2s ease;
  z-index: 0;
}
.model-card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(64, 224, 255, 0.06);
  pointer-events: none;
  z-index: 0;
}
.model-card > * {
  position: relative;
  z-index: 1;
}
.model-card:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(0, 212, 170, 0.12);
}
.model-card:hover::before {
  transform: translate3d(-6px, 4px, 0) scale(1.04);
  opacity: 1;
}
.model-card:hover::after {
  border-color: rgba(64, 224, 255, 0.14);
}
.model-card.legacy {
  opacity: 0.5;
}
.model-card.legacy:hover {
  opacity: 0.7;
}
.model-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
  position: relative;
  z-index: 1;
}
.model-card-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  font-family: var(--mono);
}
.model-card-type-badge {
  font-size: 10px;
  padding: 2px 8px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  font-weight: 600;
}
.model-card-type-badge.embedding {
  background: rgba(0, 212, 170, 0.15);
  color: #00D4AA;
}
.model-card-type-badge.reranking {
  background: rgba(64, 224, 255, 0.15);
  color: #40E0FF;
}
.model-card-type-badge.multimodal {
  background: rgba(255, 170, 64, 0.15);
  color: #FFAA40;
}
.model-card-shared-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  color: var(--blue, #40E0FF);
  margin-bottom: 6px;
  position: relative;
  z-index: 1;
}
.model-card-rteb {
  position: absolute;
  top: 14px;
  right: 14px;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  font-family: var(--mono);
  z-index: 1;
}
.model-card-desc {
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 12px;
  position: relative;
  z-index: 1;
}
.model-card-specs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 16px;
  font-size: 12px;
  position: relative;
  z-index: 1;
}
.model-card-spec {
  display: flex;
  justify-content: space-between;
}
.model-card-spec-label {
  color: var(--text-muted);
}
.model-card-spec-value {
  color: var(--text);
  font-family: var(--mono);
  font-weight: 500;
}
.model-card-actions {
  display: flex;
  gap: 6px;
  margin-top: 12px;
  position: relative;
  z-index: 1;
}
.model-card-action {
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--accent);
  cursor: pointer;
  font-family: var(--font);
  transition: all 0.15s;
}
.model-card-action:hover {
  background: rgba(0, 212, 170, 0.1);
  border-color: var(--accent);
}
.models-rteb-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}
.models-rteb-label {
  min-width: 160px;
  font-size: 13px;
  white-space: nowrap;
}
.models-rteb-track {
  flex: 1;
  height: 24px;
  background: var(--bg-surface);
  border-radius: 4px;
  overflow: hidden;
}
.models-rteb-fill {
  height: 100%;
  border-radius: 4px;
  transition: width 0.6s ease-out;
}
.models-rteb-score {
  min-width: 50px;
  text-align: right;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 13px;
}

/* ── Model Picker Overlay ── */
.model-picker-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  z-index: 1001;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.model-picker-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.model-picker {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  width: 480px;
  max-width: 90vw;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.model-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
}
.model-picker-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}
.model-picker-context {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
.model-picker-close {
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 22px;
  cursor: pointer;
  padding: 0 4px;
  line-height: 1;
}
.model-picker-close:hover {
  color: var(--text);
}
.model-picker-search {
  padding: 12px 20px;
}
.model-picker-search input {
  width: 100%;
  background: var(--bg-input, var(--bg-surface));
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  border-radius: var(--radius);
  font-size: 13px;
  font-family: var(--font);
}
.model-picker-recommendation {
  padding: 0 20px 8px;
  font-size: 12px;
  color: var(--blue, #40E0FF);
  display: none;
}
.model-picker-recommendation.visible {
  display: block;
}
.model-picker-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 12px 12px;
}
.model-picker-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.1s;
  border: 1px solid transparent;
}
.model-picker-item:hover,
.model-picker-item:focus {
  background: rgba(0, 212, 170, 0.06);
  border-color: var(--border);
  outline: none;
}
.model-picker-item.selected {
  background: rgba(0, 212, 170, 0.1);
  border-color: var(--accent);
}
.model-picker-item-radio {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 2px;
  transition: all 0.15s;
}
.model-picker-item.selected .model-picker-item-radio {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: inset 0 0 0 3px var(--bg-card);
}
.model-picker-item-info {
  flex: 1;
  min-width: 0;
}
.model-picker-item-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  font-family: var(--mono);
}
.model-picker-item-meta {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
}
.model-picker-item-tags {
  display: flex;
  gap: 4px;
  margin-top: 4px;
  flex-wrap: wrap;
}
.model-picker-item-tag {
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 8px;
  background: rgba(0, 212, 170, 0.08);
  color: var(--accent);
}
.model-picker-item-shared {
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 8px;
  background: rgba(64, 224, 255, 0.1);
  color: var(--blue, #40E0FF);
}
.model-picker-footer {
  padding: 10px 20px;
  border-top: 1px solid var(--border);
  text-align: center;
  font-size: 12px;
}
.model-picker-footer a {
  color: var(--accent);
  text-decoration: none;
}
.model-picker-footer a:hover {
  text-decoration: underline;
}
.model-picker-trigger {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 4px;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-left: 4px;
}
.model-picker-trigger:hover {
  color: var(--accent);
  background: rgba(0, 212, 170, 0.08);
  border-color: var(--accent);
}
[data-theme="light"] .model-card:hover {
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.09);
}
[data-theme="light"] .models-hero {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.86)),
    radial-gradient(circle at top right, rgba(0, 136, 204, 0.10), transparent 34%),
    linear-gradient(135deg, rgba(0, 212, 170, 0.06), rgba(64, 224, 255, 0.04)),
    var(--bg-card);
}
[data-theme="light"] .models-hero::before,
[data-theme="light"] .model-card::before {
  opacity: 0.7;
}
[data-theme="light"] .models-hero::after,
[data-theme="light"] .model-card::after {
  border-color: rgba(0, 136, 204, 0.08);
}
[data-theme="light"] .model-card {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.86)),
    radial-gradient(circle at top right, rgba(0, 136, 204, 0.08), transparent 38%),
    var(--bg-card);
}

/* Explore modal */
.explore-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.explore-modal-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.explore-modal {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  max-width: 720px;
  width: 92%;
  max-height: 85vh;
  overflow-y: auto;
  padding: 0;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  position: relative;
  animation: exploreModalIn 0.2s ease-out;
}
@keyframes exploreModalIn {
  from { opacity: 0; transform: scale(0.95) translateY(10px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}
.explore-modal::-webkit-scrollbar { width: 6px; }
.explore-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.explore-modal-header {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 24px 28px 16px;
  border-bottom: 1px solid var(--border);
}
.explore-modal-heading {
  flex: 1;
  min-width: 0;
}
.explore-modal-icon { color: var(--accent, #00D4AA); }
.explore-modal-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--text);
}
.explore-modal-summary {
  font-size: 13px;
  color: var(--text-dim);
  margin-top: 2px;
}
.explore-modal-guide {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(64, 224, 255, 0.14);
  background: linear-gradient(135deg, rgba(64, 224, 255, 0.08), rgba(0, 212, 170, 0.05));
  min-width: 170px;
}
.explore-modal-guide.is-hidden {
  display: none;
}
.explore-modal-robot-sprite {
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: 10px;
  background: rgba(0, 30, 43, 0.22);
  box-shadow: inset 0 0 0 1px rgba(64, 224, 255, 0.08);
}
.explore-modal-robot-sprite svg {
  width: 32px;
  height: 32px;
}
.explore-modal-guide-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.explore-modal-guide-copy {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-dim);
  margin-top: 2px;
}
.explore-modal-close {
  position: absolute;
  top: 16px; right: 18px;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 22px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
  transition: all 0.15s;
  z-index: 1;
}
.explore-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
.explore-modal-body {
  padding: 20px 28px 24px;
  font-size: 14px;
  line-height: 1.75;
  color: var(--text);
  white-space: pre-wrap;
}
.explore-modal-links {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.explore-modal-links-title {
  font-size: 11px;
  font-weight: 600;
  color: var(--accent-text);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 6px;
}
.explore-modal-links a {
  display: block;
  color: var(--blue);
  font-size: 12px;
  word-break: break-all;
  margin-bottom: 4px;
  text-decoration: none;
}
.explore-modal-links a:hover { text-decoration: underline; }
.explore-modal-tryit {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.explore-modal-tryit-title {
  font-size: 11px;
  font-weight: 600;
  color: var(--accent-text);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 8px;
}
.explore-modal-tryit-cmd {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text-dim);
  background: var(--bg);
  padding: 6px 10px;
  border-radius: 5px;
  margin-bottom: 4px;
}
.explore-modal-actions {
  display: flex;
  gap: 8px;
  padding: 0 28px 24px;
}

/* Agent mode info modal */
.agent-info-overlay {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
  z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 0.25s ease;
}
.agent-info-overlay.open { opacity: 1; pointer-events: auto; }
.agent-info-modal {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  max-width: 520px; width: 92%;
  padding: 28px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  animation: exploreModalIn 0.2s ease-out;
}
.agent-info-modal h3 {
  margin: 0 0 16px; font-size: 16px; color: var(--text);
  display: flex; align-items: center; gap: 8px;
}
.agent-info-modal h3 .mode-icon { color: var(--accent); }
.agent-info-modal .mode-compare {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 18px;
}
.agent-info-modal .mode-card {
  background: var(--bg-input); border: 1px solid var(--border);
  border-radius: 8px; padding: 14px; font-size: 12px;
}
.agent-info-modal .mode-card.active { border-color: var(--accent); }
.agent-info-modal .mode-card h4 {
  margin: 0 0 8px; font-size: 13px; color: var(--text);
}
.agent-info-modal .mode-card ul {
  margin: 0; padding-left: 16px; color: var(--text-dim); line-height: 1.6;
}
.agent-info-modal .mode-note {
  font-size: 12px; color: var(--text-dim); margin-bottom: 18px; line-height: 1.5;
}
.agent-info-modal .mode-actions {
  display: flex; align-items: center; justify-content: space-between;
}
.agent-info-modal .mode-dismiss-label {
  font-size: 11px; color: var(--text-dim); display: flex; align-items: center; gap: 6px; cursor: pointer;
}
.agent-info-modal .mode-dismiss-label input { accent-color: var(--accent); }
.agent-info-modal .mode-ok-btn {
  background: var(--accent); color: #000; border: none;
  padding: 8px 24px; border-radius: 6px; font-size: 13px;
  font-weight: 600; cursor: pointer; font-family: var(--font);
}
.agent-info-modal .mode-ok-btn:hover { opacity: 0.9; }

/* Benchmark tab */
.bench-panels { display: flex; gap: 8px; margin-bottom: 16px; }
.bench-panel-btn {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px 18px;
  border-radius: var(--radius);
  font-size: 13px;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.2s;
}
.bench-panel-btn:hover { color: var(--text); border-color: var(--text-dim); }
.bench-panel-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); }

.bench-view { display: none; }
.bench-view.active { display: block; }

.latency-chart { margin-top: 16px; }
.latency-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.latency-model {
  font-family: var(--mono);
  font-size: 13px;
  min-width: 170px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.latency-bar-outer {
  flex: 1;
  height: 28px;
  background: var(--bg-input);
  border-radius: 4px;
  overflow: hidden;
  position: relative;
}
.latency-bar-inner {
  height: 100%;
  border-radius: 4px;
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
  display: flex;
  align-items: center;
  padding: 0 10px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--green-dark);
  font-weight: 600;
  white-space: nowrap;
}
.latency-bar-inner.running {
  background: var(--border) !important;
  width: 100% !important;
  animation: pulse-bar 1.2s ease-in-out infinite;
}
@keyframes pulse-bar {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.6; }
}
.latency-stats {
  min-width: 90px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text-dim);
  text-align: right;
}
.latency-badge {
  font-size: 14px;
  min-width: 20px;
  text-align: center;
}

/* Ranking diff */
.rank-comparison { margin-top: 16px; }
.rank-row {
  display: grid;
  grid-template-columns: 30px 1fr 40px 1fr;
  gap: 8px;
  align-items: start;
  margin-bottom: 8px;
  padding: 8px;
  background: var(--bg-input);
  border-radius: var(--radius);
}
.rank-num {
  font-size: 16px;
  font-weight: 700;
  color: var(--accent-text);
  font-family: var(--mono);
  text-align: center;
}
.rank-item {
  font-size: 13px;
  padding: 6px 10px;
  border-radius: 4px;
  border-left: 3px solid var(--border);
}
.rank-item .rank-score {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
}
.rank-match { border-left-color: var(--green); }
.rank-differ { border-left-color: var(--yellow); }
.rank-arrow { text-align: center; color: var(--text-muted); font-size: 18px; padding-top: 4px; }

/* Quantization charts */
.quant-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
@media (max-width: 768px) { .quant-charts { grid-template-columns: 1fr; } }

.quant-bar-group { margin-bottom: 14px; }
.quant-bar-label {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 4px; font-size: 13px;
}
.quant-bar-label .dtype-name { color: var(--accent-text); font-weight: 600; font-family: var(--mono); }
.quant-bar-label .dtype-value { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
.quant-bar-track {
  height: 32px; background: var(--bg-input); border-radius: 6px;
  overflow: hidden; position: relative;
}
.quant-bar-fill {
  height: 100%; border-radius: 6px;
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
  display: flex; align-items: center; padding: 0 10px;
  font-family: var(--mono); font-size: 12px; font-weight: 600;
  color: var(--green-dark); white-space: nowrap; min-width: fit-content;
}
.quant-bar-fill.storage { background: linear-gradient(90deg, #00D4AA, #5CE8CC); }
.quant-bar-fill.latency { background: linear-gradient(90deg, #40E0FF, #0088CC); }
.quant-bar-badge {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  font-size: 12px; color: var(--text-dim); font-family: var(--mono);
}

.quant-quality-meter { margin-bottom: 14px; }
.quant-meter-header {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 6px;
}
.quant-meter-header .dtype-name { color: var(--accent-text); font-weight: 600; font-family: var(--mono); font-size: 13px; }
.quant-meter-header .verdict-badge {
  font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
}
.quant-meter-header .verdict-badge.perfect { background: rgba(0,212,170,0.15); color: var(--green); }
.quant-meter-header .verdict-badge.good { background: rgba(255,217,61,0.15); color: var(--yellow); }
.quant-meter-header .verdict-badge.degraded { background: rgba(255,107,107,0.15); color: var(--red); }
.quant-meter-track {
  height: 10px; background: var(--bg-input); border-radius: 5px; overflow: hidden;
}
.quant-meter-fill {
  height: 100%; border-radius: 5px;
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.quant-meter-fill.perfect { background: linear-gradient(90deg, #00D4AA, #5CE8CC); }
.quant-meter-fill.good { background: linear-gradient(90deg, #FFC010, #FFEC9E); }
.quant-meter-fill.degraded { background: linear-gradient(90deg, #FF6960, #FFCDC7); }
.quant-meter-detail { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }

.quant-rank-cols {
  display: grid; gap: 12px;
}
.quant-rank-col-header {
  font-weight: 600; color: var(--accent-text); font-size: 13px; font-family: var(--mono);
  margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
}
.quant-rank-item {
  padding: 8px 10px; margin-bottom: 4px; border-radius: 6px;
  font-size: 12px; position: relative; border-left: 3px solid transparent;
  transition: background 0.2s;
}
.quant-rank-item:hover { background: rgba(255,255,255,0.03); }
.quant-rank-item.match { border-left-color: var(--green); background: rgba(0,212,170,0.06); }
.quant-rank-item.differ { border-left-color: var(--red); background: rgba(255,107,107,0.06); }
.quant-rank-item.baseline { border-left-color: var(--border); background: var(--bg-input); }
.quant-rank-pos {
  display: inline-block; width: 22px; height: 22px; line-height: 22px;
  text-align: center; border-radius: 50%; background: var(--bg-surface);
  color: var(--accent-text); font-weight: 700; font-size: 11px; font-family: var(--mono);
  margin-right: 8px;
}
.quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }

/* Cost calculator */
.cost-controls {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px 32px;
  margin-bottom: 20px;
}
.cost-controls-full { grid-column: 1 / -1; }
.cost-slider-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.cost-slider-label {
  font-size: 12px;
  color: var(--text-dim);
  min-width: 110px;
}
.cost-slider {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 6px;
  background: var(--bg-input);
  border-radius: 3px;
  outline: none;
}
.cost-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
  box-shadow: 0 0 8px var(--accent-glow);
}
.cost-slider-value {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--accent-text);
  min-width: 70px;
  text-align: right;
  font-weight: 600;
}
.cost-mode-toggle {
  display: flex;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  width: fit-content;
}
.cost-mode-btn {
  padding: 8px 20px;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
  transition: all 0.2s;
  font-family: var(--mono);
}
.cost-mode-btn.active {
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
}
.cost-mode-btn:hover:not(.active) {
  background: rgba(0, 212, 170, 0.1);
  color: var(--text);
}
.cost-select {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  padding: 6px 10px;
  min-width: 160px;
}
.cost-select:focus { border-color: var(--accent); outline: none; }
.cost-model-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.cost-model-label {
  font-size: 12px;
  color: var(--text-dim);
  min-width: 110px;
}
.cost-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}
.cost-summary-card {
  background: var(--bg-input);
  border-radius: 8px;
  padding: 14px 16px;
  border: 1px solid var(--border);
}
.cost-summary-label {
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}
.cost-summary-value {
  font-family: var(--mono);
  font-size: 20px;
  font-weight: 700;
  color: var(--accent-text);
}
.cost-summary-detail {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 4px;
  font-family: var(--mono);
}
.cost-strategy-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
  margin-top: 16px;
}
.cost-strategy {
  background: var(--bg-input);
  border-radius: 10px;
  padding: 18px;
  border: 1px solid var(--border);
  transition: border-color 0.2s, box-shadow 0.2s;
  position: relative;
}
.cost-strategy.recommended {
  border-color: var(--accent);
  box-shadow: 0 0 16px var(--accent-glow);
}
.cost-strategy-badge {
  position: absolute;
  top: -10px;
  right: 16px;
  background: var(--accent);
  color: var(--bg);
  font-size: 10px;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.cost-strategy-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 12px;
}
.cost-strategy-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 0;
  font-size: 12px;
}
.cost-strategy-row-label { color: var(--text-dim); }
.cost-strategy-row-value { font-family: var(--mono); color: var(--text); font-weight: 500; }
.cost-strategy-total {
  border-top: 1px solid var(--border);
  margin-top: 10px;
  padding-top: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.cost-strategy-total-label { font-size: 13px; font-weight: 600; color: var(--text); }
.cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent-text); }
.cost-savings {
  font-size: 11px;
  color: var(--success);
  font-weight: 600;
  margin-top: 6px;
  text-align: right;
}
.cost-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  margin-top: 12px;
}
.cost-table th {
  text-align: left;
  padding: 8px 12px;
  color: var(--text-dim);
  font-weight: 500;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.cost-table td {
  padding: 8px 12px;
  border-bottom: 1px solid rgba(42, 53, 80, 0.3);
  font-family: var(--mono);
}
.cost-table tr:hover { background: rgba(0, 212, 170, 0.03); }
.cost-highlight {
  color: var(--accent-text);
  font-weight: 600;
}
.cost-bar-cell { position: relative; }
.cost-bar {
  position: absolute;
  left: 0; top: 50%;
  transform: translateY(-50%);
  height: 20px;
  background: var(--accent-glow);
  border-radius: 3px;
  transition: width 0.4s ease;
}
.cost-section-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin: 20px 0 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.cost-tip {
  font-size: 12px;
  color: var(--text-muted);
  background: rgba(0, 212, 170, 0.05);
  border-left: 3px solid var(--accent);
  padding: 10px 14px;
  border-radius: 0 6px 6px 0;
  margin-top: 16px;
}
.cost-help-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  background: transparent;
  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  margin-left: 8px;
  transition: all 0.2s;
  vertical-align: middle;
  font-family: var(--mono);
  line-height: 1;
}
.cost-help-btn:hover {
  background: var(--accent);
  color: var(--bg);
  box-shadow: 0 0 10px var(--accent-glow);
}
.cost-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.cost-modal-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.cost-modal {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  max-width: 680px;
  width: 90%;
  max-height: 85vh;
  overflow-y: auto;
  padding: 32px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  position: relative;
}
.cost-modal::-webkit-scrollbar { width: 6px; }
.cost-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.cost-modal-close {
  position: absolute;
  top: 14px; right: 16px;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 22px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
  transition: all 0.15s;
}
.cost-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
.cost-modal h2 {
  font-size: 18px;
  color: var(--text);
  margin: 0 0 20px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.cost-modal h3 {
  font-size: 14px;
  color: var(--accent-text);
  margin: 22px 0 10px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.cost-modal p, .cost-modal li {
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.7;
}
.cost-modal ul { padding-left: 20px; margin: 6px 0; }
.cost-modal li { margin-bottom: 4px; }
.cost-modal code {
  background: var(--bg-input);
  padding: 2px 7px;
  border-radius: 4px;
  font-size: 12px;
  color: var(--accent-text);
  font-family: var(--mono);
}
.cost-modal .formula {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 18px;
  margin: 10px 0;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--text);
  line-height: 1.8;
}
.cost-modal .formula .label {
  color: var(--text-muted);
  font-size: 11px;
}
.cost-modal .formula .accent { color: var(--accent); font-weight: 600; }
.cost-modal .example {
  background: rgba(0, 212, 170, 0.05);
  border-left: 3px solid var(--accent);
  border-radius: 0 8px 8px 0;
  padding: 12px 16px;
  margin: 12px 0;
  font-size: 12px;
  color: var(--text-dim);
  font-family: var(--mono);
  line-height: 1.8;
}

/* History chart */
.history-empty {
  text-align: center;
  padding: 40px;
  color: var(--text-muted);
  font-size: 14px;
}
.history-chart {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 120px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.history-bar-group {
  flex: 1;
  display: flex;
  gap: 2px;
  align-items: flex-end;
  height: 100%;
  min-width: 0;
}
.history-bar {
  flex: 1;
  border-radius: 3px 3px 0 0;
  min-width: 4px;
  transition: height 0.4s ease;
  cursor: pointer;
  position: relative;
}
.history-bar:hover { opacity: 0.8; }
.history-legend {
  display: flex;
  gap: 16px;
  justify-content: center;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.history-legend-dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 2px;
  margin-right: 4px;
  vertical-align: middle;
}
.history-labels {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 4px;
}

/* About page */
.about-container { max-width: 100%; margin: 0; }
.about-header { display: flex; gap: 24px; align-items: center; margin-bottom: 24px; }
.about-avatar {
  width: 120px; height: 120px;
  border-radius: 50%;
  border: 3px solid var(--accent);
  box-shadow: 0 0 20px var(--accent-glow);
  flex-shrink: 0;
}
.about-name { font-size: 24px; font-weight: 700; color: var(--text); }
.about-role { font-size: 14px; color: var(--accent-text); margin-top: 4px; }
.about-links { display: flex; gap: 12px; margin-top: 8px; }
.about-links a {
  color: var(--text-dim);
  font-size: 13px;
  text-decoration: none;
  transition: color 0.2s;
}
.about-links a:hover { color: var(--accent); }
.about-section { margin-bottom: 24px; }
.about-section-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-text);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 8px;
}
.about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
.about-changelog { font-size: 13px; line-height: 1.6; }
.about-changelog details {
  border-bottom: 1px solid rgba(255,255,255,0.06);
  padding: 0;
}
.about-changelog details:last-child { border-bottom: none; }
.about-changelog summary {
  padding: 10px 0;
  cursor: pointer;
  color: var(--text);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
}
.about-changelog summary::before {
  content: '';
  display: inline-block;
  width: 5px; height: 5px;
  border-right: 1.5px solid var(--accent, #00D4AA);
  border-bottom: 1.5px solid var(--accent, #00D4AA);
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
  flex-shrink: 0;
}
.about-changelog details[open] > summary::before {
  transform: rotate(45deg);
}
.about-changelog summary::-webkit-details-marker { display: none; }
.about-changelog summary:hover { color: var(--accent-text); }
.about-changelog p {
  margin: 0 0 10px 13px;
  color: var(--text-muted);
  font-size: 12.5px;
  line-height: 1.6;
}
[data-theme="light"] .about-changelog details { border-color: rgba(0,0,0,0.08); }
.about-text a { color: var(--blue); text-decoration: none; }
.about-text a:hover { text-decoration: underline; }
.about-disclaimer {
  background: rgba(255, 215, 61, 0.08);
  border: 1px solid rgba(255, 215, 61, 0.2);
  border-radius: var(--radius);
  padding: 16px 20px;
  margin-top: 24px;
}
.about-disclaimer-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--warning);
  margin-bottom: 6px;
}
.about-disclaimer-text {
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-dim);
}

/* ── Page Headers ── */
.page-header {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.explore-page-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 20px;
  align-items: end;
  padding-right: 96px;
}
.explore-header-copy {
  min-width: 0;
}
.explore-header-guide {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  align-self: center;
  padding: 12px 14px;
  min-width: 220px;
  border-radius: 14px;
  border: 1px solid rgba(64, 224, 255, 0.14);
  background:
    linear-gradient(135deg, rgba(64, 224, 255, 0.08), rgba(0, 212, 170, 0.05)),
    var(--bg-card);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.explore-header-guide.is-hidden {
  display: none;
}
.explore-header-robot {
  width: 54px;
  height: 54px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: 12px;
  background: rgba(0, 30, 43, 0.2);
  box-shadow: inset 0 0 0 1px rgba(64, 224, 255, 0.10);
}
.explore-header-robot svg {
  width: 42px;
  height: 42px;
}
.explore-header-guide-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.explore-header-guide-text {
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
  margin-top: 2px;
  max-width: 160px;
}
.page-header-title {
  font-size: 22px;
  font-weight: 700;
  color: var(--accent-text, var(--text));
  margin: 0 0 4px;
  letter-spacing: -0.3px;
}
.page-header-subtitle {
  font-size: 14px;
  color: var(--text-dim);
  margin: 0 0 6px;
  font-weight: 500;
}
.page-header-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.5;
}

/* ── Contextual docs link ── */
.page-header { position: relative; }
.page-header-docs {
  position: absolute; top: 0; right: 0;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 500;
  color: var(--text-muted); text-decoration: none;
  padding: 4px 10px; border-radius: 6px;
  border: 1px solid var(--border);
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.page-header-docs:hover {
  color: var(--accent, #6c63ff); border-color: var(--accent, #6c63ff);
  background: rgba(108,99,255,0.06);
}
.page-header-docs svg { opacity: 0.6; }
.page-header-docs:hover svg { opacity: 1; }
[data-theme="light"] .explore-header-guide,
[data-theme="light"] .explore-modal-guide {
  border-color: rgba(0, 136, 204, 0.16);
  background: linear-gradient(135deg, rgba(0, 136, 204, 0.08), rgba(0, 158, 128, 0.05));
}
[data-theme="light"] .explore-header-robot,
[data-theme="light"] .explore-modal-robot-sprite {
  background: rgba(255, 255, 255, 0.7);
  box-shadow: inset 0 0 0 1px rgba(0, 136, 204, 0.10);
}
@media (max-width: 900px) {
  .explore-page-header {
    grid-template-columns: 1fr;
    padding-right: 96px;
  }
  .explore-header-guide {
    min-width: 0;
    width: 100%;
    max-width: 320px;
  }
  .explore-modal-guide {
    min-width: 0;
  }
}
@media (max-width: 680px) {
  .explore-page-header {
    padding-right: 0;
  }
  .explore-header-guide {
    max-width: none;
  }
  .explore-modal-header {
    flex-wrap: wrap;
  }
  .explore-modal-guide {
    width: 100%;
  }
}
.sidebar-docs-link {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10px; color: var(--text-muted); text-decoration: none;
  padding: 2px 6px; border-radius: 4px;
  transition: color 0.15s, background 0.15s;
}
.sidebar-docs-link:hover {
  color: var(--accent, #6c63ff); background: rgba(108,99,255,0.08);
}
.sidebar-docs-link svg { width: 12px; height: 12px; }

/* ── Settings page ── */
.settings-container { max-width: 640px; margin: 0 auto; }
.settings-section {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
  margin-bottom: 20px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
[data-theme="light"] .settings-section {
  box-shadow: 0 1px 4px rgba(0,30,43,0.07);
  border-color: #DDE4E2;
}
.settings-section-title {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: 16px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
/* API key alert banner */
.settings-api-banner {
  background: rgba(255,192,16,0.08);
  border: 1px solid rgba(255,192,16,0.25);
  border-radius: var(--radius);
  padding: 14px 18px;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--warning);
  line-height: 1.5;
}
.settings-api-banner.success {
  background: rgba(0,212,170,0.06);
  border-color: rgba(0,212,170,0.2);
  color: var(--success);
}
[data-theme="light"] .settings-api-banner {
  background: rgba(148,79,1,0.06);
  border-color: rgba(148,79,1,0.2);
}
[data-theme="light"] .settings-api-banner.success {
  background: rgba(0,104,74,0.06);
  border-color: rgba(0,104,74,0.2);
}
.settings-api-banner-icon { font-size: 20px; flex-shrink: 0; }
/* Heatmap preview swatches */
.heatmap-preview {
  display: inline-block;
  width: 80px;
  height: 12px;
  border-radius: 3px;
  vertical-align: middle;
  margin-left: 8px;
  border: 1px solid var(--border);
}
.heatmap-opt {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* Get a key link as button */
.settings-key-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--blue);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  padding: 4px 10px;
  border: 1px solid var(--blue);
  border-radius: 4px;
  transition: all 0.15s;
  white-space: nowrap;
}
.settings-key-link:hover {
  background: rgba(4,152,236,0.1);
  text-decoration: none;
}
/* API key field integration */
.settings-api-field {
  display: flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-input);
  overflow: hidden;
  transition: border-color 0.2s;
}
.settings-api-field:focus-within { border-color: var(--accent); }
.settings-api-field input {
  flex: 1;
  border: none;
  background: transparent;
  padding: 8px 12px;
  font-size: 13px;
  font-family: var(--mono);
  color: var(--text);
  min-width: 0;
}
.settings-api-field input:focus { outline: none; }
.settings-api-field button {
  background: none;
  border: none;
  border-left: 1px solid var(--border);
  padding: 8px 10px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text-dim);
  transition: all 0.15s;
  flex-shrink: 0;
}
.settings-api-field button:hover { color: var(--text); background: rgba(255,255,255,0.05); }
[data-theme="light"] .settings-api-field button:hover { background: rgba(0,0,0,0.03); }
/* Version badges */
.version-badge {
  display: inline-block;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  background: rgba(0,212,170,0.08);
  border: 1px solid rgba(0,212,170,0.2);
  border-radius: 6px;
  padding: 4px 12px;
  letter-spacing: 0.3px;
}
[data-theme="light"] .version-badge {
  background: rgba(0,104,74,0.06);
  border-color: rgba(0,104,74,0.2);
  color: #007A63;
}
.settings-api-field .save-btn {
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  font-family: var(--font);
  padding: 8px 14px;
}
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.settings-row:last-child { border-bottom: none; }
[data-theme="light"] .settings-row { border-bottom-color: rgba(0,0,0,0.04); }
.settings-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.settings-label-text {
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
}
.settings-label-hint {
  font-size: 12px;
  color: var(--text-muted);
}
.settings-control { flex-shrink: 0; }
.settings-select {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  border-radius: var(--radius);
  font-size: 13px;
  font-family: var(--mono);
  cursor: pointer;
  min-width: 180px;
}
.settings-select:focus { outline: none; border-color: var(--accent); }
.settings-input {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  border-radius: var(--radius);
  font-size: 13px;
  font-family: var(--mono);
  min-width: 180px;
}
.settings-input:focus { outline: none; border-color: var(--accent); }
.settings-toggle {
  position: relative;
  width: 44px;
  height: 24px;
  background: var(--border);
  border-radius: 12px;
  border: none;
  cursor: pointer;
  transition: background 0.2s;
  padding: 0;
}
.settings-toggle.active { background: var(--accent); }
.settings-toggle::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  background: white;
  border-radius: 50%;
  transition: transform 0.2s;
}
.settings-toggle.active::after { transform: translateX(20px); }
.settings-saved {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--success);
  opacity: 0;
  transition: opacity 0.3s;
}
.settings-saved.show { opacity: 1; }
.doctor-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--bg-input);
  border: 1px solid var(--border);
}
.doctor-check-icon { font-size: 15px; flex-shrink: 0; line-height: 1.4; }
.doctor-check-body { flex: 1; min-width: 0; }
.doctor-check-name { font-size: 13px; font-weight: 600; color: var(--text); }
.doctor-check-msg { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.doctor-check-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
.doctor-check-badge {
  font-size: 10px;
  padding: 2px 8px;
  border-radius: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  flex-shrink: 0;
  align-self: center;
}
.doctor-check-badge.pass { background: rgba(0,212,170,0.12); color: var(--success); }
.doctor-check-badge.fail { background: rgba(255,105,96,0.12); color: var(--error); }
.doctor-check-badge.warn { background: rgba(255,192,16,0.12); color: var(--warning); }
.doctor-check-badge.optional { background: rgba(136,147,151,0.12); color: var(--text-muted); }
.settings-reset-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px 16px;
  border-radius: var(--radius);
  font-size: 13px;
  cursor: pointer;
  transition: all 0.2s;
}
.settings-reset-btn:hover { border-color: var(--error); color: var(--error); }
.settings-toggle-vis {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 8px;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  transition: all 0.2s;
  color: var(--text-dim);
}
.settings-toggle-vis:hover { border-color: var(--accent); }
.settings-api-key-active { color: var(--success); }
.settings-api-key-missing { color: var(--warning); }
.settings-row .settings-select, .settings-row .settings-input { text-align: right; }

/* ── Settings two-column layout (Claude Desktop style) ── */
.settings-layout {
  display: flex;
  height: 100%;
  max-width: 960px;
  margin: 0 auto;
  gap: 0;
}
.settings-nav {
  width: 200px;
  min-width: 200px;
  flex-shrink: 0;
  padding: 24px 0;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: 0;
  align-self: flex-start;
}
.settings-nav-header {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  padding: 0 16px 20px;
  letter-spacing: -0.3px;
}
.settings-nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 16px;
  background: none;
  border: none;
  border-left: 3px solid transparent;
  border-radius: 0 6px 6px 0;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
  font-family: var(--font);
  cursor: pointer;
  transition: all 0.15s;
  text-align: left;
}
.settings-nav-item:hover {
  background: rgba(255,255,255,0.04);
  color: var(--text);
}
[data-theme="light"] .settings-nav-item:hover {
  background: rgba(0,30,43,0.04);
}
.settings-nav-item.active {
  color: var(--accent);
  border-left-color: var(--accent);
  background: var(--accent-glow);
}
.settings-nav-item svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: inherit;
}
.settings-content {
  flex: 1;
  padding: 24px 32px;
  overflow-y: auto;
  max-width: 640px;
}
.settings-panel {
  display: none;
}
.settings-panel.active {
  display: block;
}
.settings-panel-header {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.settings-panel-title {
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 4px;
  letter-spacing: -0.3px;
}
.settings-panel-subtitle {
  font-size: 14px;
  color: var(--text-dim);
  margin: 0;
  font-weight: 500;
}
/* Settings origin/provenance badge */
.settings-origin {
  display: inline-flex;
  align-items: center;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 2px 7px;
  border-radius: 4px;
  margin-left: 6px;
  white-space: nowrap;
  vertical-align: middle;
  line-height: 1;
}
.settings-origin:empty { display: none; }
.settings-origin.origin-env {
  background: rgba(4,152,236,0.12);
  color: #0497EC;
}
[data-theme="light"] .settings-origin.origin-env {
  background: rgba(4,120,190,0.10);
  color: #0088CC;
}
.settings-origin.origin-config {
  background: rgba(180,90,242,0.12);
  color: #B45AF2;
}
[data-theme="light"] .settings-origin.origin-config {
  background: rgba(140,60,200,0.10);
  color: #7B3DB5;
}
.settings-origin.origin-project {
  background: rgba(0,212,170,0.12);
  color: var(--accent);
}
[data-theme="light"] .settings-origin.origin-project {
  background: rgba(0,158,128,0.10);
  color: #009E80;
}
.settings-origin.origin-default {
  background: rgba(136,147,151,0.12);
  color: var(--text-muted);
}
/* MCP integration cards */
.mcp-card { display:flex; align-items:center; gap:14px; padding:14px 16px; background:var(--bg-surface); border:1px solid var(--border); border-radius:var(--radius); transition:border-color 0.15s; flex-wrap:wrap; }
.mcp-card:hover { border-color:var(--accent); }
.mcp-card-icon { width:36px; height:36px; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:18px; flex-shrink:0; background:rgba(0,212,170,0.08); }
.mcp-card-info { flex:1; min-width:80px; }
.mcp-card-name { font-size:13px; font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mcp-card-path { font-size:11px; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:2px; }
.mcp-card-actions { flex-shrink:0; display:flex; align-items:center; gap:8px; }
.mcp-card-status { display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; padding:3px 10px; border-radius:4px; white-space:nowrap; }
.mcp-card-status.installed { background:rgba(0,212,170,0.10); color:var(--success); }
.mcp-card-status.not-installed { background:rgba(136,147,151,0.10); color:var(--text-muted); }
.mcp-install-btn { padding:5px 14px; font-size:12px; font-weight:600; border-radius:5px; border:1px solid var(--accent); background:transparent; color:var(--accent); cursor:pointer; font-family:var(--font); transition:all 0.15s; }
.mcp-install-btn:hover { background:var(--accent); color:var(--bg); }
.mcp-install-btn.uninstall { border-color:var(--error); color:var(--error); }
.mcp-install-btn.uninstall:hover { background:var(--error); color:#fff; }
.mcp-install-btn:disabled { opacity:0.5; pointer-events:none; }
/* Chat not-configured state */
.chat-not-configured {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 48px 24px;
  text-align: center;
  color: var(--text-dim);
  font-size: 14px;
}
.chat-not-configured-icon {
  font-size: 40px;
  opacity: 0.5;
}
.chat-not-configured .btn {
  margin-top: 8px;
  background: var(--accent);
  color: var(--bg);
  border: none;
  padding: 8px 20px;
  border-radius: var(--radius);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  font-family: var(--font);
}
.chat-not-configured .btn:hover {
  filter: brightness(1.1);
}
/* Responsive: collapse settings nav on small screens */
@media (max-width: 768px) {
  .settings-layout {
    flex-direction: column;
  }
  .settings-nav {
    width: 100%;
    min-width: 100%;
    flex-direction: row;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 12px 8px;
    gap: 4px;
    position: static;
  }
  .settings-nav-header { display: none; }
  .settings-nav-item {
    border-left: none;
    border-bottom: 2px solid transparent;
    border-radius: 6px;
    padding: 8px 12px;
    white-space: nowrap;
    font-size: 12px;
  }
  .settings-nav-item.active {
    border-left-color: transparent;
    border-bottom-color: var(--accent);
  }
  .settings-content { padding: 16px; }
}

/* ── Chat Tab ── */
/* (chat-container flex layout is defined in the KB Panel section below) */
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.chat-message {
  max-width: 85%;
  padding: 12px 16px;
  border-radius: 12px;
  line-height: 1.6;
  font-size: 14px;
  white-space: pre-wrap;
  word-break: break-word;
}
.chat-message.user {
  align-self: flex-end;
  background: var(--accent);
  color: #fff;
  border-bottom-right-radius: 4px;
}
.chat-message.assistant {
  align-self: flex-start;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-bottom-left-radius: 4px;
  position: relative;
}
.chat-copy-btn {
  position: absolute; top: 6px; right: 6px;
  width: 28px; height: 28px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.15s, background 0.15s, color 0.15s;
  pointer-events: none; z-index: 1;
}
.chat-message.assistant:hover .chat-copy-btn {
  opacity: 1; pointer-events: auto;
}
.chat-copy-btn:hover {
  background: var(--bg-card); color: var(--text); border-color: var(--text-muted);
}
.chat-copy-btn.copied {
  color: #69F0AE; border-color: #69F0AE;
}
.chat-message.system-msg {
  align-self: center;
  background: none;
  color: var(--text-muted);
  font-size: 12px;
  padding: 4px 8px;
}
.chat-sources {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
}
.chat-sources summary { cursor: pointer; }
.chat-sources ul { margin: 4px 0 0 16px; padding: 0; }
.chat-sources li { margin: 2px 0; }
/* Markdown rendering in assistant messages */
.chat-message.assistant .chat-message-content.rendered {
  white-space: normal;
}
.chat-message.assistant .chat-message-content.rendered p {
  margin: 0 0 8px 0;
}
.chat-message.assistant .chat-message-content.rendered p:last-child {
  margin-bottom: 0;
}
.chat-message.assistant .chat-message-content.rendered h1,
.chat-message.assistant .chat-message-content.rendered h2,
.chat-message.assistant .chat-message-content.rendered h3,
.chat-message.assistant .chat-message-content.rendered h4 {
  margin: 12px 0 6px 0;
  font-weight: 600;
  color: var(--accent-text, #fff);
}
.chat-message.assistant .chat-message-content.rendered h1 { font-size: 1.3em; }
.chat-message.assistant .chat-message-content.rendered h2 { font-size: 1.15em; }
.chat-message.assistant .chat-message-content.rendered h3 { font-size: 1.05em; }
.chat-message.assistant .chat-message-content.rendered code {
  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
  font-size: 0.9em;
  background: var(--bg-input, #112733);
  padding: 2px 5px;
  border-radius: 4px;
}
.chat-message.assistant .chat-message-content.rendered pre {
  background: var(--bg-input, #112733);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  margin: 8px 0;
  overflow-x: auto;
  white-space: pre;
}
.chat-message.assistant .chat-message-content.rendered pre code {
  background: none;
  padding: 0;
  border-radius: 0;
  font-size: 13px;
  line-height: 1.5;
}
.chat-message.assistant .chat-message-content.rendered ul,
.chat-message.assistant .chat-message-content.rendered ol {
  margin: 6px 0;
  padding-left: 20px;
}
.chat-message.assistant .chat-message-content.rendered li {
  margin: 3px 0;
}
.chat-message.assistant .chat-message-content.rendered blockquote {
  border-left: 3px solid var(--accent, #00D4AA);
  margin: 8px 0;
  padding: 4px 12px;
  color: var(--text-muted);
}
.chat-message.assistant .chat-message-content.rendered a {
  color: var(--accent, #00D4AA);
  text-decoration: none;
}
.chat-message.assistant .chat-message-content.rendered a:hover {
  text-decoration: underline;
}
.chat-message.assistant .chat-message-content.rendered table {
  border-collapse: collapse;
  margin: 8px 0;
  font-size: 13px;
  width: 100%;
}
.chat-message.assistant .chat-message-content.rendered th,
.chat-message.assistant .chat-message-content.rendered td {
  border: 1px solid var(--border);
  padding: 6px 10px;
  text-align: left;
}
.chat-message.assistant .chat-message-content.rendered th {
  background: var(--bg-input, #112733);
  font-weight: 600;
}
.chat-message.assistant .chat-message-content.rendered hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 12px 0;
}

/* Agent thinking panel */
.chat-thinking {
  align-self: flex-start;
  max-width: 85%;
  margin-bottom: 2px;
  font-size: 13px;
}
.chat-thinking summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 10px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 13px;
  transition: background 0.15s, border-color 0.15s;
  user-select: none;
}
.chat-thinking summary::-webkit-details-marker { display: none; }
.chat-thinking summary:hover {
  background: var(--bg-surface);
  border-color: var(--accent-dim, #009E80);
}
.chat-thinking[open] summary {
  border-radius: 10px 10px 0 0;
  border-bottom-color: transparent;
}
.chat-thinking .thinking-icon {
  font-size: 14px;
  line-height: 1;
  display: flex;
  align-items: center;
}
.chat-thinking .thinking-icon svg { color: var(--accent); }
.chat-thinking .thinking-label {
  font-weight: 500;
}
.chat-thinking .thinking-chevron {
  margin-left: auto;
  font-size: 10px;
  opacity: 0.5;
  transition: transform 0.15s;
}
.chat-thinking[open] .thinking-chevron {
  transform: rotate(90deg);
}
.chat-thinking .thinking-count {
  background: var(--bg-input, #112733);
  color: var(--text-muted);
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 10px;
  font-weight: 600;
}
.chat-thinking .thinking-elapsed {
  font-size: 11px;
  opacity: 0.45;
}
.chat-thinking .thinking-timeline {
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 10px 10px;
  padding: 10px 14px;
  background: var(--bg-card);
}
.thinking-step {
  display: flex;
  gap: 10px;
  padding: 7px 0;
  position: relative;
  animation: thinkingSlideIn 0.25s ease-out;
}
@keyframes thinkingSlideIn {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.thinking-step + .thinking-step {
  border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.thinking-step-icon {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  flex-shrink: 0;
  background: var(--bg-input, #112733);
  border: 1px solid var(--border);
}
.thinking-step-icon svg { color: var(--text-dim); }
.thinking-step.active .thinking-step-icon {
  border-color: var(--accent);
  animation: thinkingPulse 1.2s ease-in-out infinite;
}
@keyframes thinkingPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0.25); }
  50% { box-shadow: 0 0 0 6px rgba(0, 212, 170, 0); }
}
.thinking-step.error .thinking-step-icon {
  border-color: #e74c3c;
  background: rgba(231, 76, 60, 0.08);
}
.thinking-step.done .thinking-step-icon {
  border-color: var(--accent-dim, #009E80);
  opacity: 0.7;
}
.thinking-step-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}
.thinking-step-header {
  display: flex;
  align-items: center;
  gap: 6px;
}
.thinking-step-name {
  font-weight: 600;
  color: var(--text);
  font-size: 12.5px;
}
.thinking-step-desc {
  font-size: 12px;
  color: var(--text-muted);
  opacity: 0.7;
}
.thinking-step-time {
  font-size: 11px;
  opacity: 0.45;
  margin-left: auto;
  white-space: nowrap;
}
.thinking-step-detail {
  font-size: 11.5px;
  color: var(--text-muted);
  margin-top: 2px;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.thinking-step-detail code {
  font-size: 11px;
  background: var(--bg-input, #112733);
  padding: 1px 4px;
  border-radius: 3px;
}
.chat-input-area {
  padding: 12px 16px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 8px;
  align-items: flex-end;
  flex-shrink: 0;
}
.chat-input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-input);
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  resize: none;
  min-height: 40px;
  max-height: 120px;
  outline: none;
}
.chat-input:focus { border-color: var(--accent); }
.chat-send-btn {
  padding: 10px 20px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  color: var(--text-muted);
  flex-shrink: 0;
}
.chat-header-status { display: flex; gap: 8px; align-items: center; }
.chat-header-status span { opacity: 0.7; }
.chat-config-toggle {
  background: none; border: none; color: var(--text-muted);
  cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 14px;
  display: flex; align-items: center; gap: 4px;
}
.chat-config-toggle:hover { background: rgba(255,255,255,0.06); color: var(--text); }
[data-theme="light"] .chat-config-toggle:hover { background: rgba(0,0,0,0.04); }
.chat-config-toggle.active { color: var(--accent); }
/* chat-config-panel/bar removed — chat settings moved to Settings page */
.chat-kb-toggle {
  background: none; border: none; color: var(--text-muted);
  cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 13px;
  display: flex; align-items: center; gap: 4px;
  transition: color 0.15s, background 0.15s;
}
.chat-kb-toggle:hover { background: rgba(255,255,255,0.06); color: var(--text); }
[data-theme="light"] .chat-kb-toggle:hover { background: rgba(0,0,0,0.04); }
.chat-kb-toggle.active { color: var(--accent); }
.chat-typing {
  align-self: flex-start;
  color: var(--text-muted);
  font-size: 13px;
  padding: 8px 16px;
}
.chat-typing::after {
  content: '';
  animation: chatDots 1.4s infinite;
}
@keyframes chatDots {
  0%, 20% { content: '.'; }
  40% { content: '..'; }
  60%, 100% { content: '...'; }
}

/* ── Turn State Indicator ── */
.chat-state-indicator {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  margin: 4px 16px;
  font-size: 0.82em;
  color: var(--text-dim);
  border-radius: var(--radius);
  background: var(--bg-surface);
  transition: opacity 0.2s;
}
.chat-state-indicator.active {
  display: flex;
}
.chat-state-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: statePulse 1s ease-in-out infinite;
}
@keyframes statePulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* ── Memory Usage Bar ── */
.chat-memory-bar {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 4px 16px;
  margin: 0 16px 4px;
  font-size: 0.75em;
  color: var(--text-muted);
}
.chat-memory-bar.active {
  display: flex;
}
.chat-memory-bar-track {
  flex: 1;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}
.chat-memory-bar-fill {
  height: 100%;
  border-radius: 2px;
  background: var(--accent);
  transition: width 0.3s ease;
}
.chat-memory-bar-fill.warning { background: var(--warning, #f59e0b); }
.chat-memory-bar-fill.danger { background: var(--error, #ef4444); }

/* ── Multimodal Tab ── */
.mm-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.mm-drop-zone {
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  padding: 40px 24px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
  background: var(--bg-input);
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.mm-drop-zone:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.mm-drop-zone.drag-active {
  border-color: var(--accent);
  background: var(--accent-glow);
  box-shadow: 0 0 0 4px var(--accent-glow);
}
.mm-drop-zone .mm-drop-icon {
  font-size: 40px;
  opacity: 0.6;
}
.mm-drop-zone .mm-drop-text {
  color: var(--text-dim);
  font-size: 14px;
}
.mm-drop-zone .mm-drop-hint {
  color: var(--text-muted);
  font-size: 12px;
}
.mm-preview {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px;
}
.mm-preview.visible { display: flex; }
.mm-preview img {
  max-height: 300px;
  max-width: 100%;
  object-fit: contain;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.mm-preview .mm-file-info {
  font-size: 12px;
  color: var(--text-muted);
  font-family: var(--mono);
}
.mm-preview .mm-clear-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 4px 14px;
  font-size: 12px;
  cursor: pointer;
  font-family: var(--font);
  transition: border-color 0.15s, color 0.15s;
}
.mm-preview .mm-clear-btn:hover {
  border-color: var(--error);
  color: var(--error);
}
.mm-gallery-section {
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}
.mm-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
.mm-gallery-slot {
  aspect-ratio: 1;
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
  background: var(--bg-input);
  position: relative;
  overflow: hidden;
}
.mm-gallery-slot:hover {
  border-color: var(--accent);
  background: var(--accent-glow);
}
.mm-gallery-slot.filled {
  border-style: solid;
  cursor: default;
  padding: 0;
}
.mm-gallery-slot.filled img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: calc(var(--radius) - 2px);
}
.mm-gallery-slot .mm-slot-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  background: rgba(0,0,0,0.6);
  color: #fff;
  border: none;
  border-radius: 50%;
  width: 22px;
  height: 22px;
  font-size: 13px;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.mm-gallery-slot.filled:hover .mm-slot-remove { display: flex; }
.mm-gallery-slot .mm-slot-add {
  font-size: 28px;
  color: var(--text-muted);
  pointer-events: none;
}
.mm-gallery-slot.query-selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.mm-result-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.mm-result-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.15s;
}
.mm-result-item:first-child {
  border-color: var(--accent);
  background: var(--accent-glow);
}
.mm-result-item .mm-result-rank {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 18px;
  color: var(--text-muted);
  min-width: 28px;
  text-align: center;
}
.mm-result-item:first-child .mm-result-rank { color: var(--accent); }
.mm-result-item .mm-result-thumb {
  width: 48px;
  height: 48px;
  border-radius: 6px;
  object-fit: cover;
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.mm-result-item .mm-result-content {
  flex: 1;
  min-width: 0;
}
.mm-result-item .mm-result-text {
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mm-result-item .mm-result-type {
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.mm-result-item .mm-result-score {
  font-family: var(--mono);
  font-weight: 600;
  font-size: 14px;
  flex-shrink: 0;
}
.mm-search-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 12px;
}
.mm-search-row input[type="text"] {
  flex: 1;
}
.mm-search-mode {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}
.mm-search-mode button {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 4px 12px;
  font-size: 12px;
  cursor: pointer;
  font-family: var(--font);
  transition: all 0.15s;
}
.mm-search-mode button.active {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-glow);
}

@media (max-width: 768px) {
  .mm-grid { grid-template-columns: 1fr; }
  .mm-gallery-grid { grid-template-columns: repeat(3, 1fr); }
  .compare-grid, .search-results { grid-template-columns: 1fr; }
  .sidebar { width: 72px; min-width: 72px; }
  .sidebar-brand-copy, .sidebar-search-btn, .sidebar-panel, .status-label, .sidebar-bug-label { display: none; }
  .sidebar-drag-region { justify-content: center; padding: 16px 0 12px; min-height: auto; }
  .sidebar-rail { width: 100%; min-width: 0; border-right: none; padding: 8px 6px; }
  .sidebar-rail-btn { padding: 10px 0; }
  .sidebar-rail-btn-label { display: none; }
  .sidebar-footer { padding: 12px 0 56px; align-items: center; border-top: none; }
  .sidebar-footer > div:last-child, .theme-toggle { display: none; }
  .main { padding: 16px; }
  .settings-row { flex-direction: column; align-items: flex-start; gap: 8px; }
  .settings-select, .settings-input { min-width: 100%; }
}

/* ========== WORKFLOW VISUALIZER ========== */
/* Expand .main when workflows tab is active to fill viewport */
.main:has(#tab-workflows.active) {
  max-width: none; padding: 0; overflow: hidden;
}
#tab-workflows.tab-panel.active {
  display: flex; flex-direction: column;
  height: 100%;
  /* Fill entire .main which is flex:1 inside the 100vh app-shell */
}
.wf-container {
  display: flex; flex: 1; gap: 0; overflow: hidden;
  border-radius: 0;
  background: var(--bg-card);
  min-height: 0;
}
.wf-library {
  width: 220px; min-width: 0; flex-shrink: 0;
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
  background: var(--bg);
  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  overflow: hidden;
}
.wf-library.collapsed {
  width: 0; border-right: none;
}
.wf-library-header {
  padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
  color: var(--text); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
}
.wf-library-list {
  flex: 1; overflow-y: auto; padding: 8px;
}
.wf-library-footer {
  padding: 8px; border-top: 1px solid var(--border);
}
.wf-load-file-btn {
  width: 100%; padding: 7px 12px; border-radius: 6px;
  border: 1px dashed var(--border); background: transparent;
  color: var(--text-muted); cursor: pointer; font-size: 12px;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.wf-load-file-btn:hover {
  color: var(--text); border-color: var(--text-muted); background: var(--bg-card);
}
.wf-library-item {
  padding: 10px 12px; border-radius: 8px; cursor: pointer;
  margin-bottom: 4px; transition: background 0.15s;
  border: 1px solid transparent; position: relative;
}
.wf-library-item:hover { background: var(--bg-card); }
.wf-library-item.active {
  background: var(--bg-card); border-color: var(--accent, #6c63ff);
}
.wf-library-item-name {
  font-weight: 600; font-size: 13px; color: var(--text);
  margin-bottom: 2px;
}
.wf-library-item-desc {
  font-size: 11px; color: var(--text-muted);
  line-height: 1.3; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.wf-library-item-meta {
  font-size: 10px; color: var(--text-muted); margin-top: 4px;
  display: flex; gap: 8px;
}
/* Collapsible examples section */
.wf-library-section { padding: 0; margin-top: 4px; }
.wf-library-section-toggle {
  display: flex; align-items: center; gap: 6px;
  width: 100%; padding: 8px 12px; border: none; background: none;
  color: var(--text-muted); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;
  transition: color 0.15s;
}
.wf-library-section-toggle:hover { color: var(--text); }
.wf-library-section-toggle .arrow {
  font-size: 8px; transition: transform 0.2s; display: inline-block;
}
.wf-library-section-toggle.open .arrow { transform: rotate(90deg); }
.wf-library-category {
  font-size: 10px; font-weight: 600; color: var(--text-muted);
  padding: 8px 12px 2px; text-transform: uppercase; letter-spacing: 0.5px;
}
/* ── Local Inference Tab ── */
.nano-status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-top: 8px;
}
.nano-status-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-radius: 8px;
  background: rgba(0, 212, 170, 0.04);
  border: 1px solid rgba(0, 212, 170, 0.1);
}
.nano-status-item.ok .nano-status-dot { background: var(--success); }
.nano-status-item.fail .nano-status-dot { background: var(--error, #ff6b6b); }
.nano-status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}
.nano-status-label { font-weight: 500; }
.nano-status-detail { font-size: 0.85em; opacity: 0.7; margin-left: auto; }
.nano-status-loading { color: var(--text-secondary); font-style: italic; }
.nano-setup-banner {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px;
  margin-top: 16px;
  border-radius: 8px;
  background: rgba(255, 107, 107, 0.08);
  border: 1px solid rgba(255, 107, 107, 0.2);
}
.nano-setup-banner code {
  background: rgba(0, 212, 170, 0.15);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 0.9em;
}
.nano-setup-banner p { margin: 4px 0 0; opacity: 0.85; }
.nano-ready-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  margin-top: 16px;
  border-radius: 8px;
  background: rgba(0, 212, 170, 0.08);
  border: 1px solid rgba(0, 212, 170, 0.2);
  color: var(--success);
  font-weight: 500;
}
.nano-sim-heatmap {
  display: grid;
  gap: 2px;
  margin-top: 12px;
  overflow-x: auto;
}
.nano-sim-cell {
  min-width: 60px;
  text-align: center;
  border-radius: 4px;
  font-size: 0.8em;
  padding: 8px 4px;
  color: #fff;
  font-weight: 500;
}
.nano-sim-label {
  max-width: 80px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.75em;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 2px;
}
.nano-sim-corner {
  min-width: 60px;
}
.nano-sim-highlights {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 12px;
}
.nano-sim-highlight-card {
  flex: 1;
  min-width: 200px;
  padding: 12px;
  border-radius: 8px;
  background: var(--bg-card, #1a1a2e);
  border-left: 4px solid var(--success);
}
.nano-sim-highlight-card.lowest {
  border-left-color: var(--error, #ff6b6b);
}
.nano-sim-highlight-card .highlight-title {
  font-weight: 600;
  font-size: 0.85em;
  margin-bottom: 6px;
}
.nano-sim-highlight-card .highlight-score {
  font-size: 1.2em;
  font-weight: 700;
  margin-bottom: 6px;
}
.nano-sim-highlight-card .highlight-texts {
  font-size: 0.8em;
  color: var(--text-secondary);
  line-height: 1.4;
}
.nano-sim-highlight-card .highlight-texts span {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 250px;
}
.nano-dim-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}
.nano-dim-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
}
.nano-dim-card-title {
  font-weight: 600;
  font-size: 1.1em;
  margin-bottom: 8px;
  color: var(--accent-text);
}
.nano-dim-stat-row {
  display: flex;
  justify-content: space-between;
  padding: 4px 0;
  font-size: 0.85em;
  border-bottom: 1px solid var(--border);
}
.nano-dim-stat-label {
  color: var(--text-muted);
}
.nano-dim-stat-value {
  font-family: var(--mono);
  color: var(--text);
}
.nano-dim-preservation {
  margin-top: 8px;
  padding: 8px;
  border-radius: 4px;
  text-align: center;
  font-weight: 600;
}
.nano-dim-preservation.high {
  background: rgba(var(--success-rgb, 72, 199, 142), 0.15);
  color: var(--success);
}
.nano-dim-preservation.mid {
  background: rgba(var(--warning-rgb, 255, 193, 7), 0.15);
  color: var(--warning);
}
.nano-dim-preservation.low {
  background: rgba(var(--error-rgb, 255, 107, 107), 0.15);
  color: var(--error, #ff6b6b);
}
.nano-dim-baseline-tag {
  background: var(--accent);
  color: var(--bg);
  font-size: 0.7em;
  padding: 2px 6px;
  border-radius: 3px;
  margin-left: 8px;
  vertical-align: middle;
}

/* ── Cross-Bridge Comparison ── */
.nano-xbridge-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px;
  margin-top: 12px;
}
.nano-xbridge-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
}
.nano-xbridge-card .xbridge-source {
  font-weight: bold;
  margin-bottom: 8px;
}
.nano-xbridge-card .xbridge-model {
  font-size: 0.85em;
  color: var(--text-muted);
}
.nano-xbridge-card .xbridge-latency {
  font-size: 0.85em;
  color: var(--text-dim);
}
.nano-xbridge-card .xbridge-vector-preview {
  font-family: var(--mono);
  font-size: 0.8em;
  max-height: 60px;
  overflow: hidden;
  color: var(--text-dim);
  margin-top: 8px;
}
.nano-xbridge-similarity {
  text-align: center;
  margin: 16px 0;
}
.nano-xbridge-similarity .score {
  font-size: 2em;
  font-weight: bold;
  color: var(--accent);
  display: block;
}
.nano-xbridge-similarity .label {
  font-size: 0.85em;
  color: var(--text-muted);
}
.nano-xbridge-similarity.perfect .score { color: var(--success); }
.nano-xbridge-similarity.high .score { color: var(--success); }
.nano-xbridge-similarity.mid .score { color: var(--warning); }
.nano-xbridge-similarity.low .score { color: var(--error); }
.nano-xbridge-no-key {
  background: var(--bg-surface);
  border: 1px dashed var(--border);
  padding: 16px;
  border-radius: var(--radius);
  text-align: center;
  color: var(--text-dim);
}
.nano-xbridge-interpretation {
  text-align: center;
  font-size: 0.9em;
  color: var(--text-muted);
  margin-top: 8px;
}
.nano-xbridge-viz {
  margin-top: 16px;
}
.nano-xbridge-viz-title {
  font-weight: 600;
  margin-bottom: 8px;
  color: var(--accent-text);
}
.nano-xbridge-viz-chart {
  position: relative;
  height: 200px;
  background: var(--bg-surface);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  overflow: hidden;
  padding: 8px;
}
.nano-xbridge-viz-bar {
  position: absolute;
  bottom: 0;
}
.nano-xbridge-viz-bar.nano-bar {
  background: var(--accent);
  opacity: 0.6;
}
.nano-xbridge-viz-bar.api-bar {
  background: var(--purple);
  opacity: 0.6;
}
.nano-xbridge-viz-legend {
  display: flex;
  gap: 16px;
  margin-top: 8px;
  font-size: 0.85em;
}
.nano-xbridge-viz-legend-item {
  display: flex;
  align-items: center;
  gap: 4px;
}
.nano-xbridge-viz-legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}
.nano-xbridge-diff-stats {
  display: flex;
  gap: 12px;
  margin-top: 12px;
}
.nano-xbridge-diff-stat {
  background: var(--bg-surface);
  padding: 12px;
  border-radius: var(--radius);
  text-align: center;
  flex: 1;
}
.nano-xbridge-diff-stat .stat-value {
  font-size: 1.4em;
  font-weight: bold;
  color: var(--accent-text);
}
.nano-xbridge-diff-stat .stat-label {
  font-size: 0.8em;
  color: var(--text-muted);
  margin-top: 4px;
}

/* ── Library Search ── */
.wf-library-search-wrap {
  padding: 8px 12px 4px; position: relative;
}
.wf-library-search-wrap input {
  width: 100%; padding: 6px 28px 6px 10px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg-card);
  color: var(--text); font-size: 12px; outline: none;
  box-sizing: border-box;
  transition: border-color 0.15s;
}
.wf-library-search-wrap input:focus {
  border-color: var(--accent);
}
.wf-library-search-wrap input::placeholder {
  color: var(--text-muted);
}
.wf-library-search-clear {
  position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 14px; padding: 2px 4px; line-height: 1; display: none;
}
.wf-library-search-clear:hover { color: var(--text); }
/* ── Collapsible Library Sections ── */
.wf-lib-section-header {
  display: flex; align-items: center; gap: 6px;
  width: 100%; padding: 8px 12px; border: none; background: none;
  color: var(--text-muted); font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 1.2px; cursor: pointer;
  transition: color 0.15s;
}
.wf-lib-section-header:hover { color: var(--text); }
.wf-lib-section-header .wf-chevron {
  font-size: 10px; transition: transform 0.2s; display: inline-block;
}
.wf-lib-section-header.expanded .wf-chevron { transform: rotate(90deg); }
.wf-lib-section-header .wf-section-count {
  color: var(--text-muted); font-size: 10px; font-weight: 400;
}
.wf-lib-section-body {
  overflow: hidden; transition: max-height 0.2s ease-out;
}
/* ── Properties Accordion ── */
.wf-accordion-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--border);
  transition: background-color 0.15s ease; user-select: none;
}
.wf-accordion-header.expanded {
  background: rgba(0,212,170,0.05);
}
.wf-accordion-header .wf-acc-left {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.5px;
  color: var(--text);
}
.wf-accordion-header .wf-acc-left .wf-chevron {
  font-size: 10px; transition: transform 0.2s; display: inline-block;
}
.wf-accordion-header.expanded .wf-acc-left .wf-chevron { transform: rotate(90deg); }
.wf-accordion-header .wf-acc-summary {
  font-size: 10px; color: var(--text-muted); font-weight: 400;
}
.wf-accordion-body {
  overflow: hidden; transition: max-height 0.2s ease-out;
}
.wf-accordion-body-inner {
  padding: 12px;
}
.wf-step-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 0; font-size: 12px;
}
.wf-step-dot {
  width: 8px; height: 8px; border-radius: 50%; display: inline-block;
  margin-right: 8px; flex-shrink: 0;
}
.wf-step-name { flex: 1; color: var(--text); }
.wf-step-time {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 10px;
  color: var(--text-muted); margin-left: 8px;
}
.wf-canvas-area {
  flex: 1; position: relative; overflow: hidden;
  background: var(--bg);
  background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
  background-size: 20px 20px;
  padding-bottom: 48px;
}
.wf-canvas-toolbar {
  position: absolute; top: 12px; left: 12px; right: 12px; z-index: 10;
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
}
.wf-canvas-toolbar button {
  width: 32px; height: 32px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--bg-card);
  color: var(--text); cursor: pointer; font-size: 14px;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
}
.wf-canvas-toolbar button:hover { background: var(--bg); border-color: var(--text-muted); }
.wf-canvas-toolbar .wf-plan-btn {
  width: auto; padding: 0 12px; gap: 4px;
  font-weight: 600; font-size: 12px;
}
.wf-canvas-toolbar .wf-plan-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wf-canvas-toolbar .wf-run-btn {
  width: auto; padding: 0 12px; gap: 4px;
  background: var(--accent, #6c63ff); color: #fff; border-color: transparent;
  font-weight: 600; font-size: 12px;
}
.wf-canvas-toolbar .wf-run-btn:hover { opacity: 0.9; }
.wf-canvas-toolbar .wf-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wf-canvas-toolbar .wf-new-btn {
  width: auto; padding: 0 12px; gap: 4px;
  font-weight: 600; font-size: 12px;
  background: var(--bg-card); border: 1px solid var(--accent);
  color: var(--accent);
}
.wf-canvas-toolbar .wf-new-btn:hover { background: var(--accent); color: #fff; }
.wf-canvas-toolbar .wf-edit-btn {
  width: auto; padding: 0 12px; gap: 4px;
  font-weight: 600; font-size: 12px;
  background: var(--bg-card); border: 1px solid var(--border);
  color: var(--text-muted);
}
.wf-canvas-toolbar .wf-edit-btn:hover { border-color: var(--accent); color: var(--accent); }
.wf-canvas-toolbar .wf-edit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.wf-canvas-toolbar .wf-edit-btn:disabled:hover { border-color: var(--border); color: var(--text-muted); }
.wf-toolbar-sep {
  width: 1px; height: 20px; background: var(--border); margin: 0 2px;
}
/* Library / Palette tabs */
.wf-library-tabs {
  display: flex; gap: 2px; width: 100%;
  background: var(--bg-input); border-radius: 6px; padding: 2px;
}
.wf-lib-tab {
  flex: 1; padding: 5px 0; border: none; border-radius: 4px;
  background: transparent; color: var(--text-muted); cursor: pointer;
  font-size: 11px; font-weight: 600; font-family: var(--font);
  transition: background 0.15s, color 0.15s; text-align: center;
}
.wf-lib-tab.active { background: var(--accent); color: #fff; }
.wf-lib-tab:hover:not(.active) { color: var(--text); }
/* Palette items */
.wf-palette-category { margin-bottom: 8px; }
.wf-palette-category-title {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--text-muted);
  padding: 4px 12px; margin-bottom: 2px;
}
.wf-palette-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; border-radius: 6px; cursor: grab;
  font-size: 12px; color: var(--text); transition: background 0.15s;
}
.wf-palette-item:hover { background: var(--bg-card); }
.wf-palette-item:active { cursor: grabbing; }
.wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
.wf-palette-label { font-weight: 500; }

/* ── Workflow Node Help ── */
.wf-palette-item { position: relative; }
.wf-help-trigger {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
  padding: 0; z-index: 2;
}
.wf-palette-item:hover .wf-help-trigger { opacity: 1; }
.wf-help-trigger:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.wf-help-trigger svg { width: 12px; height: 12px; }

.wf-inspector-help-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 4px;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text-muted); font-size: 11px; cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
  margin-left: 8px; vertical-align: middle;
}
.wf-inspector-help-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.wf-inspector-help-btn svg { width: 12px; height: 12px; }

/* Workflow info button (library items) */
.wf-library-item-info {
  position: absolute; top: 10px; right: 8px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--bg-surface); border: 1px solid var(--border);
  color: var(--text-muted); font-size: 11px; font-weight: 600;
  display: none; align-items: center; justify-content: center;
  cursor: pointer; z-index: 2; transition: all 0.15s;
  font-family: var(--font); line-height: 1;
}
.wf-library-item:hover .wf-library-item-info { display: flex; }
.wf-library-item-info:hover { border-color: var(--accent); color: var(--accent); background: var(--bg); }
/* Toolbar info button */
.wf-toolbar-info-btn {
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 6px; width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.15s;
}
.wf-toolbar-info-btn:hover { border-color: var(--accent); color: var(--accent); }
.wf-toolbar-info-btn:disabled { opacity: 0.3; cursor: default; pointer-events: none; }
/* Workflow info modal */
.wf-info-modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  z-index: 10000; animation: wfStoreDetailFadeIn 0.12s ease-out;
}
.wf-info-modal {
  background: var(--bg); border: 1px solid var(--border); border-radius: 12px;
  width: 480px; max-height: 80vh; overflow-y: auto;
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.wf-info-modal-header {
  padding: 16px 20px 12px; display: flex; align-items: center; gap: 12px;
  border-bottom: 1px solid var(--border);
}
.wf-info-modal-close {
  position: absolute; top: 12px; right: 12px; background: none; border: none;
  color: var(--text-muted); font-size: 18px; cursor: pointer; padding: 4px 8px;
  border-radius: 6px; transition: all 0.15s;
}
.wf-info-modal-close:hover { background: var(--bg-surface); color: var(--text); }
.wf-info-modal-body { padding: 16px 20px; }
.wf-info-modal-section { margin-bottom: 14px; }
.wf-info-modal-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 6px; font-weight: 600; }
.wf-info-modal-stats { display: flex; gap: 16px; }
.wf-info-modal-stat { text-align: center; }
.wf-info-modal-stat-val { font-size: 16px; font-weight: 700; color: var(--text); }
.wf-info-modal-stat-lbl { font-size: 10px; color: var(--text-muted); }
.wf-info-modal-tools { display: flex; flex-wrap: wrap; gap: 4px; }
.wf-info-modal-tool { font-size: 11px; padding: 2px 8px; border-radius: 4px; border: 1px solid; font-family: var(--mono); }
.wf-info-modal-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.wf-info-modal-tag { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--bg-surface); color: var(--text-muted); }
.wf-info-modal-actions { padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; }
.wf-info-modal-hero {
  padding: 20px; border-radius: 12px 12px 0 0; position: relative;
  display: flex; align-items: center; gap: 14px;
}
.wf-info-modal-hero-icon {
  width: 40px; height: 40px; border-radius: 10px; background: rgba(255,255,255,0.15);
  display: flex; align-items: center; justify-content: center; color: #fff; flex-shrink: 0;
}
.wf-info-modal-hero-name { font-size: 18px; font-weight: 700; color: #fff; }
.wf-info-modal-hero-pkg { font-size: 11px; color: rgba(255,255,255,0.7); font-family: var(--mono); }

.wf-help-modal-backdrop {
  position: fixed; inset: 0; z-index: 10000;
  background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  animation: wfStoreDetailFadeIn 0.12s ease-out;
}
.wf-help-modal {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 12px; width: 540px; max-width: 92vw;
  max-height: 82vh; display: flex; flex-direction: column;
  box-shadow: 0 16px 48px rgba(0,0,0,0.3);
  animation: wfStoreDetailFadeIn 0.12s ease-out;
}
.wf-help-modal-header {
  display: flex; align-items: center; gap: 10px;
  padding: 16px 20px; border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.wf-help-modal-header .wf-help-icon {
  width: 32px; height: 32px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.wf-help-modal-header .wf-help-title { font-size: 16px; font-weight: 600; flex: 1; }
.wf-help-modal-header .wf-help-category-badge {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.5px; padding: 2px 8px; border-radius: 4px;
  background: var(--bg-card); color: var(--text-muted); flex-shrink: 0;
}
.wf-help-modal-header .wf-help-close-btn {
  width: 28px; height: 28px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; padding: 0; transition: background 0.15s ease, color 0.15s ease;
}
.wf-help-modal-header .wf-help-close-btn:hover { background: var(--bg-card); color: var(--text); }

.wf-help-modal-body {
  overflow-y: auto; padding: 20px; flex: 1;
}
.wf-help-section { margin-bottom: 20px; }
.wf-help-section:last-child { margin-bottom: 0; }
.wf-help-section-title {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px;
}
.wf-help-section p {
  font-size: 13px; line-height: 1.6; color: var(--text); margin: 0;
}

.wf-help-io-table {
  width: 100%; border-collapse: collapse; font-size: 12px;
}
.wf-help-io-table th {
  text-align: left; padding: 6px 8px; font-weight: 600;
  color: var(--text-muted); border-bottom: 1px solid var(--border);
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.3px;
}
.wf-help-io-table td {
  padding: 6px 8px; border-bottom: 1px solid var(--border);
  color: var(--text); vertical-align: top;
}
.wf-help-io-table tr:last-child td { border-bottom: none; }
.wf-help-io-table .wf-help-key {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
  color: var(--accent); white-space: nowrap;
}
.wf-help-io-table .wf-help-type {
  font-size: 10px; color: var(--text-muted); white-space: nowrap;
}
.wf-help-io-table .wf-help-required {
  display: inline-block; font-size: 9px; font-weight: 600;
  color: #ff6b6b; background: rgba(255,107,107,0.1);
  padding: 1px 4px; border-radius: 3px; margin-left: 4px;
}

.wf-help-tip {
  display: flex; gap: 8px; margin-bottom: 8px;
  font-size: 13px; line-height: 1.5; color: var(--text);
}
.wf-help-tip:last-child { margin-bottom: 0; }
.wf-help-tip-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); flex-shrink: 0; margin-top: 7px;
}

/* ── Workflow Store ── */
.wf-store-overlay {
  position: absolute; inset: 0; background: var(--bg); z-index: 50;
  display: flex; flex-direction: column;
  opacity: 0; transform: scale(0.98); pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.wf-store-overlay.open {
  opacity: 1; transform: scale(1); pointer-events: all;
}
.wf-store-overlay::before {
  content: ''; position: absolute; top: -150px; left: 50%; transform: translateX(-50%);
  width: 700px; height: 500px;
  background: radial-gradient(ellipse, rgba(0,212,170,.05) 0%, rgba(64,224,255,.02) 40%, transparent 70%);
  pointer-events: none; z-index: 0;
}
.wf-store-header {
  padding: 14px 20px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; flex-shrink: 0;
}
.wf-store-back {
  display: flex; align-items: center; gap: 6px; background: none; border: none;
  color: var(--text-muted); font-family: var(--font); font-size: 13px; cursor: pointer;
  padding: 6px 10px; border-radius: 6px; transition: all 0.15s;
}
.wf-store-back:hover { background: var(--bg-surface); color: var(--accent); }
.wf-store-title-area { display: flex; align-items: center; gap: 8px; flex: 1; }
.wf-store-title { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: 0.02em; }
.wf-store-badge { font-family: var(--mono); font-size: 9px; padding: 3px 8px; background: rgba(0,212,170,.15); border: 1px solid rgba(0,212,170,.2); border-radius: 100px; color: var(--accent); letter-spacing: 0.04em; text-transform: uppercase; }
.wf-store-search-wrap { position: relative; width: 220px; }
.wf-store-search {
  width: 100%; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 12px 8px 30px; font-family: var(--font); font-size: 13px; color: var(--text); outline: none;
  transition: border-color 0.15s;
}
.wf-store-search:focus { border-color: var(--accent); }
.wf-store-search::placeholder { color: var(--text-muted); }
.wf-store-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; pointer-events: none; }
.wf-store-sort {
  background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 10px; font-family: var(--font); font-size: 12px; color: var(--text-muted); outline: none; cursor: pointer;
}
.wf-store-body { flex: 1; overflow-y: auto; position: relative; z-index: 1; }
.wf-store-body::-webkit-scrollbar { width: 6px; }
.wf-store-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.wf-store-inner { max-width: 1100px; margin: 0 auto; padding: 20px 24px 40px; }
.wf-store-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 20px; }
.wf-store-chip {
  padding: 6px 14px; background: none; border: 1px solid var(--border); border-radius: 100px;
  font-family: var(--font); font-size: 12px; color: var(--text-muted); cursor: pointer;
  transition: all 0.15s; white-space: nowrap;
}
.wf-store-chip:hover { border-color: var(--text-dim); color: var(--text-dim); }
.wf-store-chip.active { background: rgba(0,212,170,.15); border-color: rgba(0,212,170,.3); color: var(--accent); }
.wf-store-section-label {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px;
  display: flex; align-items: center; gap: 8px;
}
.wf-store-count { background: var(--bg-surface); padding: 2px 7px; border-radius: 100px; font-size: 10px; }
/* Featured cards */
.wf-store-featured-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 24px; }
.wf-store-featured-card {
  border-radius: 14px; padding: 22px 20px 18px; position: relative; overflow: hidden;
  cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; min-height: 150px;
  display: flex; flex-direction: column; justify-content: flex-end;
}
.wf-store-featured-card:hover { transform: translateY(-3px); box-shadow: 0 16px 48px -8px rgba(0,0,0,.5); }
.wf-store-featured-card::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 15%, rgba(0,0,0,.65) 100%); z-index: 1;
}
.wf-store-featured-content { position: relative; z-index: 2; }
.wf-store-featured-content h3 { font-family: var(--mono); font-size: 14px; font-weight: 700; margin-bottom: 4px; color: #fff; }
.wf-store-featured-content p { font-size: 12px; color: rgba(255,255,255,.65); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.wf-store-featured-icon { position: absolute; top: 14px; left: 16px; z-index: 2; width: 36px; height: 36px; border-radius: 10px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; }
.wf-store-featured-icon svg { width: 20px; height: 20px; color: #fff; }
.wf-store-featured-dl { position: absolute; top: 12px; right: 14px; z-index: 2; font-family: var(--mono); font-size: 10px; color: rgba(255,255,255,.5); }
.wf-store-featured-author { position: relative; z-index: 2; display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 11px; color: rgba(255,255,255,.55); }
.wf-store-featured-avatar { width: 18px; height: 18px; border-radius: 50%; background: rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff; font-weight: 700; overflow: hidden; }
.wf-store-featured-avatar img { width: 100%; height: 100%; object-fit: cover; }
/* Workflow cards grid */
.wf-store-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
.wf-store-card {
  background: var(--bg-surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 16px; cursor: pointer; transition: all 0.15s; position: relative; overflow: hidden;
}
.wf-store-card:hover { border-color: var(--text-dim); background: var(--bg-card); transform: translateY(-1px); }
.wf-store-card-bar { position: absolute; top: 0; left: 0; right: 0; height: 2px; opacity: 0; transition: opacity 0.15s; }
.wf-store-card:hover .wf-store-card-bar { opacity: 1; }
.wf-store-card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; gap: 8px; }
.wf-store-card-icon { width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.wf-store-card-icon svg { width: 16px; height: 16px; }
.wf-store-card-name-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
.wf-store-card-name { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); }
.wf-store-card-badges { display: flex; gap: 4px; }
.wf-store-badge-official { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(0,212,170,.1); color: var(--accent); border: 1px solid rgba(0,212,170,.15); white-space: nowrap; }
.wf-store-badge-installed { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.15); white-space: nowrap; }
.wf-store-badge-verified { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.15); white-space: nowrap; }
.wf-store-badge-capability { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 100px; white-space: nowrap; display: inline-flex; align-items: center; gap: 3px; }
.wf-store-badge-cap-network { background: rgba(59,130,246,.1); color: #3B82F6; border: 1px solid rgba(59,130,246,.15); }
.wf-store-badge-cap-writedb { background: rgba(245,158,11,.1); color: #F59E0B; border: 1px solid rgba(245,158,11,.15); }
.wf-store-badge-cap-llm { background: rgba(139,92,246,.1); color: #8B5CF6; border: 1px solid rgba(139,92,246,.15); }
.wf-store-badge-cap-loop { background: rgba(6,182,212,.1); color: #06B6D4; border: 1px solid rgba(6,182,212,.15); }
.wf-store-badge-cap-readdb { background: rgba(34,197,94,.1); color: #22C55E; border: 1px solid rgba(34,197,94,.15); }
.wf-store-detail-security-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg-card); border-radius: 6px; margin-bottom: 4px; font-size: 12px; color: var(--text-dim); }
.wf-store-detail-security-sev { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; font-weight: 700; text-transform: uppercase; }
.wf-store-detail-security-sev.critical { background: rgba(239,68,68,.15); color: #EF4444; }
.wf-store-detail-security-sev.high { background: rgba(249,115,22,.15); color: #F97316; }
.wf-store-detail-security-sev.medium { background: rgba(234,179,8,.15); color: #EAB308; }
.wf-store-detail-security-sev.low { background: rgba(148,163,184,.15); color: #94A3B8; }
.wf-store-detail-security-ok { color: #10B981; font-size: 12px; padding: 8px 10px; background: rgba(16,185,129,.06); border-radius: 6px; }
.wf-store-detail-stars { display: inline-flex; gap: 2px; cursor: pointer; }
.wf-store-detail-stars span { font-size: 18px; color: var(--text-muted); transition: color 0.1s; }
.wf-store-detail-stars span.filled { color: #F59E0B; }
.wf-store-detail-stars span:hover, .wf-store-detail-stars span:hover ~ span { color: var(--text-muted); }
.wf-store-report-form { margin-top: 8px; padding: 12px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); }
.wf-store-report-form select, .wf-store-report-form textarea { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px; color: var(--text); font-family: var(--font); font-size: 12px; margin-bottom: 8px; }
.wf-store-report-form textarea { resize: vertical; min-height: 60px; }
.wf-store-report-submit { padding: 6px 14px; background: var(--error); color: #fff; border: none; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; }
.wf-store-report-submit:hover { filter: brightness(1.1); }
.wf-store-cap-badges { display: flex; gap: 3px; flex-wrap: wrap; margin-top: 4px; }
.wf-store-card-desc { font-size: 12px; color: var(--text-dim); line-height: 1.5; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.wf-store-card-author { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
.wf-store-card-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wf-store-card-cat { font-family: var(--mono); font-size: 10px; padding: 2px 8px; background: var(--bg-card); border-radius: 100px; color: var(--text-muted); }
.wf-store-card-dots { display: flex; gap: 2px; }
.wf-store-card-dot { width: 7px; height: 7px; border-radius: 50%; opacity: 0.65; }
.wf-store-card-complexity { font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
.wf-store-card-downloads { margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
.wf-store-empty { text-align: center; padding: 40px 0; color: var(--text-muted); }
.wf-store-empty p { font-family: var(--mono); font-size: 13px; }
/* Detail modal */
.wf-store-detail-bg {
  position: fixed; inset: 0; background: rgba(0,0,0,.55); backdrop-filter: blur(6px);
  z-index: 200; display: flex; align-items: center; justify-content: center;
  animation: wfStoreDetailFadeIn 0.12s ease-out; padding: 20px;
}
@keyframes wfStoreDetailFadeIn { from { opacity: 0; } }
@keyframes wfStoreDetailSlideIn { from { transform: translateY(20px); opacity: 0; } }
.wf-store-detail-panel {
  background: var(--bg-surface); border: 1px solid var(--border); border-radius: 18px;
  width: 100%; max-width: 580px; max-height: 82vh; overflow: hidden;
  animation: wfStoreDetailSlideIn 0.18s ease-out; display: flex; flex-direction: column;
}
.wf-store-detail-scroll {
  overflow-y: auto; flex: 1; min-height: 0;
}
.wf-store-detail-scroll::-webkit-scrollbar { width: 4px; }
.wf-store-detail-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.wf-store-detail-hero { padding: 28px 28px 20px; border-radius: 18px 18px 0 0; position: relative; }
.wf-store-detail-hero::before { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 25%, var(--bg-surface) 100%); border-radius: 18px 18px 0 0; }
.wf-store-detail-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; gap: 14px; }
.wf-store-detail-hero-icon { width: 48px; height: 48px; border-radius: 12px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.wf-store-detail-hero-icon svg { width: 26px; height: 26px; color: #fff; }
.wf-store-detail-close {
  position: absolute; top: 14px; right: 14px; z-index: 2; width: 28px; height: 28px;
  border-radius: 50%; background: rgba(0,0,0,.35); border: none; color: #fff; font-size: 16px;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.wf-store-detail-close:hover { background: rgba(0,0,0,.55); }
.wf-store-detail-name { font-family: var(--mono); font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 4px; }
.wf-store-detail-pkg { font-family: var(--mono); font-size: 11px; color: rgba(255,255,255,.45); }
.wf-store-detail-body { padding: 0 28px 28px; }
.wf-store-detail-desc { font-size: 14px; color: var(--text-dim); line-height: 1.65; margin-bottom: 20px; }
.wf-store-detail-section { margin-bottom: 16px; }
.wf-store-detail-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px; }
.wf-store-detail-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.wf-store-detail-stat { background: var(--bg-card); border-radius: 8px; padding: 12px; text-align: center; }
.wf-store-detail-stat-val { font-family: var(--mono); font-size: 20px; font-weight: 700; color: var(--text); }
.wf-store-detail-stat-lbl { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.wf-store-detail-tools { display: flex; gap: 5px; flex-wrap: wrap; }
.wf-store-detail-tool { padding: 4px 10px; border-radius: 100px; font-size: 11px; font-family: var(--mono); border: 1px solid; }
.wf-store-detail-tags { display: flex; gap: 5px; flex-wrap: wrap; }
.wf-store-detail-tag { padding: 4px 10px; background: var(--bg-card); border-radius: 100px; font-size: 11px; color: var(--text-dim); font-family: var(--mono); }
.wf-store-detail-install { margin-top: 20px; padding: 16px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); }
.wf-store-detail-cmd {
  font-family: var(--mono); font-size: 12px; color: var(--accent); background: var(--bg);
  padding: 10px 14px; border-radius: 6px; margin-top: 6px; display: flex; align-items: center;
  justify-content: space-between; cursor: pointer; border: 1px solid var(--border);
  transition: border-color 0.15s; overflow-x: auto; white-space: nowrap; gap: 10px;
}
.wf-store-detail-cmd:hover { border-color: var(--accent); }
.wf-store-detail-cmd-hint { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
.wf-store-detail-author { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
.wf-store-detail-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--bg-card); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--text-muted); font-weight: 700; overflow: hidden; flex-shrink: 0; }
.wf-store-detail-avatar img { width: 100%; height: 100%; object-fit: cover; }
.wf-store-detail-author-name { font-size: 13px; color: var(--text-dim); }
.wf-store-detail-author-name a { color: var(--accent); text-decoration: none; }
.wf-store-detail-author-name a:hover { text-decoration: underline; }
.wf-store-detail-inputs-table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: var(--mono); }
.wf-store-detail-inputs-table th { text-align: left; padding: 6px 10px; color: var(--text-muted); font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
.wf-store-detail-inputs-table td { padding: 6px 10px; color: var(--text-dim); border-bottom: 1px solid var(--border); }
.wf-store-detail-screenshots { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; }
.wf-store-detail-screenshots img { height: 180px; border-radius: 8px; border: 1px solid var(--border); flex-shrink: 0; }
.wf-store-detail-actions { display: flex; gap: 8px; margin-top: 16px; }
.wf-store-detail-btn {
  flex: 1; padding: 10px; border-radius: 8px; font-family: var(--mono); font-size: 12px;
  font-weight: 700; cursor: pointer; border: none; transition: all 0.15s; text-align: center;
}
.wf-store-detail-btn-primary { background: var(--accent); color: var(--bg); }
.wf-store-detail-btn-primary:hover { filter: brightness(1.1); }
.wf-store-detail-btn-secondary { background: var(--bg-card); color: var(--text-dim); border: 1px solid var(--border); }
.wf-store-detail-btn-secondary:hover { background: var(--border); color: var(--text); }
.wf-store-detail-btn-installed { background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.2); cursor: default; }
/* Store button in library header */
.wf-store-btn {
  width: 28px; height: 28px; border-radius: 6px; border: 1px solid transparent;
  background: none; color: var(--text-muted); font-size: 14px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; transition: all 0.15s;
  flex-shrink: 0;
}
.wf-store-btn:hover { background: var(--bg-card); color: var(--accent); border-color: rgba(0,212,170,.15); }
.wf-store-btn.active { color: var(--accent); }
@media (max-width: 700px) {
  .wf-store-featured-grid { grid-template-columns: 1fr; }
  .wf-store-grid { grid-template-columns: 1fr; }
}

/* Builder port styles */
.wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
.wf-port-builder:hover { r: 9; filter: brightness(1.4); }
.wf-inspector-delete-btn {
  width: 100%; padding: 8px 0; border: none; border-radius: 6px;
  background: var(--error); color: #fff; cursor: pointer;
  font-size: 12px; font-weight: 600; margin-top: 12px;
  transition: opacity 0.15s;
}
.wf-inspector-delete-btn:hover { opacity: 0.85; }
.wf-validation-errors {
  background: rgba(255,105,96,0.08); border: 1px solid var(--error);
  border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
  font-size: 11px; color: var(--error);
}
.wf-validation-errors ul { margin: 4px 0 0 16px; }
/* Dry-run plan overlay */
.wf-dryrun-overlay {
  position: absolute; top: 0; left: 0; right: 0; bottom: 0;
  z-index: 8; background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  animation: wf-modal-fade-in 0.15s ease;
}
.wf-dryrun-panel {
  width: 90%; max-width: 520px; max-height: 70vh;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 12px; display: flex; flex-direction: column;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
  animation: wf-modal-slide-up 0.2s ease;
}
.wf-dryrun-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; border-bottom: 1px solid var(--border);
}
.wf-dryrun-title { font-weight: 700; font-size: 14px; color: var(--text); }
.wf-dryrun-close {
  border: none; background: none; color: var(--text-muted);
  cursor: pointer; font-size: 20px; padding: 2px 8px;
}
.wf-dryrun-close:hover { color: var(--text); }
.wf-dryrun-body {
  flex: 1; overflow-y: auto; padding: 16px 20px;
}
.wf-dryrun-layer {
  margin-bottom: 16px;
}
.wf-dryrun-layer-title {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px;
  display: flex; align-items: center; gap: 8px;
}
.wf-dryrun-layer-badge {
  font-size: 10px; padding: 1px 6px; border-radius: 10px;
  background: var(--accent); color: #fff; font-weight: 600;
}
.wf-dryrun-step {
  padding: 8px 12px; border-radius: 6px; margin-bottom: 4px;
  background: var(--bg-card); border: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px; font-size: 12px;
}
.wf-dryrun-step-icon { flex-shrink: 0; display: flex; align-items: center; }
.wf-dryrun-step-info { flex: 1; min-width: 0; }
.wf-dryrun-step-name { font-weight: 600; color: var(--text); }
.wf-dryrun-step-tool { color: var(--text-muted); font-size: 11px; }
.wf-dryrun-step-cond {
  font-size: 10px; color: #FFD54F; margin-top: 2px;
  font-family: 'SF Mono', 'Fira Code', monospace;
}
.wf-dryrun-summary {
  padding: 12px 16px; border-radius: 8px; margin-top: 8px;
  background: var(--bg-card); border: 1px solid var(--border);
  font-size: 12px; color: var(--text-muted);
  display: flex; gap: 16px;
}
.wf-dryrun-stat { text-align: center; }
.wf-dryrun-stat-value { font-size: 20px; font-weight: 700; color: var(--text); }
.wf-dryrun-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; }
[data-theme="light"] .wf-dryrun-panel { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
[data-theme="light"] .wf-dryrun-step-cond { color: #944F01; }
.wf-canvas-toolbar .wf-stop-btn {
  width: auto; padding: 0 12px; gap: 4px;
  background: #e74c3c; color: #fff; border-color: transparent;
  font-weight: 600; font-size: 12px;
}
.wf-canvas-toolbar .wf-stop-btn:hover { opacity: 0.9; background: #c0392b; }
/* Execution status bar */
.wf-exec-status {
  position: absolute; bottom: 56px; left: 50%; transform: translateX(-50%);
  z-index: 10; display: flex; align-items: center; gap: 8px;
  padding: 6px 16px; border-radius: 20px;
  background: var(--bg-card); border: 1px solid var(--border);
  font-size: 12px; color: var(--text); box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.wf-exec-status-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: #69F0AE; animation: wf-status-blink 1s ease-in-out infinite;
}
.wf-exec-status.error .wf-exec-status-dot { background: #e74c3c; animation: none; }
.wf-exec-status.stopped .wf-exec-status-dot { background: #FFB74D; animation: none; }
.wf-exec-status.done .wf-exec-status-dot { background: #69F0AE; animation: none; }
@keyframes wf-status-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.wf-exec-status-time { color: var(--text-muted); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; }
/* Output modal */
.wf-output-modal-backdrop {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.6); z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  animation: wf-modal-fade-in 0.15s ease;
}
@keyframes wf-modal-fade-in { from { opacity: 0; } to { opacity: 1; } }
.wf-output-modal {
  width: 90%; max-width: 800px; max-height: 80vh;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 12px; display: flex; flex-direction: column;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
  animation: wf-modal-slide-up 0.2s ease;
}
@keyframes wf-modal-slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.wf-output-modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; border-bottom: 1px solid var(--border);
}
.wf-output-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
.wf-output-modal-actions { display: flex; align-items: center; gap: 6px; }
.wf-output-modal-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 10px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg-card);
  color: var(--text); cursor: pointer; font-size: 12px;
  transition: background 0.15s, border-color 0.15s;
}
.wf-output-modal-btn:hover { background: var(--bg); border-color: var(--text-muted); }
.wf-output-modal-btn.close { border: none; font-size: 20px; padding: 2px 8px; }
.wf-output-modal-body {
  flex: 1; overflow: auto; padding: 16px 20px;
  margin: 0; font-family: 'SF Mono', 'Fira Code', monospace;
  font-size: 12px; line-height: 1.5; color: var(--text);
  white-space: pre-wrap; word-break: break-word;
}
/* Expand button for output sections */
.wf-output-expand-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 4px; margin-top: 6px;
  border: 1px solid var(--border); background: transparent;
  color: var(--text-muted); cursor: pointer; font-size: 10px;
  transition: color 0.15s, border-color 0.15s;
}
.wf-output-expand-btn:hover { color: var(--text); border-color: var(--text-muted); }
/* Input modal (pre-execution) */
.wf-input-modal-backdrop {
  position: fixed; inset: 0; z-index: 9999;
  background: rgba(0,0,0,0.55); display: flex;
  align-items: center; justify-content: center;
}
.wf-input-modal {
  background: var(--bg-surface, var(--bg)); border: 1px solid var(--border);
  border-radius: 10px; width: 440px; max-width: 90vw;
  max-height: 80vh; display: flex; flex-direction: column;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.wf-input-modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; border-bottom: 1px solid var(--border);
}
.wf-input-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
.wf-input-modal-body {
  flex: 1; overflow-y: auto; padding: 16px 20px;
}
.wf-input-modal-field { margin-bottom: 14px; }
.wf-input-modal-label {
  font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 4px;
}
.wf-input-modal-desc {
  font-size: 11px; color: var(--text-muted); margin-bottom: 4px; line-height: 1.3;
}
.wf-input-modal-input {
  width: 100%; box-sizing: border-box; padding: 6px 10px;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); color: var(--text); font-size: 13px;
  font-family: inherit; outline: none;
  transition: border-color 0.15s;
}
.wf-input-modal-input:focus { border-color: var(--accent, #6c63ff); }
.wf-input-modal-input.error { border-color: #e74c3c; }
.wf-input-modal-error { font-size: 11px; color: #e74c3c; margin-top: 3px; display: none; }
.wf-input-modal-footer {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  padding: 12px 20px; border-top: 1px solid var(--border);
}
.wf-input-modal-cancel {
  padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px;
  background: transparent; color: var(--text); font-size: 12px; cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.wf-input-modal-cancel:hover { background: var(--bg); border-color: var(--text-muted); }
.wf-input-modal-run {
  padding: 6px 16px; border: none; border-radius: 6px;
  background: var(--accent, #6c63ff); color: #fff; font-size: 12px;
  font-weight: 600; cursor: pointer; transition: filter 0.15s;
}
.wf-input-modal-run:hover { filter: brightness(1.15); }
#wf-canvas {
  width: 100%; height: 100%; display: block; position: relative; z-index: 1;
}
/* SVG node styles */
.wf-node { cursor: pointer; }
.wf-node rect {
  rx: 10; ry: 10; stroke-width: 2;
  transition: stroke 0.2s, filter 0.2s;
}
.wf-node:hover rect { filter: brightness(1.1); }
.wf-node.selected rect { stroke-width: 3; stroke: var(--accent, #6c63ff); }
.wf-node-label {
  font-size: 12px; font-weight: 600; fill: #fff;
  pointer-events: none; text-anchor: middle; dominant-baseline: central;
}
.wf-node-icon {
  pointer-events: none;
  color: #fff;
}
.wf-node-badge {
  font-size: 10px; fill: rgba(255,255,255,0.7);
  pointer-events: none; text-anchor: middle; dominant-baseline: central;
}
.wf-node-condition {
  font-size: 10px; fill: #FFD54F;
  pointer-events: none; text-anchor: end;
}
/* Execution states */
.wf-node--pending rect { opacity: 0.5; }
.wf-node--running rect {
  animation: wf-pulse 1.2s ease-in-out infinite;
}
@keyframes wf-pulse {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(108,99,255,0.4)); }
  50% { filter: drop-shadow(0 0 12px rgba(108,99,255,0.8)); }
}
.wf-node--completed rect { opacity: 1; }
.wf-node--skipped rect { opacity: 0.3; }
.wf-node--error rect { stroke: #e74c3c !important; stroke-width: 3; }
.wf-node-status {
  font-size: 12px; pointer-events: none; fill: #fff;
  text-anchor: middle; dominant-baseline: central;
}
.wf-node--error .wf-node-status { fill: #e74c3c; }
.wf-node--skipped .wf-node-status { fill: rgba(255,255,255,0.5); }
.wf-node-time {
  font-size: 9px; fill: rgba(255,255,255,0.6);
  pointer-events: none; text-anchor: middle;
}

/* Draft validation visual states */
.wf-node-warning rect { 
  stroke: #FFB74D !important; 
  stroke-dasharray: 6 3; 
  stroke-width: 2; 
}
.wf-node-info { 
  opacity: 0.6; 
}
.wf-node-error rect { 
  stroke: #EF5350 !important; 
  stroke-width: 3; 
}
.wf-node-orphan rect { 
  stroke: #616161 !important; 
  stroke-dasharray: 2 4; 
  stroke-width: 2; 
}
.wf-validation-badge {
  font-size: 10px;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
  cursor: default;
}
.wf-validation-badge-bg {
  rx: 6;
  ry: 6;
  pointer-events: all;
  cursor: help;
}
.wf-validation-bar {
  position: absolute;
  top: 52px;
  left: 12px;
  right: 12px;
  height: 28px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  display: none;
  align-items: center;
  padding: 0 12px;
  font-size: 12px;
  z-index: 9;
  cursor: pointer;
  transition: background-color 0.15s;
}
.wf-validation-bar:hover {
  background: var(--bg-card);
}
.wf-validation-bar .wf-validation-close {
  margin-left: auto;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 14px;
  padding: 0 4px;
  opacity: 0.6;
  line-height: 1;
}
.wf-validation-bar .wf-validation-close:hover {
  opacity: 1;
}
.wf-validation-bar.warning {
  color: #FFB74D;
  border-bottom-color: #FFB74D;
}
.wf-validation-bar.info {
  color: #889397;
  border-bottom-color: #889397;
}
.wf-validation-bar.error {
  color: #EF5350;
  border-bottom-color: #EF5350;
}

/* Light mode variants */
[data-theme="light"] .wf-node-warning rect { stroke: #FF8F00 !important; }
[data-theme="light"] .wf-node-error rect { stroke: #C62828 !important; }
[data-theme="light"] .wf-node-orphan rect { stroke: #424242 !important; }
[data-theme="light"] .wf-validation-bar.warning { color: #FF8F00; border-bottom-color: #FF8F00; }
[data-theme="light"] .wf-validation-bar.error { color: #C62828; border-bottom-color: #C62828; }
[data-theme="light"] .wf-validation-bar.info { color: #424242; border-bottom-color: #424242; }

/* Port styles */
.wf-port { pointer-events: none; transition: fill 0.2s, stroke 0.2s; }
.wf-port-in { fill: var(--bg-card); }
.wf-port-out { fill: currentColor; stroke: rgba(255,255,255,0.8); }
.wf-node:hover .wf-port-out { filter: brightness(1.3); }
.wf-node--completed .wf-port-out { fill: #69F0AE; stroke: #fff; }
.wf-node--error .wf-port-in { fill: #e74c3c; }
/* Edge styles */
.wf-edge {
  fill: none; stroke: #6b7b8d; stroke-width: 2;
  opacity: 0.5; transition: opacity 0.3s, stroke 0.3s;
  stroke-linecap: round;
}
.wf-edge--backward {
  stroke-dasharray: 6 4; opacity: 0.35;
}
.wf-edge--active {
  stroke: var(--accent, #6c63ff); opacity: 0.85;
  stroke-dasharray: 8 4;
  animation: wf-flow 0.6s linear infinite;
}
@keyframes wf-flow { to { stroke-dashoffset: -12; } }
.wf-edge--complete { stroke: var(--accent, #6c63ff); opacity: 0.6; stroke-dasharray: none; }
.wf-edge--else { stroke-dasharray: 6 4; opacity: 0.4; }
.wf-edge--skipped { opacity: 0.12; stroke-dasharray: 4 4; }
/* Inspector */
/* ── Library collapse chevron ── */
.wf-library-collapse-btn {
  width: 28px; height: 28px; border: none; background: none;
  color: var(--text-muted); cursor: pointer; font-size: 14px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 6px; transition: color 0.15s, background 0.15s;
  flex-shrink: 0; padding: 0;
}
.wf-library-collapse-btn:hover { color: var(--text); background: var(--bg-card); }

/* ── Canvas edge handles ── */
.wf-edge-handle {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 28px; height: 56px; z-index: 20;
  background: var(--bg-card); border: 1px solid var(--border);
  color: var(--text-muted); cursor: pointer;
  display: none; align-items: center; justify-content: center;
  font-size: 14px; transition: background 0.15s, color 0.15s;
}
.wf-edge-handle:hover { background: var(--bg); color: var(--text); }
.wf-edge-handle--left {
  left: 0; border-radius: 0 8px 8px 0; border-left: none;
}
.wf-edge-handle--right {
  right: 0; border-radius: 8px 0 0 8px; border-right: none;
}
.wf-edge-handle.visible { display: flex; }

/* ── Toolbar panel toggle group ── */
.wf-toolbar-spacer { flex: 1; }
.wf-toolbar-toggle-group {
  display: flex; background: var(--bg); border-radius: 6px; padding: 2px;
  border: 1px solid var(--border);
}
.wf-toolbar-toggle-group button {
  width: auto !important; height: 28px !important; padding: 0 10px !important;
  border: none !important; border-radius: 4px !important;
  background: transparent !important; color: var(--text-muted) !important;
  font-size: 11px !important; font-weight: 500 !important; cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.wf-toolbar-toggle-group button.active {
  background: rgba(0,212,170,0.15) !important; color: var(--accent) !important;
}
.wf-toolbar-toggle-group button:hover:not(.active) {
  color: var(--text) !important;
}

/* ── Inspector (upgraded) ── */
.wf-inspector {
  flex-shrink: 0; position: relative;
  display: flex; flex-direction: row;
  background: var(--bg);
  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  width: 300px;
  overflow: hidden;
  align-self: stretch;
  border-left: 1px solid var(--border);
}
.wf-inspector.collapsed {
  width: 0; border-left: none;
}
.wf-inspector-toggle {
  display: none; /* replaced by edge handle */
}
.wf-inspector.collapsed .wf-inspector-content {
  display: none;
}
.wf-inspector-content {
  flex: 1; display: flex; flex-direction: column;
  overflow-y: auto; min-width: 0; height: 100%;
}
.wf-inspector-header {
  padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
  color: var(--text); border-bottom: 1px solid var(--border);
}
.wf-inspector-body {
  padding: 16px; padding-bottom: 56px; flex: 1; overflow-y: auto;
}
.wf-inspector-empty {
  color: var(--text-muted); font-size: 13px; text-align: center;
  padding: 40px 16px;
}
.wf-inspector-section {
  margin-bottom: 16px;
}
.wf-inspector-section-title {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 6px;
}
.wf-inspector-field {
  font-size: 12px; color: var(--text); margin-bottom: 4px;
  display: flex; gap: 6px;
}
.wf-inspector-field-label {
  font-weight: 600; min-width: 60px; color: var(--text-muted);
}
.wf-inspector-field-value {
  word-break: break-all;
}
.wf-inspector-code {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 6px; padding: 8px; overflow-x: auto;
  white-space: pre-wrap; color: var(--text); margin-top: 4px;
}
/* Output format selector and rich rendering */
.wf-output-format-select {
  font-size: 11px; padding: 2px 6px; border-radius: 4px;
  background: var(--bg-card); border: 1px solid var(--border);
  color: var(--text); cursor: pointer; margin-left: 8px;
}
.wf-output-format-select:hover { border-color: var(--accent); }
.wf-output-table {
  width: 100%; border-collapse: collapse; font-size: 11px;
  color: var(--text); margin-top: 8px;
}
.wf-output-table th {
  text-align: left; padding: 6px 8px; font-weight: 600;
  border-bottom: 1px solid var(--accent); color: var(--accent);
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
}
.wf-output-table td {
  padding: 5px 8px; border-bottom: 1px solid var(--border);
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
}
.wf-output-table tr:hover td { background: rgba(0,212,170,0.05); }
.wf-output-markdown {
  font-size: 12px; line-height: 1.5; color: var(--text); padding: 8px;
}
.wf-output-markdown h2, .wf-output-markdown h3 {
  color: var(--accent); margin-top: 12px; margin-bottom: 4px;
}
.wf-output-markdown table {
  border-collapse: collapse; width: 100%; margin: 8px 0;
}
.wf-output-markdown th, .wf-output-markdown td {
  border: 1px solid var(--border); padding: 4px 8px; font-size: 11px;
}
.wf-output-text-field { margin-bottom: 12px; }
.wf-output-text-label {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  color: var(--accent); letter-spacing: 0.5px; margin-bottom: 2px;
}
.wf-output-text-value {
  font-size: 12px; color: var(--text); line-height: 1.5; white-space: pre-wrap;
}
.wf-output-metric {
  display: inline-block; margin-right: 16px; margin-bottom: 6px;
}
.wf-output-metric-val {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
  color: #40E0FF; font-weight: 600;
}
.wf-tool-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 10px; font-size: 11px;
  font-weight: 600; color: #fff;
}
.wf-inspector-input {
  width: 100%; padding: 6px 10px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg-card);
  color: var(--text); font-size: 12px; margin-top: 4px;
}
.wf-inspector-input:focus {
  outline: none; border-color: var(--accent, #6c63ff);
}
.wf-inspector-btn {
  width: 100%; padding: 8px 16px; border-radius: 8px;
  border: none; background: var(--accent, #6c63ff);
  color: #fff; font-weight: 600; font-size: 13px;
  cursor: pointer; margin-top: 12px;
}
.wf-inspector-btn:hover { opacity: 0.9; }
.wf-inspector-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wf-inspector-result {
  margin-top: 8px; padding: 8px; border-radius: 6px;
  background: var(--bg-card); border: 1px solid var(--border);
  font-size: 11px; max-height: 200px; overflow-y: auto;
}
.wf-inspector-result.success { border-color: #69F0AE; }
.wf-inspector-result.error { border-color: #e74c3c; }
/* Responsive */
@media (max-width: 900px) {
  .wf-library { width: 0; border-right: none; }
  .wf-inspector:not(.collapsed) { width: 240px; }
}
@media (max-width: 600px) {
  .wf-inspector { width: 0; border-left: none; }
}
/* Workflow visualizer light mode */
[data-theme="light"] .wf-node-label { fill: #001E2B; }
[data-theme="light"] .wf-node-icon { color: #001E2B; }
[data-theme="light"] .wf-node-badge { fill: rgba(0,30,43,0.55); }
[data-theme="light"] .wf-node-time { fill: rgba(0,30,43,0.5); }
[data-theme="light"] .wf-node-condition { fill: #944F01; }
[data-theme="light"] .wf-node-status { fill: #001E2B; }
[data-theme="light"] .wf-node--skipped .wf-node-status { fill: rgba(0,30,43,0.4); }
[data-theme="light"] .wf-node--error .wf-node-status { fill: #DB3030; }
[data-theme="light"] .wf-node rect { stroke-opacity: 0.7; }
[data-theme="light"] .wf-node.selected rect { stroke: var(--accent); }
[data-theme="light"] .wf-edge { stroke: #889397; opacity: 0.45; }
[data-theme="light"] .wf-edge--backward { opacity: 0.3; }
[data-theme="light"] .wf-edge--active { stroke: var(--accent); opacity: 0.7; }
[data-theme="light"] .wf-edge--complete { stroke: var(--accent); opacity: 0.5; }
[data-theme="light"] .wf-port-in { fill: var(--bg); }
[data-theme="light"] .wf-port-out { stroke: rgba(0,30,43,0.3); }
[data-theme="light"] .wf-node--completed .wf-port-out { fill: #009E80; stroke: var(--bg); }
[data-theme="light"] .wf-canvas-area { background: var(--bg-surface); }
[data-theme="light"] .wf-inspector-result.success { border-color: #009E80; }
[data-theme="light"] .wf-inspector-result.error { border-color: #DB3030; }
[data-theme="light"] .wf-exec-status { box-shadow: 0 2px 8px rgba(0,30,43,0.08); }
[data-theme="light"] .wf-run-btn { background: var(--accent); }
[data-theme="light"] .wf-output-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
[data-theme="light"] .wf-input-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
[data-theme="light"] .wf-input-modal-backdrop { background: rgba(0,0,0,0.3); }
@keyframes wf-pulse-light {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(0,158,128,0.3)); }
  50% { filter: drop-shadow(0 0 12px rgba(0,158,128,0.6)); }
}
[data-theme="light"] .wf-node--running rect { animation-name: wf-pulse-light; }
/* Empty canvas state */
.wf-canvas-empty {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  text-align: center; color: var(--text-muted); pointer-events: none;
}
.wf-canvas-empty-icon { margin-bottom: 16px; opacity: 0.15; }
.wf-canvas-empty-icon img { width: 200px; height: 200px; filter: invert(1); }
[data-theme="light"] .wf-canvas-empty-icon img { filter: none; opacity: 0.1; }
.wf-canvas-empty-text { font-size: 14px; }
/* Persistent watermark behind workflows */
.wf-canvas-watermark {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  pointer-events: none; z-index: 0; opacity: 0.04;
}
.wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
[data-theme="light"] .wf-canvas-watermark img { filter: none; }

/* ── Home Page Styles ── */

/* Home Header */
.home-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 24px 32px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.home-header-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.home-logo {
  width: 32px;
  height: 32px;
}

.home-header-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}

.home-version-badge {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 500;
}

.home-header-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.home-settings-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
}

.home-settings-btn:hover {
  background: var(--bg-card);
  color: var(--text);
  border-color: var(--accent);
}

.home-settings-btn svg {
  width: 16px;
  height: 16px;
}

/* API Key Warning Banner */
.home-api-warning {
  margin: 16px 32px;
  background: linear-gradient(135deg, rgba(255, 201, 16, 0.1), rgba(255, 105, 96, 0.1));
  border: 1px solid rgba(255, 201, 16, 0.3);
  border-radius: 12px;
  padding: 16px;
  animation: slideDown 0.3s ease-out;
}

.home-api-warning-content {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--warning);
}

.home-api-warning svg {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.home-api-warning button {
  margin-left: auto;
  background: none;
  border: 1px solid var(--warning);
  color: var(--warning);
  padding: 6px 12px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
}

.home-api-warning button:hover {
  background: var(--warning);
  color: var(--bg);
}

/* Home Content */
.home-content {
  padding: 32px;
  max-width: 1200px;
  margin: 0 auto;
  overflow-y: auto;
  height: calc(100vh - 120px);
}

/* Brand Shape Decorations */
.brand-shape-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.brand-shape-layer svg {
  position: absolute;
  pointer-events: none;
  width: 100%;
  height: 100%;
}
.home-section-divider {
  height: 40px;
  position: relative;
  overflow: hidden;
  pointer-events: none;
  margin: 8px 0;
}
.home-section-divider svg {
  width: 100%;
  height: 100%;
}
.wf-store-brand-shapes {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.wf-store-brand-shapes svg {
  position: absolute;
  pointer-events: none;
}

/* Announcements Banner */
.home-announcements {
  margin-bottom: 48px;
  animation: fadeInUp 0.6s ease-out;
}

.home-announcements-carousel {
  position: relative;
  overflow: hidden;
  border-radius: 16px;
  background: linear-gradient(135deg, var(--bg-card), var(--bg-surface));
  border: 1px solid var(--border);
  min-height: 180px;
}

.home-announcement-card {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: 32px;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease-in-out;
}

.home-announcement-card.has-custom-bg {
  color: #fff;
}

.home-announcement-card.has-custom-bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 30, 43, 0.24), rgba(0, 30, 43, 0.52));
  border-radius: inherit;
  z-index: 0;
}

.home-announcement-card.has-custom-bg > * {
  position: relative;
  z-index: 1;
}

[data-theme="light"] .home-announcement-card.has-custom-bg::before {
  background: linear-gradient(180deg, rgba(0, 30, 43, 0.34), rgba(0, 30, 43, 0.62));
}

.home-announcement-card.active {
  position: relative;
  opacity: 1;
  pointer-events: auto;
}

.home-announcement-card .badge {
  display: inline-block;
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: white;
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 600;
  margin-bottom: 16px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.home-announcement-card h3 {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent-text);
  margin-bottom: 12px;
}

.home-announcement-card p {
  color: var(--text-dim);
  line-height: 1.6;
  margin-bottom: 24px;
  max-width: 600px;
  margin-left: auto;
  margin-right: auto;
}

.home-announcement-card .cta {
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.home-announcement-card .cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 32px rgba(0, 212, 170, 0.3);
}

.home-announcement-card.has-custom-bg .home-announcement-dismiss {
  color: rgba(255, 255, 255, 0.82);
}

.home-announcement-card.has-custom-bg .home-announcement-dismiss:hover {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.home-announcement-dismiss {
  position: absolute;
  top: 16px;
  right: 16px;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  transition: all 0.2s;
}

.home-announcement-dismiss:hover {
  background: var(--bg-card);
  color: var(--text);
}

.home-announcements-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 16px;
}

.home-announcement-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
  cursor: pointer;
  transition: all 0.2s;
}

.home-announcement-dot.active {
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
}

.home-announcements-restore {
  text-align: center;
  margin-top: 12px;
}

.home-announcements-restore button {
  background: var(--bg-card);
  border: 1px dashed var(--border);
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
  padding: 8px 16px;
  border-radius: 8px;
  transition: all 0.2s;
}

.home-announcements-restore button:hover {
  color: var(--accent-text);
  border-color: var(--accent-text);
  background: var(--bg-surface);
}

.home-announcement-card.has-bg-image {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  min-height: 220px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.home-announcement-card.has-custom-bg h3,
.home-announcement-card.has-custom-bg p {
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
}

.home-announcement-card.has-custom-bg h3 {
  color: #fff;
}

.home-announcement-card.has-custom-bg p {
  color: rgba(255, 255, 255, 0.9);
}

.home-announcement-icon {
  font-size: 48px;
  margin-bottom: 12px;
  line-height: 1;
}

/* Section Headers */
.home-section {
  margin-bottom: 48px;
}

.home-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}

.home-section-header h3 {
  font-size: 20px;
  font-weight: 700;
  color: var(--accent-text);
}

.home-section-header a,
.home-marketplace-cta {
  color: var(--blue);
  text-decoration: none;
  font-weight: 500;
  transition: all 0.2s;
}

.home-marketplace-cta {
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.2s;
}

.home-marketplace-cta:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 16px rgba(0, 212, 170, 0.3);
}

.home-section-header a:hover {
  color: var(--accent);
}

/* Release Notes Timeline */
.home-releases-timeline {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.home-release-item {
  display: flex;
  gap: 16px;
  padding: 24px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  border-left: 4px solid transparent;
  border-left-color: #00D4AA;
  background: linear-gradient(90deg, transparent, var(--bg-card) 4px);
  transition: all 0.2s;
  animation: fadeInUp 0.6s ease-out;
}

.home-release-item:hover {
  background: var(--bg-surface);
  border-color: var(--accent);
}

.home-release-version {
  font-size: 16px;
  font-weight: 700;
  color: var(--accent-text);
  margin-bottom: 4px;
}

.home-release-date {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 12px;
}

.home-release-highlights {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.home-release-highlights li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  color: var(--text-dim);
  line-height: 1.5;
}

.home-release-highlights li:before {
  content: "•";
  color: var(--accent);
  font-weight: bold;
  flex-shrink: 0;
  margin-top: 2px;
}

/* Subsections */
.home-subsection {
  margin-bottom: 32px;
}

.home-subsection h4 {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 16px;
}

.home-subsection-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

/* Workflow Carousels */
.home-workflows-carousel {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  padding-bottom: 8px;
  scroll-snap-type: x mandatory;
}

.home-workflows-carousel::-webkit-scrollbar {
  height: 6px;
}

.home-workflows-carousel::-webkit-scrollbar-track {
  background: var(--bg-surface);
  border-radius: 3px;
}

.home-workflows-carousel::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
}

.home-workflow-card {
  flex: 0 0 280px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px;
  scroll-snap-align: start;
  transition: all 0.2s;
  animation: fadeInUp 0.6s ease-out;
}

.home-workflow-card:hover {
  background: var(--bg-surface);
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 8px 32px rgba(0, 212, 170, 0.1);
}

.home-workflow-card h5 {
  font-size: 14px;
  font-weight: 600;
  color: var(--accent-text);
  margin-bottom: 8px;
}

.home-workflow-card p {
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.5;
  margin-bottom: 12px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.home-workflow-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
}

.home-workflow-domain {
  background: rgba(0, 212, 170, 0.1);
  color: var(--accent);
  padding: 4px 8px;
  border-radius: 6px;
  font-size: 10px;
  font-weight: 500;
}

.home-workflow-author {
  font-size: 11px;
  color: var(--text-muted);
}

.home-workflow-actions {
  display: flex;
  gap: 8px;
}

.home-workflow-btn {
  flex: 1;
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 11px;
  cursor: pointer;
  transition: all 0.2s;
}

.home-workflow-btn:hover {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}

/* Domain Pills */
.home-domain-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.home-domain-pill {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px 16px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.home-domain-pill:hover {
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: white;
  border-color: transparent;
  transform: translateY(-1px);
}

/* Sort Tabs */
.home-sort-tabs {
  display: flex;
  gap: 2px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px;
}

.home-sort-tab {
  background: none;
  border: none;
  color: var(--text-muted);
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
  transition: all 0.2s;
}

.home-sort-tab.active {
  background: var(--accent);
  color: white;
}

.home-sort-tab:hover:not(.active) {
  background: var(--bg-card);
  color: var(--text);
}

/* Community Grid */
.home-community-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
}

.home-community-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 48px;
  color: var(--text-muted);
}

.home-community-empty h4 {
  font-size: 16px;
  margin-bottom: 8px;
  color: var(--text);
}

/* Quick Actions */
.home-quick-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

.home-quick-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 24px;
  cursor: pointer;
  transition: all 0.2s;
  text-decoration: none;
  color: var(--text);
  animation: fadeInUp 0.6s ease-out;
}

.home-quick-action:hover {
  background: var(--bg-surface);
  border-color: var(--accent);
  transform: translateY(-4px);
  box-shadow: 0 12px 40px rgba(0, 212, 170, 0.1);
  color: var(--text);
}

.home-quick-action svg {
  width: 24px;
  height: 24px;
  color: var(--accent);
}

.home-quick-action span {
  font-size: 14px;
  font-weight: 500;
  text-align: center;
}

/* Newsletter CTA — professional marketing block */
.home-newsletter-cta {
  text-align: center;
  padding: 48px 40px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: linear-gradient(145deg, var(--bg-card) 0%, var(--bg-surface) 100%);
  max-width: 600px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}

.home-newsletter-cta::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, #00D4AA, #40E0FF);
  opacity: 0.9;
}

[data-theme="light"] .home-newsletter-cta {
  box-shadow: 0 4px 24px rgba(0, 30, 43, 0.06);
}

.home-newsletter-cta-badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 16px;
  padding: 6px 12px;
  background: var(--accent-glow);
  border-radius: 20px;
}

.home-newsletter-cta-title {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent-text);
  margin: 0 0 12px 0;
  letter-spacing: -0.02em;
  line-height: 1.25;
}

.home-newsletter-cta-desc {
  font-size: 15px;
  color: var(--text-dim);
  line-height: 1.6;
  margin: 0 0 20px 0;
  max-width: 480px;
  margin-left: auto;
  margin-right: auto;
}

.home-newsletter-cta-benefits {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px 24px;
  margin-bottom: 28px;
  list-style: none;
}

.home-newsletter-cta-benefits li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-dim);
}

.home-newsletter-cta-benefits li svg {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.home-newsletter-cta-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: white;
  text-decoration: none;
  font-weight: 600;
  font-size: 15px;
  padding: 14px 28px;
  border-radius: 10px;
  transition: all 0.2s;
  box-shadow: 0 4px 16px rgba(0, 212, 170, 0.25);
}

.home-newsletter-cta-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 212, 170, 0.4);
}

.home-newsletter-cta-button .arrow {
  width: 16px;
  height: 16px;
  transition: transform 0.2s;
}

.home-newsletter-cta-button:hover .arrow {
  transform: translateX(3px);
}

.home-newsletter-cta-note {
  margin-top: 16px;
  font-size: 12px;
  color: var(--text-muted);
}

/* Footer */
.home-footer {
  margin-top: 64px;
  padding-top: 32px;
  border-top: 1px solid var(--border);
  text-align: center;
  position: relative;
  overflow: hidden;
}

.home-footer-links {
  display: flex;
  justify-content: center;
  gap: 24px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.home-footer-links a {
  color: var(--text-dim);
  text-decoration: none;
  font-size: 12px;
  transition: color 0.2s;
}

.home-footer-links a:hover {
  color: var(--accent);
}

.home-footer-branding {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--text-muted);
  font-size: 11px;
}

/* Simplified Home Dashboard */
.home-hero-panel,
.home-overview-card {
  background: linear-gradient(145deg, var(--bg-card) 0%, var(--bg-surface) 100%);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}

[data-theme="light"] .home-hero-panel,
[data-theme="light"] .home-overview-card {
  box-shadow: 0 8px 24px rgba(0, 30, 43, 0.05);
}

.home-hero-panel {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: 24px;
  padding: 28px;
  margin-bottom: 32px;
  position: relative;
  overflow: hidden;
}

.home-hero-panel::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at top right, rgba(64, 224, 255, 0.12), transparent 32%),
    radial-gradient(circle at bottom left, rgba(0, 212, 170, 0.09), transparent 28%);
  pointer-events: none;
}

.home-hero-copy,
.home-hero-side,
.home-hero-actions,
.home-footer-links,
.home-footer-branding {
  position: relative;
  z-index: 1;
}

.home-kicker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(0, 212, 170, 0.25);
  background: rgba(0, 212, 170, 0.08);
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.home-kicker::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.75;
}

.home-hero-title {
  margin: 0 0 12px 0;
  font-size: 34px;
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--accent-text);
}

.home-hero-description {
  margin: 0;
  max-width: 620px;
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1.65;
}

.home-hero-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.home-hero-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
}

.home-hero-side {
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: stretch;
}

.home-hero-robot-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 18px 20px;
  border: 1px solid rgba(0, 212, 170, 0.16);
  border-radius: 16px;
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.03), rgba(0, 212, 170, 0.04));
}

.home-hero-robot-copy {
  min-width: 0;
}

.home-hero-robot-kicker {
  display: inline-block;
  margin-bottom: 8px;
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.home-hero-robot-copy p {
  margin: 0;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.55;
  max-width: 220px;
}

.home-hero-robot {
  flex-shrink: 0;
  width: 112px;
  height: 112px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 16px;
  background: radial-gradient(circle at 50% 50%, rgba(64, 224, 255, 0.12), transparent 68%);
}

.home-hero-robot svg {
  display: block;
  width: 96px;
  height: 96px;
  image-rendering: pixelated;
  filter: drop-shadow(0 8px 24px rgba(0, 212, 170, 0.16));
}

.home-link-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.home-link-button:hover {
  color: var(--text);
  border-color: var(--accent);
  background: var(--bg-card);
}

.home-hero-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  align-content: start;
}

.home-primary-action {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 16px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border);
  border-radius: 14px;
  color: var(--text);
  cursor: pointer;
  transition: all 0.2s;
}

.home-primary-action:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  background: var(--bg-card);
  box-shadow: 0 10px 24px rgba(0, 212, 170, 0.08);
}

.home-primary-action-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: rgba(0, 212, 170, 0.12);
  color: var(--accent);
  flex-shrink: 0;
}

.home-primary-action-icon svg {
  width: 20px;
  height: 20px;
}

.home-primary-action-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.home-primary-action-body strong {
  font-size: 14px;
  line-height: 1.3;
  color: var(--accent-text);
}

.home-primary-action-body span {
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.5;
}

.home-demo-gallery-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 0.9fr);
  gap: 18px;
  width: 100%;
  margin-top: 24px;
  padding: 18px;
  min-height: 208px;
  border: 1px solid rgba(0, 212, 170, 0.16);
  border-radius: 18px;
  background:
    linear-gradient(145deg, rgba(255, 255, 255, 0.03), rgba(0, 212, 170, 0.05)),
    radial-gradient(circle at top right, rgba(64, 224, 255, 0.12), transparent 38%);
  overflow: hidden;
  text-decoration: none;
  transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}

.home-demo-gallery-card:hover {
  transform: translateY(-2px);
  border-color: rgba(0, 212, 170, 0.42);
  box-shadow: 0 16px 30px rgba(0, 212, 170, 0.08);
}

.home-demo-gallery-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  min-width: 0;
  padding: 0;
}

.home-demo-gallery-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 4px 9px;
  border-radius: 999px;
  border: 1px solid rgba(64, 224, 255, 0.22);
  background: rgba(64, 224, 255, 0.08);
  color: var(--blue);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.home-demo-gallery-badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.85;
}

.home-demo-gallery-title {
  display: block;
  margin: 0 0 8px 0;
  color: var(--accent-text);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.home-demo-gallery-description {
  display: block;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.65;
  max-width: 40ch;
}

.home-demo-gallery-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}

.home-demo-gallery-link svg {
  width: 14px;
  height: 14px;
  transition: transform 0.2s ease;
}

.home-demo-gallery-card:hover .home-demo-gallery-link svg {
  transform: translateX(2px);
}

.home-demo-gallery-media {
  position: relative;
  min-height: 168px;
  border: 1px solid rgba(0, 212, 170, 0.12);
  border-radius: 14px;
  background:
    linear-gradient(180deg, rgba(64, 224, 255, 0.06), rgba(0, 212, 170, 0.04)),
    radial-gradient(circle at 50% 10%, rgba(64, 224, 255, 0.12), transparent 48%),
    var(--bg);
  overflow: hidden;
}

.home-demo-gallery-media-label {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  padding: 4px 8px;
  border-radius: 999px;
  background: rgba(7, 17, 26, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--text-muted);
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.home-demo-gallery-media::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(7, 17, 26, 0.04), rgba(7, 17, 26, 0.46)),
    repeating-linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.015) 0px,
      rgba(255, 255, 255, 0.015) 1px,
      transparent 1px,
      transparent 6px
    );
  pointer-events: none;
}

.home-demo-gallery-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(0.96) contrast(1.02);
}

.home-overview-grid,
.home-main-grid {
  display: grid;
  gap: 24px;
}

.home-overview-grid {
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  margin-bottom: 24px;
}

.home-main-grid {
  grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
}

.home-overview-card {
  padding: 24px;
  margin-bottom: 0;
}

.home-overview-card .home-section-header {
  margin-bottom: 18px;
}

.home-overview-card .home-section-header h3 {
  font-size: 18px;
}

.home-status-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

.home-status-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 152px;
  padding: 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.home-status-card.is-ok {
  border-color: rgba(0, 212, 170, 0.28);
}

.home-status-card.is-warning {
  border-color: rgba(255, 201, 16, 0.32);
}

.home-status-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.home-status-value {
  font-size: 20px;
  font-weight: 700;
  color: var(--accent-text);
  letter-spacing: -0.02em;
}

.home-status-description {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.5;
  flex: 1;
}

.home-status-action {
  align-self: flex-start;
}

.home-resume-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 18px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  margin-bottom: 16px;
}

.home-resume-label {
  display: block;
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 6px;
}

.home-resume-title {
  display: block;
  color: var(--accent-text);
  font-size: 18px;
  font-weight: 700;
  margin-bottom: 4px;
}

.home-resume-description {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.5;
}

.home-recent-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.home-recent-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
}

.home-recent-item-main {
  min-width: 0;
}

.home-recent-item-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 2px;
}

.home-recent-item-meta {
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
}

.home-empty-state {
  padding: 20px;
  border: 1px dashed var(--border);
  border-radius: 14px;
  background: var(--bg);
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.6;
}

.home-update-card {
  padding: 20px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--bg);
}

.home-update-kicker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.home-update-title {
  margin: 0 0 6px 0;
  color: var(--accent-text);
  font-size: 22px;
}

.home-update-date {
  color: var(--text-muted);
  font-size: 12px;
  margin-bottom: 16px;
}

.home-update-summary {
  color: var(--text-dim);
  line-height: 1.6;
}

.home-update-highlights {
  margin: 16px 0 0 0;
  padding-left: 18px;
  color: var(--text-dim);
}

.home-update-highlights li + li {
  margin-top: 8px;
}

.home-workflow-stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.home-workflow-spotlight {
  padding: 16px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--bg);
}

.home-workflow-spotlight h4 {
  margin: 0 0 8px 0;
  font-size: 15px;
  color: var(--accent-text);
}

.home-workflow-spotlight p {
  margin: 0 0 12px 0;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.55;
}

.home-workflow-spotlight-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}

.home-workflow-spotlight .home-workflow-domain {
  background: rgba(0, 212, 170, 0.1);
  color: var(--accent);
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
}

.home-workflow-spotlight .home-workflow-author {
  color: var(--text-muted);
  font-size: 12px;
}

/* Animations */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Responsive */
@media (max-width: 1024px) {
  .home-content {
    padding: 24px;
  }

  .home-hero-panel,
  .home-overview-grid,
  .home-main-grid {
    grid-template-columns: 1fr;
  }

  .home-hero-robot-copy p {
    max-width: none;
  }

  .home-status-grid {
    grid-template-columns: 1fr;
  }
  
  .home-quick-actions {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  }
  
  .home-community-grid {
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  }
}

@media (max-width: 768px) {
  .home-header {
    padding: 16px 24px 12px;
  }
  
  .home-content {
    padding: 16px;
  }

  .home-hero-panel,
  .home-overview-card {
    padding: 20px;
  }

  .home-hero-title {
    font-size: 28px;
  }

  .home-hero-actions {
    grid-template-columns: 1fr;
  }

  .home-demo-gallery-card {
    grid-template-columns: 1fr;
  }

  .home-demo-gallery-media {
    min-height: 140px;
  }

  .home-hero-robot-wrap {
    flex-direction: column;
    align-items: flex-start;
  }

  .home-hero-robot {
    align-self: center;
  }

  .home-resume-main,
  .home-recent-item {
    flex-direction: column;
    align-items: flex-start;
  }
  
  .home-section-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  
  .home-footer-links {
    flex-direction: column;
    gap: 12px;
  }
}

/* ══════════════════════════════════════════════════════════════════
   Export Dialog
   ══════════════════════════════════════════════════════════════════ */
.export-modal-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
  z-index: 2000; display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
}
.export-modal-overlay.open { opacity: 1; pointer-events: auto; }
.export-modal {
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: 14px; max-width: 560px; width: 92%; max-height: 80vh;
  overflow-y: auto; padding: 24px; position: relative;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  animation: exportModalIn 0.2s ease-out;
}
@keyframes exportModalIn {
  from { opacity: 0; transform: scale(0.95) translateY(10px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}
.export-modal h2 { margin: 0 0 4px; font-size: 18px; color: var(--text); }
.export-modal .export-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 16px; }
.export-modal .export-close {
  position: absolute; top: 16px; right: 16px; background: none; border: none;
  color: var(--text-muted); font-size: 20px; cursor: pointer; padding: 4px 8px;
}
.export-modal .export-close:hover { color: var(--text); }
.export-format-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.export-format-chip {
  padding: 8px 16px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text-muted); cursor: pointer;
  font-size: 13px; transition: all 0.15s;
}
.export-format-chip:hover { border-color: var(--accent); color: var(--text); }
.export-format-chip.active { border-color: var(--accent); background: var(--accent); color: #fff; }
.export-preview {
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 0; margin: 12px 0; max-height: 360px; overflow: auto; position: relative;
}
.export-preview-label {
  position: sticky; top: 0; z-index: 1; padding: 6px 12px;
  font-size: 11px; color: var(--text-muted); background: var(--bg-surface);
  border-bottom: 1px solid var(--border); display: flex; justify-content: space-between;
}
.export-preview-source {
  padding: 12px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px;
  color: var(--text-muted); white-space: pre-wrap; word-break: break-all; line-height: 1.5;
}
.export-preview-source .json-key { color: #82aaff; }
.export-preview-source .json-str { color: #c3e88d; }
.export-preview-source .json-num { color: #f78c6c; }
.export-preview-source .json-bool { color: #c792ea; }
.export-preview-source .json-null { color: #546e7a; }
.export-preview-rendered {
  padding: 16px; color: var(--text); line-height: 1.6; font-size: 13px;
}
.export-preview-rendered h1 { font-size: 18px; margin: 0 0 8px; color: var(--accent); }
.export-preview-rendered h2 { font-size: 15px; margin: 16px 0 6px; color: var(--text); }
.export-preview-rendered h3 { font-size: 13px; margin: 12px 0 4px; }
.export-preview-rendered table { border-collapse: collapse; width: 100%; font-size: 12px; margin: 8px 0; }
.export-preview-rendered th, .export-preview-rendered td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
.export-preview-rendered th { background: var(--bg-surface); font-weight: 600; }
.export-preview-rendered blockquote { border-left: 3px solid var(--accent); margin: 8px 0; padding: 4px 12px; color: var(--text-muted); }
.export-preview-rendered code { background: var(--bg-surface); padding: 1px 4px; border-radius: 3px; font-size: 12px; }
.export-preview-rendered pre { background: var(--bg-surface); padding: 10px; border-radius: 6px; overflow-x: auto; font-size: 12px; }
.export-preview-rendered hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
.export-preview-rendered ul, .export-preview-rendered ol { padding-left: 20px; margin: 4px 0; }
.export-preview-rendered strong { color: var(--accent); }
.export-preview-image { display: flex; align-items: center; justify-content: center; padding: 16px; min-height: 120px; }
.export-preview-image img { max-width: 100%; max-height: 300px; border-radius: 4px; }
.export-preview-mermaid { padding: 16px; display: flex; justify-content: center; min-height: 120px; }
.export-preview-mermaid svg { max-width: 100%; }
.export-preview-tabs {
  display: flex; gap: 0; border-bottom: 1px solid var(--border); background: var(--bg-surface);
  position: sticky; top: 0; z-index: 2;
}
.export-preview-tab {
  padding: 6px 14px; font-size: 12px; cursor: pointer; border: none; background: none;
  color: var(--text-muted); border-bottom: 2px solid transparent; transition: all 0.15s;
}
.export-preview-tab:hover { color: var(--text); }
.export-preview-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.export-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.export-actions button {
  padding: 8px 20px; border-radius: 8px; font-size: 13px; cursor: pointer;
  border: 1px solid var(--border); background: var(--bg); color: var(--text);
  transition: all 0.15s;
}
.export-actions button:hover { border-color: var(--accent); }
.export-actions .export-primary {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
.export-actions .export-primary:hover { opacity: 0.9; }
.export-snack {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--accent); color: #fff; padding: 10px 24px; border-radius: 8px;
  font-size: 13px; z-index: 3000; animation: exportSnackIn 0.2s ease-out;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
@keyframes exportSnackIn {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.export-toolbar-btn {
  background: none; border: 1px solid var(--border); border-radius: 6px;
  color: var(--text-muted); cursor: pointer; padding: 4px 8px; display: inline-flex;
  align-items: center; gap: 4px; font-size: 12px; transition: all 0.15s;
}
.export-toolbar-btn:hover { border-color: var(--accent); color: var(--accent); }
.export-toolbar-btn svg { width: 14px; height: 14px; }

/* ══════════════════════════════════════════════════════════════════
   Command Palette
   ══════════════════════════════════════════════════════════════════ */

.command-palette-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(8px);
  z-index: 3000;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 20vh;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease-out;
}

.command-palette-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

.command-palette {
  background: var(--bg-card);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  width: 640px;
  max-width: 90vw;
  max-height: 480px;
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4);
  transform: scale(0.98);
  transition: transform 0.12s ease-out;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.command-palette-overlay.open .command-palette {
  transform: scale(1);
}

.command-palette-input {
  border: none;
  background: none;
  font-family: var(--font);
  font-size: 16px;
  color: var(--text);
  padding: 16px 20px;
  outline: none;
  border-bottom: 1px solid var(--border);
}

.command-palette-input::placeholder {
  color: var(--text-muted);
}

.command-palette-results {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}

.command-palette-empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--text-muted);
  font-size: 14px;
}

.command-palette-section-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 12px 16px 4px;
  margin-top: 8px;
}

.command-palette-section-title:first-child {
  margin-top: 0;
}

.command-palette-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  margin: 2px 4px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.1s;
  border: 2px solid transparent;
}

.command-palette-item:hover,
.command-palette-item.selected {
  background: rgba(0, 212, 170, 0.12);
}

.command-palette-item.selected {
  border-left-color: var(--accent);
}

.command-palette-icon {
  font-size: 16px;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  opacity: 0.8;
}

.command-palette-content {
  flex: 1;
  min-width: 0;
}

.command-palette-label {
  font-size: 14px;
  color: var(--text);
  margin-bottom: 2px;
}

.command-palette-description {
  font-size: 12px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.command-palette-badge {
  font-size: 10px;
  font-weight: 600;
  padding: 2px 6px;
  border-radius: 4px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  flex-shrink: 0;
}

.command-palette-badge.navigation { background: rgba(64, 224, 255, 0.2); color: #40E0FF; }
.command-palette-badge.action { background: rgba(0, 212, 170, 0.2); color: #00D4AA; }
.command-palette-badge.workflow { background: rgba(206, 147, 216, 0.2); color: #CE93D8; }
.command-palette-badge.setting { background: rgba(255, 183, 77, 0.2); color: #FFB74D; }
.command-palette-badge.explainer { background: rgba(102, 187, 106, 0.2); color: #66BB6A; }
.command-palette-badge.recent { background: rgba(158, 158, 158, 0.2); color: #9E9E9E; }

/* Sub-palette header with back arrow */
.command-palette-header {
  display: flex;
  align-items: center;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
}

.command-palette-back {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 16px;
  cursor: pointer;
  padding: 4px;
  margin-right: 8px;
  border-radius: 4px;
  transition: all 0.1s;
}

.command-palette-back:hover {
  background: rgba(0, 212, 170, 0.12);
  color: var(--accent);
}

.command-palette-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
}

/* Preview pane */
.command-palette-with-preview {
  display: flex;
}

.command-palette-main {
  flex: 1;
  min-width: 0;
}

.command-palette-preview {
  width: 240px;
  border-left: 1px solid var(--border);
  background: var(--bg-surface);
  flex-shrink: 0;
}

.command-palette-preview-content {
  padding: 16px;
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.4;
  max-height: 200px;
  overflow-y: auto;
}

.command-palette-preview-title {
  font-weight: 600;
  color: var(--text);
  margin-bottom: 8px;
}

.command-palette-preview-description {
  margin-bottom: 12px;
}

.command-palette-preview-steps {
  list-style: none;
  padding: 0;
  margin: 0;
}

.command-palette-preview-steps li {
  padding: 2px 0;
  position: relative;
  padding-left: 12px;
}

.command-palette-preview-steps li::before {
  content: '•';
  position: absolute;
  left: 0;
  color: var(--accent);
}

/* Transition effects */
.command-palette-results-transition {
  opacity: 1;
  transition: opacity 0.15s ease;
}

.command-palette-results-transition.fade-out {
  opacity: 0;
}

/* Recent items clock icon */
.command-palette-item.recent .command-palette-icon::before {
  content: '⏱';
}

/* Reduced motion support - Phase 3 */
@media (prefers-reduced-motion: reduce) {
  .command-palette-overlay {
    transition: none;
  }
  
  .command-palette {
    transition: none;
  }
  
  .command-palette-results-transition {
    transition: none;
  }
  
  .command-palette-item {
    transition: none;
  }
  
  .command-palette-back {
    transition: none;
  }
}

/* Light mode refinements - Phase 3 */
[data-theme="light"] .command-palette-overlay {
  background: rgba(0, 30, 43, 0.4);
  backdrop-filter: blur(8px);
}

[data-theme="light"] .command-palette {
  background: #FFFFFF;
  border: 1px solid #E8EDEB;
  box-shadow: 0 24px 48px rgba(0, 30, 43, 0.15);
}

[data-theme="light"] .command-palette-input {
  color: #001E2B;
  border-bottom-color: #E8EDEB;
}

[data-theme="light"] .command-palette-input::placeholder {
  color: #889397;
}

[data-theme="light"] .command-palette-item:hover,
[data-theme="light"] .command-palette-item.selected {
  background: rgba(0, 158, 128, 0.08);
}

[data-theme="light"] .command-palette-item.selected {
  border-left-color: #009E80;
}

[data-theme="light"] .command-palette-back:hover {
  background: rgba(0, 158, 128, 0.08);
  color: #009E80;
}

[data-theme="light"] .command-palette-header {
  border-bottom-color: #E8EDEB;
}

[data-theme="light"] .command-palette-preview {
  background: #F9FBFA;
  border-left-color: #E8EDEB;
}

[data-theme="light"] .command-palette-footer {
  background: #F9FBFA;
  border-top-color: #E8EDEB;
}

/* Long workflow name truncation - Phase 3 Edge Case */
.command-palette-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 350px; /* Approximately 50 characters at 14px font */
}

/* Hide preview on narrow viewports */
@media (max-width: 800px) {
  .command-palette-preview {
    display: none;
  }
}

/* Onboarding tooltip - Phase 3 */
.onboarding-tooltip {
  position: fixed;
  top: 60px;
  right: 20px;
  background: linear-gradient(135deg, #00D4AA, #40E0FF);
  color: #FFFFFF;
  padding: 8px 12px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 500;
  opacity: 0;
  transform: translateY(-10px);
  transition: all 0.3s ease;
  z-index: 2000;
  pointer-events: none;
  box-shadow: 0 4px 16px rgba(0, 212, 170, 0.3);
}

.onboarding-tooltip.show {
  opacity: 1;
  transform: translateY(0);
}

[data-theme="light"] .onboarding-tooltip {
  background: linear-gradient(135deg, #009E80, #0088CC);
  box-shadow: 0 4px 16px rgba(0, 158, 128, 0.2);
}

/* Reduce motion for onboarding tooltip */
@media (prefers-reduced-motion: reduce) {
  .onboarding-tooltip {
    transition: opacity 0.1s;
    transform: none;
  }
  
  .onboarding-tooltip.show {
    transform: none;
  }
}

.command-palette-footer {
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  background: var(--bg-surface);
  font-size: 11px;
  color: var(--text-muted);
  display: flex;
  gap: 16px;
  align-items: center;
}

.command-palette-footer span {
  display: flex;
  align-items: center;
  gap: 4px;
}

.command-palette-key {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 4px;
  font-family: var(--mono);
  font-size: 9px;
}

/* Search icon in sidebar header */
.sidebar-search-btn {
  -webkit-app-region: no-drag;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 4px;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s;
  flex-shrink: 0;
}

.sidebar-search-btn:hover { 
  color: var(--text); 
  background: rgba(255,255,255,0.06); 
}

[data-theme="light"] .sidebar-search-btn:hover { 
  background: rgba(0,0,0,0.04); 
}

.sidebar.collapsed .sidebar-search-btn { 
  display: none; 
}

/* ── KB Wizard Modal ── */
.kb-wizard {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

.kb-wizard.visible {
  opacity: 1;
  pointer-events: auto;
}

.kb-wizard-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(2px);
}

.kb-wizard-card {
  position: relative;
  z-index: 1;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 16px;
  padding: 40px;
  width: 500px;
  max-width: 90vw;
  max-height: 80vh;
  overflow-y: auto;
  box-shadow: 0 20px 60px rgba(0,0,0,0.4), 0 0 80px rgba(0,212,170,0.08);
  animation: slideUp 0.3s cubic-bezier(0.4,0,0.2,1);
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.kb-wizard-close {
  position: absolute;
  top: 16px;
  right: 16px;
  background: none;
  border: none;
  font-size: 28px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 8px;
  transition: color 0.15s;
}

.kb-wizard-close:hover {
  color: var(--text);
}

.kb-wizard-step {
  text-align: center;
}

.kb-wizard-icon {
  font-size: 48px;
  margin-bottom: 16px;
}

.kb-wizard-title {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent-text);
  margin-bottom: 8px;
}

.kb-wizard-desc {
  font-size: 14px;
  color: var(--text-dim);
  margin-bottom: 28px;
  line-height: 1.6;
}

.kb-wizard-options {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.kb-wizard-option {
  text-align: left;
}

.kb-wizard-select {
  width: 100%;
  padding: 10px 12px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font);
  font-size: 13px;
  transition: border-color 0.15s;
}

.kb-wizard-select:focus {
  outline: none;
  border-color: var(--accent);
}

.kb-wizard-divider {
  text-align: center;
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.kb-wizard-dropzone {
  border: 2px dashed var(--border);
  border-radius: 12px;
  padding: 28px 16px;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
}

.kb-wizard-dropzone:hover {
  border-color: var(--accent);
  background: var(--accent-glow);
}

/* ── KB Panel (Sidebar) ── */
.kb-panel {
  width: 300px;
  min-width: 300px;
  background: var(--bg-surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  transition: width 0.25s ease, min-width 0.25s ease, border-color 0.25s ease;
  flex-shrink: 0;
}

.kb-panel.collapsed {
  width: 0;
  min-width: 0;
  border-right-color: transparent;
  overflow: hidden;
}

.kb-panel-header {
  padding: 12px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
}

.kb-panel-title {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  margin: 0;
}

.kb-panel-collapse-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 16px;
  cursor: pointer;
  padding: 4px;
  transition: transform 0.2s;
}

.kb-panel.collapsed .kb-panel-collapse-btn {
  transform: rotate(180deg);
}

.kb-panel-content {
  padding: 12px;
  flex: 1;
  overflow-y: auto;
}

.kb-panel-kb-info {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 16px;
}

.kb-panel-stat {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
}

.kb-panel-stat:last-child {
  border-bottom: none;
}

.kb-panel-stat-label {
  color: var(--text-muted);
  font-weight: 600;
}

.kb-panel-stat-value {
  color: var(--accent);
  font-weight: 600;
  font-family: var(--mono);
}

.kb-panel-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}

.kb-panel-btn {
  padding: 8px 12px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-family: var(--font);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
  text-align: left;
}

.kb-panel-btn:hover {
  background: var(--bg-card);
  border-color: var(--accent);
  color: var(--accent);
}

.kb-panel-btn-primary {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}

.kb-panel-btn-primary:hover {
  background: var(--accent-dim);
  border-color: var(--accent-dim);
  color: var(--bg);
}

.kb-panel-btn-secondary {
  background: transparent;
}

.kb-panel-btn-danger {
  color: var(--error);
  border-color: var(--error);
}

.kb-panel-btn-danger:hover {
  background: rgba(255,105,96,0.1);
}

.kb-panel-dropzone {
  border: 2px dashed var(--border);
  border-radius: 8px;
  padding: 20px 12px;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
  margin-bottom: 16px;
}

.kb-panel-dropzone:hover,
.kb-panel-dropzone.drag-over {
  border-color: var(--accent);
  background: var(--accent-glow);
}

.kb-panel-dropzone svg {
  color: var(--accent);
  margin-bottom: 4px;
}

.kb-panel-progress {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  margin-top: 12px;
}

.kb-panel-progress-label {
  font-size: 11px;
  color: var(--text-muted);
  margin-bottom: 8px;
  word-break: break-word;
}

.kb-panel-progress-bar {
  width: 100%;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}

.kb-panel-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent-dim));
  border-radius: 2px;
  transition: width 0.3s ease;
  width: 0%;
}

/* ── KB Panel Sections (collapsible) ── */
.kb-section {
  border-bottom: 1px solid var(--border);
}
.kb-section:last-child { border-bottom: none; }

.kb-section-header {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 10px 12px;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
  user-select: none;
}
.kb-section-header:hover {
  color: var(--text);
  background: rgba(255,255,255,0.03);
}
[data-theme="light"] .kb-section-header:hover { background: rgba(0,0,0,0.02); }

.kb-section-header .kb-chevron {
  font-size: 10px;
  transition: transform 0.2s;
  display: inline-block;
}
.kb-section-header.expanded .kb-chevron {
  transform: rotate(90deg);
}

.kb-section-header .kb-section-badge {
  margin-left: auto;
  font-size: 10px;
  font-weight: 600;
  color: var(--accent);
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 140px;
  text-align: right;
}
.kb-name-editable { cursor: pointer; border-bottom: 1px dashed transparent; transition: border-color 0.2s; }
.kb-name-editable:hover { border-bottom-color: var(--accent); }
.kb-name-input {
  background: var(--bg-secondary, #1a1d23);
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: 10px;
  font-weight: 600;
  font-family: var(--mono);
  padding: 1px 4px;
  border-radius: 3px;
  max-width: 140px;
  outline: none;
}

.kb-section-body {
  overflow: hidden;
  transition: max-height 0.2s ease-out;
}
.kb-section-body-inner { padding: 0 12px 12px; }

/* ── KB Files List ── */
.kb-file-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 200px;
  overflow-y: auto;
}
.kb-file-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.kb-file-item:last-child { border-bottom: none; }
.kb-file-icon { flex-shrink: 0; color: var(--accent); }
.kb-file-info { flex: 1; min-width: 0; }
.kb-file-name {
  color: var(--text);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kb-file-meta {
  font-size: 10px;
  color: var(--text-muted);
  display: flex;
  gap: 6px;
}
.kb-file-delete {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px;
  border-radius: 4px;
  transition: color 0.15s, background 0.15s;
  opacity: 0;
}
.kb-file-item:hover .kb-file-delete { opacity: 1; }
.kb-file-delete:hover { color: var(--error); background: rgba(255,105,96,0.1); }
.kb-file-empty {
  text-align: center;
  color: var(--text-muted);
  font-size: 12px;
  padding: 12px 0;
}

/* ── KB Panel Config Fields (compact) ── */
.kb-config-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}
.kb-config-row:last-child { margin-bottom: 0; }
.kb-config-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
}
.kb-config-row .settings-select,
.kb-config-row .settings-input {
  min-width: unset;
  width: 100%;
  font-size: 12px;
  padding: 6px 8px;
  text-align: left;
}
.kb-config-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* ── Embedding Badge Styles ── */
.embed-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 6px;
  border-radius: 8px;
  margin-left: 6px;
  vertical-align: middle;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  white-space: nowrap;
}
.embed-badge-local { background: #dcfce7; color: #166534; }
.embed-badge-api { background: #dbeafe; color: #1e40af; }
.embed-option-price { font-size: 10px; color: var(--text-muted); margin-left: auto; padding-left: 8px; }
.embed-option-disabled { opacity: 0.45; cursor: not-allowed; }
[data-theme="light"] .embed-badge-local { background: #dcfce7; color: #166534; }
[data-theme="light"] .embed-badge-api { background: #dbeafe; color: #1e40af; }

/* ── Health Dot Indicators ── */
.health-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-left: 6px;
  vertical-align: middle;
  flex-shrink: 0;
}
.health-dot.available {
  background: #22c55e;
  box-shadow: 0 0 4px rgba(34, 197, 94, 0.4);
}
.health-dot.unavailable {
  background: #ef4444;
  box-shadow: 0 0 4px rgba(239, 68, 68, 0.3);
}

/* ── Onboarding Banner ── */
.onboarding-banner {
  margin: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg, var(--surface));
  padding: 20px;
  text-align: center;
}
.onboarding-banner-title {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 12px;
  color: var(--text);
}
.onboarding-banner-services {
  display: flex;
  justify-content: center;
  gap: 16px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
.onboarding-service-item {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  color: var(--text-muted);
}
.onboarding-service-item .health-dot {
  margin-left: 0;
}
.onboarding-banner-recommendation {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 16px;
  font-style: italic;
}
.onboarding-banner-actions {
  display: flex;
  justify-content: center;
  gap: 8px;
}

/* Instructions textarea compact */
.kb-instructions-textarea {
  width: 100%;
  min-height: 60px;
  max-height: 120px;
  resize: vertical;
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 8px;
  font-size: 12px;
  font-family: var(--font);
  line-height: 1.4;
  box-sizing: border-box;
}
.kb-instructions-textarea:focus { outline: none; border-color: var(--accent); }

/* Compact drop zone */
.kb-panel-dropzone-compact {
  border: 2px dashed var(--border);
  border-radius: 8px;
  padding: 12px 8px;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
}
.kb-panel-dropzone-compact:hover,
.kb-panel-dropzone-compact.drag-over {
  border-color: var(--accent);
  background: var(--accent-glow);
}
.kb-panel-dropzone-compact svg { color: var(--accent); }

/* ── Chat tab layout ── */
/* Expand .main when chat tab is active to fill viewport */
.main:has(#tab-chat.active) {
  max-width: none; padding: 0; overflow: hidden;
}
#tab-chat.tab-panel.active {
  display: flex;
  position: relative;
  overflow: hidden;
  height: 100%;
  padding-bottom: 40px; /* Clear the fixed cost status bar */
}

.chat-container {
  flex: 1;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

</style>
</head>
<body>

<!-- Lucide Icons (lucide.dev) — stroke-based, 16×16 -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
  <!-- Zap (Embed) -->
  <symbol id="lg-lightning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
  </symbol>
  <!-- Arrow Left Right (Compare) -->
  <symbol id="lg-arrows" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>
  </symbol>
  <!-- Search -->
  <symbol id="lg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
  </symbol>
  <!-- Gauge (Benchmark) -->
  <symbol id="lg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>
  </symbol>
  <!-- Lightbulb (Explore) -->
  <symbol id="lg-bulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>
  </symbol>
  <!-- Info (About) -->
  <symbol id="lg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
  </symbol>
  <!-- Image (Multimodal) -->
  <symbol id="lg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
  </symbol>
  <!-- Settings (Config) -->
  <symbol id="lg-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>
  </symbol>
  <!-- Code (Generate) -->
  <symbol id="lg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
  </symbol>
  <!-- Palette (Theme) -->
  <symbol id="lg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
  </symbol>
  <!-- Box (Cube) -->
  <symbol id="lg-cube" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
  </symbol>
  <!-- Message Square (Chat) -->
  <symbol id="lg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
  </symbol>
  <!-- Shield -->
  <symbol id="lg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6z"/>
  </symbol>
  <!-- Activity (Pulse) -->
  <symbol id="lg-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2"/>
  </symbol>
  <!-- Brain (Thinking) -->
  <symbol id="lg-brain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/>
  </symbol>
  <!-- Sparkles -->
  <symbol id="lg-sparkles" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>
  </symbol>
  <!-- Target (Similarity) -->
  <symbol id="lg-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>
  </symbol>
  <!-- Database (Collections) -->
  <symbol id="lg-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/>
  </symbol>
  <!-- Bot (Models) -->
  <symbol id="lg-bot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
  </symbol>
  <!-- ArrowUpDown (Rerank) -->
  <symbol id="lg-arrow-up-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/>
  </symbol>
  <!-- Coins (Cost) -->
  <symbol id="lg-coins" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/>
  </symbol>
  <!-- Inbox (Ingest) -->
  <symbol id="lg-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
  </symbol>
  <!-- SearchCode (Vector Search) -->
  <symbol id="lg-search-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="m13 13.5 2-2.5-2-2.5"/><path d="m9 8.5-2 2.5 2 2.5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
  </symbol>
  <!-- Plug (MCP Integrations) -->
  <symbol id="lg-plug" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
    <path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a6 6 0 0 1-6 6 6 6 0 0 1-6-6V8z"/>
  </symbol>
</svg>

<div class="app-shell">

<!-- Sidebar -->
<aside class="sidebar">
  <div class="sidebar-drag-region">
    <button class="sidebar-brand" id="sidebarHomeBtn" type="button" title="Overview">
      <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
      <span class="sidebar-brand-copy">
        <span class="sidebar-title">Vai</span>
        <span class="sidebar-subtitle">Overview dashboard</span>
      </span>
    </button>
    <div class="sidebar-header-actions">
      <button class="sidebar-search-btn" onclick="toggleCommandPalette()" title="Search commands (⌘K)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></button>
      <button class="sidebar-collapse-btn" id="sidebarCollapseBtn" onclick="toggleSidebarCollapse()" title="Toggle sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
    </div>
  </div>
  <div class="sidebar-nav-shell">
    <nav class="sidebar-rail" aria-label="Primary navigation">
      <button class="sidebar-rail-btn active" data-section="build" data-default-tab="embed" data-tooltip="Build" type="button">
        <span class="sidebar-rail-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span>
        <span class="sidebar-rail-btn-label">Build</span>
      </button>
      <button class="sidebar-rail-btn" data-section="evaluate" data-default-tab="compare" data-tooltip="Evaluate" type="button">
        <span class="sidebar-rail-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span>
        <span class="sidebar-rail-btn-label">Evaluate</span>
      </button>
      <button class="sidebar-rail-btn" data-section="reference" data-default-tab="models" data-tooltip="Reference" type="button">
        <span class="sidebar-rail-btn-icon" aria-hidden="true"><svg><use href="#lg-bot"/></svg></span>
        <span class="sidebar-rail-btn-label">Reference</span>
      </button>
      <button class="sidebar-rail-btn" data-section="settings" data-default-tab="settings" data-tooltip="Settings" type="button">
        <span class="sidebar-rail-btn-icon" aria-hidden="true"><svg><use href="#lg-config"/></svg></span>
        <span class="sidebar-rail-btn-label">Settings</span>
      </button>
    </nav>
    <div class="sidebar-panel">
      <div class="sidebar-panel-header">
        <div class="sidebar-panel-eyebrow" id="sidebarPanelEyebrow">Build</div>
        <div class="sidebar-panel-title" id="sidebarPanelTitle">Create and run core workflows</div>
      </div>
      <nav class="sidebar-nav" aria-label="Section navigation">
        <div class="sidebar-nav-group active" data-section="build" role="tablist" aria-label="Build tools">
          <div class="sidebar-nav-section">Core tools</div>
          <button class="tab-btn" data-tab="embed" data-tooltip="Embed" role="tab" aria-selected="false" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
          <button class="tab-btn" data-tab="search" data-tooltip="Rerank" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Rerank</span></button>
          <button class="tab-btn" data-tab="multimodal" data-tooltip="Multimodal" role="tab" aria-selected="false" aria-controls="tab-multimodal" id="tab-btn-multimodal"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
          <div class="sidebar-nav-section">Advanced tools</div>
          <button class="tab-btn" data-tab="generate" data-tooltip="Generate" role="tab" aria-selected="false" aria-controls="tab-generate" id="tab-btn-generate"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-code"/></svg></span><span>Generate</span></button>
          <button class="tab-btn" data-tab="chat" data-tooltip="Chat" role="tab" aria-selected="false" aria-controls="tab-chat" id="tab-btn-chat"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-chat"/></svg></span><span>Chat</span></button>
          <button class="tab-btn" data-tab="workflows" data-tooltip="Workflows" role="tab" aria-selected="false" aria-controls="tab-workflows" id="tab-btn-workflows"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="19" cy="18" r="3"/><line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/><line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/></svg></span><span>Workflows</span></button>
        </div>
        <div class="sidebar-nav-group" data-section="evaluate" role="tablist" aria-label="Evaluate tools" hidden>
          <div class="sidebar-nav-section">Inspect and compare</div>
          <button class="tab-btn" data-tab="compare" data-tooltip="Compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
          <button class="tab-btn" data-tab="benchmark" data-tooltip="Benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
          <button class="tab-btn" data-tab="optimize" data-tooltip="Optimize" role="tab" aria-selected="false" aria-controls="tab-optimize" id="tab-btn-optimize"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v8m4-4H8"/><circle cx="12" cy="12" r="10"/></svg></span><span>Optimize</span></button>
          <button class="tab-btn" data-tab="explore" data-tooltip="Explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
          <button class="tab-btn" data-tab="nano" data-tooltip="Local Inference" role="tab" aria-selected="false" aria-controls="tab-nano" id="tab-btn-nano"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg></span><span>Local Inference</span></button>
        </div>
        <div class="sidebar-nav-group" data-section="reference" role="tablist" aria-label="Reference tools" hidden>
          <div class="sidebar-nav-section">Library</div>
          <button class="tab-btn" data-tab="models" data-tooltip="Models" role="tab" aria-selected="false" aria-controls="tab-models" id="tab-btn-models"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bot"/></svg></span><span>Models</span></button>
          <button class="tab-btn" data-tab="about" data-tooltip="About" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
        </div>
        <div class="sidebar-nav-group" data-section="settings" role="tablist" aria-label="Settings" hidden>
          <div class="sidebar-nav-section">Preferences</div>
          <button class="tab-btn" data-tab="settings" data-tooltip="Settings" role="tab" aria-selected="false" aria-controls="tab-settings" id="tab-btn-settings"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-config"/></svg></span><span>Settings</span></button>
        </div>
      </nav>
    </div>
  </div>
  <div class="sidebar-footer">
    <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
      <div style="display:flex;align-items:center;gap:5px;">
        <div class="status-dot" id="statusDot"></div>
        <span class="status-label" id="statusLabel">Checking...</span>
      </div>
      <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
    </div>
    <div style="display:flex;align-items:center;justify-content:space-between;">
      <div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
      <div style="display:flex;align-items:center;gap:6px;">
        <a class="sidebar-docs-link" href="https://docs.vaicli.com" target="_blank" rel="noopener" title="Documentation (F1)">
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/><path d="M6 8h4M6 10.5h4"/></svg>
          <span>Docs</span>
        </a>
        <button class="sidebar-bug-link" id="bugButton" title="Report a Bug">
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="9" r="4.5"/><path d="M5.5 5.5L3 3M10.5 5.5L13 3M3 9H1M15 9h-2M5.5 12.5L4 15M10.5 12.5L12 15"/></svg>
          <span class="sidebar-bug-label">Bug</span>
        </button>
      </div>
    </div>
  </div>
</aside>

<!-- Content -->
<div class="content-area">
  <div class="content-drag-region"></div>
  <div class="update-banner" id="updateBanner">
    <span class="update-banner-icon">🚀</span>
    <span class="update-banner-text" id="updateBannerText">
      <strong>Vai <span id="updateVersion"></span></strong> is available.
      You're on <span id="currentVersion"></span>.
    </span>
    <div class="update-progress" id="updateProgress" style="display:none;">
      <div class="update-progress-bar"><div class="update-progress-fill" id="updateProgressFill"></div></div>
      <span class="update-progress-label" id="updateProgressLabel">0%</span>
    </div>
    <button class="update-banner-btn" id="updateDownloadBtn">Download &amp; Install</button>
    <button class="update-banner-btn" id="updateInstallBtn" style="display:none;">Restart &amp; Update</button>
    <button class="update-banner-dismiss" id="updateDismissBtn" title="Dismiss">×</button>
  </div>
  <div class="main">

<!-- ========== HOME TAB ========== -->
<div class="tab-panel active" id="tab-home" role="tabpanel" aria-labelledby="sidebarHomeBtn" tabindex="0">
  <!-- Header Bar -->
  <div class="home-header">
    <div class="home-header-left">
      <img class="home-logo" id="homeLogo" src="/icons/V.png" alt="Vai" onerror="this.src='/icons/dark/64.png'">
      <span class="home-header-title">Voyage AI Playground</span>
      <span class="home-version-badge" id="homeVersionBadge">v1.0.0</span>
    </div>
    <div class="home-header-right">
      <button class="home-settings-btn" onclick="switchTab('settings')" title="Settings">
        <svg width="20" height="20" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
      </button>
    </div>
  </div>

  <div class="home-content">
    <section class="home-hero-panel">
      <div class="home-hero-copy">
        <span class="home-kicker">Desktop workspace</span>
        <h2 class="home-hero-title">Build, test, and ship with Voyage AI</h2>
        <p class="home-hero-description">Use one focused desktop app to generate embeddings, compare models, rerank results, and run production-friendly workflows without bouncing between docs, scripts, and browser tabs.</p>
        <div class="home-hero-links">
          <button class="home-link-button" onclick="switchTab('settings')">Open settings</button>
          <button class="home-link-button" onclick="toggleCommandPalette()">Command palette</button>
          <a class="home-link-button" href="https://docs.vaicli.com" target="_blank" rel="noopener">Documentation</a>
        </div>
        <a
          class="home-demo-gallery-card"
          href="https://vaicli.com/demos"
          target="_blank"
          rel="noopener"
          title="Open the VAI demo gallery"
          onclick="if (typeof sendTelemetry === 'function') sendTelemetry('open_demo_gallery')"
        >
          <span class="home-demo-gallery-copy">
            <span class="home-demo-gallery-badge">New</span>
            <span>
              <strong class="home-demo-gallery-title">Demo gallery</strong>
              <span class="home-demo-gallery-description">Watch tiny terminal demos, copy the commands, and jump from the desktop app into the full public gallery.</span>
            </span>
            <span class="home-demo-gallery-link">
              Explore the gallery
              <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
                <path d="M4 12 12 4"/>
                <path d="M6 4h6v6"/>
              </svg>
            </span>
          </span>
          <span class="home-demo-gallery-media" aria-hidden="true">
            <video
              class="home-demo-gallery-video"
              autoplay
              muted
              loop
              playsinline
              preload="metadata"
              poster="/assets/home/demo-gallery-preview.png"
            >
              <source src="/assets/home/demo-gallery-preview.mp4" type="video/mp4">
            </video>
          </span>
        </a>
      </div>
      <div class="home-hero-side">
        <div class="home-hero-robot-wrap">
          <div class="home-hero-robot-copy">
            <span class="home-hero-robot-kicker">Workspace guide</span>
            <p>Your pixel robot lives in the corner now, adding personality without turning the screen into another promo panel.</p>
          </div>
          <div class="home-hero-robot" aria-hidden="true">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160" shape-rendering="crispEdges">
              <rect x="40" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="20" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="30" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="40" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="40" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="40" width="10" height="10" fill="#0d1117"/>
              <rect x="70" y="40" width="10" height="10" fill="#0d1117"/>
              <rect x="100" y="40" width="10" height="10" fill="#0d1117"/>
              <rect x="110" y="40" width="10" height="10" fill="#0d1117"/>
              <rect x="130" y="40" width="10" height="10" fill="#00d4aa"/>
              <rect x="140" y="40" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="50" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="50" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="50" width="10" height="10" fill="#0d1117"/>
              <rect x="70" y="50" width="10" height="10" fill="#40e0ff"/>
              <rect x="100" y="50" width="10" height="10" fill="#0d1117"/>
              <rect x="110" y="50" width="10" height="10" fill="#40e0ff"/>
              <rect x="130" y="50" width="10" height="10" fill="#00d4aa"/>
              <rect x="140" y="50" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="60" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="70" width="10" height="10" fill="#00d4aa"/>
              <rect x="20" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="130" y="80" width="10" height="10" fill="#00d4aa"/>
              <rect x="20" y="90" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="90" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="70" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="80" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="90" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="100" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="110" y="90" width="10" height="10" fill="#0d1117"/>
              <rect x="130" y="90" width="10" height="10" fill="#00d4aa"/>
              <rect x="140" y="90" width="10" height="10" fill="#00d4aa"/>
              <rect x="20" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="100" width="10" height="10" fill="#0d1117"/>
              <rect x="70" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="100" width="10" height="10" fill="#40e0ff"/>
              <rect x="90" y="100" width="10" height="10" fill="#0d1117"/>
              <rect x="100" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="130" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="140" y="100" width="10" height="10" fill="#00d4aa"/>
              <rect x="20" y="110" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="110" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="110" width="10" height="10" fill="#0d1117"/>
              <rect x="70" y="110" width="10" height="10" fill="#0d1117"/>
              <rect x="80" y="110" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="110" width="10" height="10" fill="#0d1117"/>
              <rect x="100" y="110" width="10" height="10" fill="#0d1117"/>
              <rect x="130" y="110" width="10" height="10" fill="#00d4aa"/>
              <rect x="140" y="110" width="10" height="10" fill="#00d4aa"/>
              <rect x="20" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="30" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="80" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="130" y="120" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="130" width="10" height="10" fill="#00d4aa"/>
              <rect x="40" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="50" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="60" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="70" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="90" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="100" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="110" y="140" width="10" height="10" fill="#00d4aa"/>
              <rect x="120" y="140" width="10" height="10" fill="#00d4aa"/>
            </svg>
          </div>
        </div>
        <div class="home-hero-actions">
        <button class="home-primary-action" onclick="switchTab('embed')" title="Generate embeddings for text">
          <span class="home-primary-action-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>
          </span>
          <span class="home-primary-action-body">
            <strong>New embedding</strong>
            <span>Generate vectors and inspect dimensions, usage, and heatmaps.</span>
          </span>
        </button>
        <button class="home-primary-action" onclick="switchTab('compare')" title="Compare model similarities">
          <span class="home-primary-action-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>
          </span>
          <span class="home-primary-action-body">
            <strong>Compare text</strong>
            <span>Check semantic similarity between two inputs side by side.</span>
          </span>
        </button>
        <button class="home-primary-action" onclick="switchTab('search')" title="Rerank documents">
          <span class="home-primary-action-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
          </span>
          <span class="home-primary-action-body">
            <strong>Rerank results</strong>
            <span>Score and reorder candidate documents against a query.</span>
          </span>
        </button>
        <button class="home-primary-action" onclick="switchTab('workflows')" title="Open workflows">
          <span class="home-primary-action-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="5" cy="12" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="19" cy="18" r="3"/><line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/><line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/></svg>
          </span>
          <span class="home-primary-action-body">
            <strong>Run workflows</strong>
            <span>Open the canvas, inspect installed flows, and execute them quickly.</span>
          </span>
        </button>
        <button class="home-primary-action" onclick="switchTab('benchmark')" title="Benchmark models">
          <span class="home-primary-action-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>
          </span>
          <span class="home-primary-action-body">
            <strong>Benchmark models</strong>
            <span>Compare latency, ranking quality, and cost from the same workspace.</span>
          </span>
        </button>
        </div>
      </div>
    </section>

    <div class="home-overview-grid">
      <section class="home-section home-overview-card">
        <div class="home-section-header">
          <h3>Workspace status</h3>
        </div>
        <div class="home-status-grid" id="homeStatusGrid">
          <div class="home-empty-state">Loading workspace status...</div>
        </div>
      </section>

      <section class="home-section home-overview-card">
        <div class="home-section-header">
          <h3>Continue where you left off</h3>
        </div>
        <div id="homeRecentActivity">
          <div class="home-empty-state">Open a tool and your recent session activity will appear here.</div>
        </div>
      </section>
    </div>

    <div class="home-main-grid">
      <section class="home-section home-overview-card">
        <div class="home-section-header">
          <h3>Latest update</h3>
          <a href="https://github.com/mrlynn/voyageai-cli/releases" target="_blank" rel="noopener">All releases →</a>
        </div>
        <div id="homeUpdateContent">
          <div class="home-empty-state">Loading the latest release notes...</div>
        </div>
      </section>

      <section class="home-section home-overview-card">
        <div class="home-section-header">
          <h3>Workflow spotlight</h3>
          <button class="home-link-button" onclick="switchTab('workflows')">Open workflows</button>
        </div>
        <div class="home-workflow-stack" id="homeWorkflowSpotlight">
          <div class="home-empty-state">Loading featured workflows...</div>
        </div>
      </section>
    </div>

    <!-- Footer -->
    <div class="home-footer">
      <div class="brand-shape-layer" id="footerLattice" style="top: auto; bottom: 0; height: 80px;"></div>
      <div class="home-footer-links" style="position: relative; z-index: 1;">
        <a href="https://docs.vaicli.com" target="_blank" rel="noopener">Documentation</a>
        <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" rel="noopener">GitHub</a>
        <a href="#" onclick="reportBug()">Report Bug</a>
        <!-- <a href="#" target="_blank" rel="noopener">Community / Discord</a> -->
      </div>
      <div class="home-footer-branding" style="position: relative; z-index: 1;">
        <span>Built with Voyage AI + MongoDB Atlas</span>
        <span id="homeFooterVersion">Version 1.0.0 • © 2026 VAI</span>
      </div>
    </div>
  </div>
</div>

<!-- ========== EMBED TAB ========== -->
<div class="tab-panel" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Embed</h2>
    <p class="page-header-subtitle">Generate vector embeddings for text</p>
    <p class="page-header-hint">Paste or type text below, choose a model, and hit Embed to see the raw vectors and token usage.</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Embed documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>
  <div class="card">
    <div class="card-title">Input Text</div>
    <textarea id="embedInput" rows="5" placeholder="Enter text to embed..." aria-label="Text to embed">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
  </div>

  <div class="options-row">
    <div class="option-group">
      <span class="option-label">Model</span>
      <select id="embedModel"></select>
    </div>
    <div class="option-group">
      <span class="option-label">Input Type</span>
      <select id="embedInputType">
        <option value="">None</option>
        <option value="query">Query</option>
        <option value="document">Document</option>
      </select>
    </div>
    <div class="option-group">
      <span class="option-label">Dimensions</span>
      <select id="embedDimensions">
        <option value="">Default</option>
        <option value="256">256</option>
        <option value="512">512</option>
        <option value="1024">1024</option>
        <option value="2048">2048</option>
      </select>
    </div>
    <div class="option-group">
      <span class="option-label">Output Type</span>
      <select id="embedOutputDtype">
        <option value="float">float (32-bit)</option>
        <option value="int8">int8 (4× smaller)</option>
        <option value="uint8">uint8 (4× smaller)</option>
        <option value="binary">binary (32× smaller)</option>
        <option value="ubinary">ubinary (32× smaller)</option>
      </select>
    </div>
    <button class="btn" id="embedBtn" onclick="doEmbed()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Embed</button>
  </div>

  <div class="error-msg" id="embedError"></div>

  <div class="result-section" id="embedResult">
    <div class="card">
      <div class="card-title">Result</div>
      <div id="embedStats"></div>
      <div class="vector-preview" id="embedVector"></div>
      <div style="margin-top:8px;">
        <button class="btn btn-secondary btn-small" onclick="copyVector()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z"/></svg>Copy Full Vector</button>
      </div>
      <div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
      <div class="heatmap" id="embedHeatmap"></div>
    </div>
  </div>
</div>

<!-- ========== COMPARE TAB ========== -->
<div class="tab-panel" id="tab-compare" role="tabpanel" aria-labelledby="tab-btn-compare" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Compare</h2>
    <p class="page-header-subtitle">Visualize similarity between text pairs</p>
    <p class="page-header-hint">Enter two texts and compare their embeddings: see cosine similarity, a heatmap of vector dimensions, and a visual diff.</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/similarity" target="_blank" rel="noopener" title="Similarity documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>
  <div class="compare-grid">
    <div class="card">
      <div class="card-title">Text A</div>
      <textarea id="compareA" rows="5" placeholder="Enter first text...">MongoDB is a popular NoSQL database</textarea>
    </div>
    <div class="card">
      <div class="card-title">Text B</div>
      <textarea id="compareB" rows="5" placeholder="Enter second text...">Document databases store data as JSON-like objects</textarea>
    </div>
  </div>

  <div class="options-row">
    <div class="option-group">
      <span class="option-label">Model</span>
      <select id="compareModel"></select>
    </div>
    <div class="option-group">
      <span class="option-label">Dimensions</span>
      <select id="compareDimensions">
        <option value="">Default</option>
        <option value="256">256</option>
        <option value="512">512</option>
        <option value="1024">1024</option>
        <option value="2048">2048</option>
      </select>
    </div>
    <button class="btn" id="compareBtn" onclick="doCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
  </div>

  <div class="error-msg" id="compareError"></div>

  <div class="result-section" id="compareResult">
    <div class="card">
      <div class="similarity-display">
        <div class="similarity-score" id="simScore">—</div>
        <div class="similarity-label">Cosine Similarity</div>
        <div class="similarity-bar-outer">
          <div class="similarity-bar-inner" id="simBar" style="width:0%"></div>
        </div>
      </div>
      <div class="metrics-grid" id="metricsGrid"></div>
      <div class="metric-note" id="metricNote"></div>
      <div id="compareStats" style="text-align:center;margin-top:16px;"></div>
    </div>
  </div>
</div>

<!-- ========== SEARCH TAB ========== -->
<div class="tab-panel" id="tab-search" role="tabpanel" aria-labelledby="tab-btn-search" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Rerank</h2>
    <p class="page-header-subtitle">Re-order documents by relevance to a query</p>
    <p class="page-header-hint">Enter a search query and a set of documents: the reranker scores and sorts them by semantic relevance.</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/rerank" target="_blank" rel="noopener" title="Rerank documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>
  <div class="card">
    <div class="card-title">Query</div>
    <input type="text" id="searchQuery" placeholder="Enter your search query..." value="How do I build AI-powered search?" aria-label="Search query">
  </div>

  <div class="card">
    <div class="card-title">Documents (one per line)</div>
    <textarea id="searchDocs" rows="8" placeholder="Enter documents, one per line...">MongoDB Atlas provides vector search capabilities
The recipe calls for two cups of flour and three eggs
Voyage AI embeddings power semantic retrieval for modern applications
Python is a popular programming language for data science
Atlas Search combines full-text and vector search in one platform
Machine learning models can be deployed at the edge
Semantic search understands meaning beyond keyword matching</textarea>
  </div>

  <div class="options-row">
    <div class="option-group">
      <span class="option-label">Embedding Model</span>
      <select id="searchEmbedModel"></select>
    </div>
    <div class="option-group">
      <span class="option-label">Rerank Model</span>
      <select id="searchRerankModel"></select>
    </div>
    <div class="option-group">
      <span class="option-label">Top K</span>
      <select id="searchTopK">
        <option value="3">3</option>
        <option value="5" selected>5</option>
        <option value="10">10</option>
      </select>
    </div>
    <button class="btn" id="searchBtn" onclick="doSearch(false)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search</button>
    <button class="btn btn-secondary" id="searchRerankBtn" onclick="doSearch(true)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M3 6h18M7 12h10M10 18h4"/></svg>Rerank</button>
    <button class="export-toolbar-btn" id="searchExportBtn" onclick="openExportDialog('search')" style="display:none;" title="Export results"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h10M8 14V5M5 8l3-3 3 3"/></svg> Export</button>
  </div>

  <div class="error-msg" id="searchError"></div>

  <div class="result-section" id="searchResult">
    <div class="search-results" id="searchResultGrid"></div>
  </div>
</div>

<!-- ========== MULTIMODAL TAB ========== -->
<div class="tab-panel" id="tab-multimodal" role="tabpanel" aria-labelledby="tab-btn-multimodal" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Multimodal</h2>
    <p class="page-header-subtitle">Compare images, video, and text in the same vector space</p>
    <p class="page-header-hint">Voyage AI's multimodal models embed images, video, and text into a unified vector space, so you can compare them directly with cosine similarity.</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Multimodal embedding documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>

  <!-- Section A: Image / Video ↔ Text Similarity -->
  <div class="mm-grid">
    <div class="card">
      <div class="card-title">Image</div>
      <div class="mm-drop-zone" id="mmDropZone">
        <div class="mm-drop-icon">🖼️</div>
        <div class="mm-drop-text">Drop an image here or click to browse</div>
        <div class="mm-drop-hint">PNG, JPEG, WebP, GIF, max 20 MB. Paste from clipboard (⌘V)</div>
      </div>
      <input type="file" id="mmFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
      <div class="mm-preview" id="mmPreview">
        <img id="mmPreviewImg" src="" alt="Preview">
        <div class="mm-file-info" id="mmFileInfo"></div>
        <button class="mm-clear-btn" onclick="clearMultimodalImage()">✕ Clear</button>
      </div>
    </div>
    <div class="card">
      <div class="card-title">Video</div>
      <div class="mm-drop-zone" id="mmVideoDropZone">
        <div class="mm-drop-icon">🎬</div>
        <div class="mm-drop-text">Drop a video here or click to browse</div>
        <div class="mm-drop-hint">MP4, WebM, MOV, max 20 MB</div>
      </div>
      <input type="file" id="mmVideoFileInput" accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-matroska" style="display:none">
      <div class="mm-preview" id="mmVideoPreview">
        <video id="mmPreviewVideo" controls style="max-width:100%;max-height:240px;border-radius:8px;background:#000;"></video>
        <div class="mm-file-info" id="mmVideoFileInfo"></div>
        <button class="mm-clear-btn" onclick="clearMultimodalVideo()">✕ Clear</button>
      </div>
    </div>
    <div class="card">
      <div class="card-title">Text</div>
      <textarea id="mmText" rows="8" placeholder="Describe what you see, or enter any text to compare against the media..."></textarea>
    </div>
  </div>

  <div class="options-row">
    <div class="option-group">
      <span class="option-label">Model</span>
      <select id="mmModel">
        <option value="voyage-multimodal-3.5">voyage-multimodal-3.5</option>
        <option value="voyage-multimodal-3">voyage-multimodal-3</option>
      </select>
    </div>
    <div class="option-group">
      <span class="option-label">Dimensions</span>
      <select id="mmDimensions">
        <option value="">Default</option>
        <option value="256">256</option>
        <option value="512">512</option>
        <option value="1024">1024</option>
        <option value="2048">2048</option>
      </select>
    </div>
    <button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
  </div>

  <div class="error-msg" id="mmError"></div>

  <div class="result-section" id="mmResult">
    <div class="card">
      <div class="similarity-display">
        <div class="similarity-score" id="mmSimScore">—</div>
        <div class="similarity-label">Cosine Similarity</div>
        <div class="similarity-bar-outer">
          <div class="similarity-bar-inner" id="mmSimBar" style="width:0%"></div>
        </div>
      </div>
      <div id="mmStats" style="text-align:center;margin-top:16px;"></div>
      <div class="metric-note" id="mmNote" style="margin-top:16px;"></div>
    </div>
  </div>

  <!-- Section B: Cross-Modal Gallery -->
  <div class="mm-gallery-section">
    <h3 style="color:var(--accent-text);margin-bottom:4px;">Cross-Modal Gallery</h3>
    <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">Build a mini corpus of images and texts, then search across both modalities at once.</p>

    <div class="card">
      <div class="card-title">Image Corpus (up to 6)</div>
      <div class="mm-gallery-grid" id="mmGalleryGrid"></div>
      <input type="file" id="mmGalleryFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
    </div>

    <div class="card" style="margin-top:12px;">
      <div class="card-title">Text Corpus (one per line)</div>
      <textarea id="mmCorpusText" rows="5" placeholder="A sunset over the ocean&#10;A cat sitting on a windowsill&#10;Abstract geometric patterns&#10;A city skyline at night"></textarea>
    </div>

    <div class="card" style="margin-top:12px;">
      <div class="card-title">Search Query</div>
      <div class="mm-search-mode" id="mmSearchMode">
        <button class="active" data-mode="text" onclick="setMmSearchMode('text')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M17 6.1H3M21 12.1H3M15.1 18H3"/></svg>Text Query</button>
        <button data-mode="image" onclick="setMmSearchMode('image')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>Image Query</button>
      </div>
      <div id="mmSearchTextWrap">
        <div class="mm-search-row">
          <input type="text" id="mmSearchQuery" placeholder="Enter a search query...">
          <button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
        </div>
      </div>
      <div id="mmSearchImageWrap" style="display:none;">
        <p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">Click an image in the corpus above to use it as the search query, then:</p>
        <div style="display:flex;gap:8px;align-items:center;">
          <span id="mmSearchImageLabel" style="font-size:13px;color:var(--text-muted);">No image selected</span>
          <button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
        </div>
      </div>
    </div>

    <div class="error-msg" id="mmSearchError"></div>

    <div class="result-section" id="mmSearchResult">
      <div class="card">
        <div class="card-title">Search Results</div>
        <div class="mm-result-list" id="mmSearchResultList"></div>
      </div>
    </div>
  </div>
</div>

<!-- ========== BENCHMARK TAB ========== -->
<div class="tab-panel" id="tab-benchmark" role="tabpanel" aria-labelledby="tab-btn-benchmark" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Benchmark</h2>
    <p class="page-header-subtitle">Compare model speed, cost, and quality</p>
    <p class="page-header-hint">Run latency tests, compare ranking accuracy, analyze quantization trade-offs, and estimate costs across models.</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/evaluation/benchmark" target="_blank" rel="noopener" title="Benchmark documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
    <button class="export-toolbar-btn" id="benchExportBtn" onclick="openExportDialog('benchmark')" style="display:none;" title="Export benchmark results"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h10M8 14V5M5 8l3-3 3 3"/></svg> Export</button>
  </div>

  <!-- Sub-panel switcher -->
  <div class="bench-panels">
    <button class="bench-panel-btn active" data-bench="latency"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Latency</button>
    <button class="bench-panel-btn" data-bench="ranking"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Ranking</button>
    <button class="bench-panel-btn" data-bench="competitors"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22"/></svg>vs Competitors</button>
    <button class="bench-panel-btn" data-bench="quantization"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10"/></svg>Quantization</button>
    <button class="bench-panel-btn" data-bench="cost"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost</button>
    <button class="bench-panel-btn" data-bench="history"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>History</button>
  </div>

  <!-- ── Latency Panel ── -->
  <div class="bench-view active" id="bench-latency">
    <div class="card">
      <div class="card-title">Embedding Latency Benchmark</div>
      <textarea id="benchLatencyInput" rows="3" placeholder="Enter text to benchmark (or leave for built-in sample)..."></textarea>
      <div class="options-row">
        <div class="option-group">
          <span class="option-label">Models</span>
          <div id="benchModelChecks" style="display:flex;gap:8px;flex-wrap:wrap;"></div>
        </div>
        <div class="option-group">
          <span class="option-label">Rounds</span>
          <select id="benchRounds">
            <option value="1">1</option>
            <option value="3" selected>3</option>
            <option value="5">5</option>
            <option value="10">10</option>
          </select>
        </div>
        <button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Run Benchmark</button>
      </div>
    </div>

    <div class="error-msg" id="benchLatencyError"></div>

    <div class="result-section" id="benchLatencyResult">
      <div class="card">
        <div class="card-title">Results</div>
        <div id="benchLatencyStats"></div>
        <div class="latency-chart" id="benchLatencyChart"></div>
      </div>
    </div>
  </div>

  <!-- ── Ranking Panel ── -->
  <div class="bench-view" id="bench-ranking">
    <div class="card">
      <div class="card-title">Model Ranking Comparison</div>
      <div style="margin-bottom:12px;">
        <input type="text" id="benchRankQuery" placeholder="Search query..." value="How do I search for similar documents using embeddings?">
      </div>
      <textarea id="benchRankDocs" rows="6" placeholder="Documents (one per line)...">Vector search finds documents by computing similarity between embedding vectors in high-dimensional space.
MongoDB Atlas Vector Search lets you index and query vector embeddings alongside your operational data.
Traditional full-text search uses inverted indexes to match keyword terms in documents.
Cosine similarity measures the angle between two vectors, commonly used for semantic search.
Database sharding distributes data across multiple servers for horizontal scalability.
Embedding models convert text into dense numerical vectors that capture meaning.
Approximate nearest neighbor algorithms like HNSW enable fast similarity search at scale.
Reranking models rescore initial search results to improve relevance ordering.</textarea>
      <div class="options-row">
        <div class="option-group">
          <span class="option-label">Model A</span>
          <select id="benchRankModelA"></select>
        </div>
        <div class="option-group">
          <span class="option-label">Model B</span>
          <select id="benchRankModelB"></select>
        </div>
        <div class="option-group">
          <span class="option-label">Mode</span>
          <select id="benchRankMode">
            <option value="embed">Embedding Similarity</option>
            <option value="rerank">Reranking</option>
          </select>
        </div>
        <div class="option-group">
          <span class="option-label">Top K</span>
          <select id="benchRankTopK">
            <option value="3">3</option>
            <option value="5" selected>5</option>
            <option value="8">8</option>
          </select>
        </div>
        <button class="btn" id="benchRankBtn" onclick="doBenchRanking()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Compare Rankings</button>
      </div>
    </div>

    <div class="error-msg" id="benchRankError"></div>

    <div class="result-section" id="benchRankResult">
      <div class="card">
        <div class="card-title">Ranking Comparison</div>
        <div id="benchRankVerdict" style="margin-bottom:12px;font-size:14px;"></div>
        <div class="rank-comparison" id="benchRankGrid"></div>
      </div>
    </div>
  </div>

  <!-- ── Competitors Panel ── -->
  <div class="bench-view" id="bench-competitors">
    <div class="card">
      <div class="card-title">Why Voyage AI? — Competitive Comparison</div>
      <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
        See how Voyage AI compares to other embedding providers on quality, cost, and features.
      </p>
    </div>

    <!-- MTEB Retrieval Benchmark -->
    <div class="card" style="margin-top:16px;">
      <div class="card-title">🏆 MTEB Retrieval Benchmark (nDCG@10)</div>
      <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
        Industry-standard benchmark for retrieval quality. Higher is better.
      </p>
      <div id="mtebChart" style="display:flex;flex-direction:column;gap:8px;">
        <!-- Voyage AI -->
        <div style="display:flex;align-items:center;gap:12px;">
          <span style="min-width:140px;font-weight:600;color:var(--accent);">Voyage-3</span>
          <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
            <div style="width:67.4%;height:100%;background:var(--accent);border-radius:4px;"></div>
          </div>
          <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--accent);font-weight:600;">67.4</span>
        </div>
        <div style="display:flex;align-items:center;gap:12px;">
          <span style="min-width:140px;font-weight:600;color:var(--accent);">Voyage-3-lite</span>
          <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
            <div style="width:64.3%;height:100%;background:var(--accent);opacity:0.7;border-radius:4px;"></div>
          </div>
          <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--accent);">64.3</span>
        </div>
        <!-- Competitors -->
        <div style="display:flex;align-items:center;gap:12px;">
          <span style="min-width:140px;color:var(--text-dim);">OpenAI text-3-large</span>
          <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
            <div style="width:64.6%;height:100%;background:#666;border-radius:4px;"></div>
          </div>
          <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">64.6</span>
        </div>
        <div style="display:flex;align-items:center;gap:12px;">
          <span style="min-width:140px;color:var(--text-dim);">OpenAI text-3-small</span>
          <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
            <div style="width:62.3%;height:100%;background:#666;border-radius:4px;"></div>
          </div>
          <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">62.3</span>
        </div>
        <div style="display:flex;align-items:center;gap:12px;">
          <span style="min-width:140px;color:var(--text-dim);">Cohere embed-v3</span>
          <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
            <div style="width:64.1%;height:100%;background:#666;border-radius:4px;"></div>
          </div>
          <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">64.1</span>
        </div>
      </div>
      <p style="color:var(--text-dim);font-size:11px;margin-top:12px;">
        Source: <a href="https://huggingface.co/spaces/mteb/leaderboard" target="_blank" style="color:var(--accent);">MTEB Leaderboard</a> (Retrieval Average, Jan 2025)
      </p>
    </div>

    <!-- Cost Comparison -->
    <div class="card" style="margin-top:16px;">
      <div class="card-title"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost per Million Tokens</div>
      <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
        Voyage AI offers significant cost savings, especially with asymmetric retrieval strategies.
      </p>
      <table style="width:100%;border-collapse:collapse;font-size:13px;">
        <thead>
          <tr style="border-bottom:1px solid var(--border);">
            <th style="text-align:left;padding:8px 12px;color:var(--text-dim);">Provider / Model</th>
            <th style="text-align:right;padding:8px 12px;color:var(--text-dim);">Price</th>
            <th style="text-align:right;padding:8px 12px;color:var(--text-dim);">vs Voyage-3</th>
          </tr>
        </thead>
        <tbody>
          <tr style="border-bottom:1px solid var(--border);">
            <td style="padding:8px 12px;font-weight:600;color:var(--accent);">Voyage-3-lite</td>
            <td style="text-align:right;padding:8px 12px;font-family:var(--mono);color:var(--accent);">$0.02</td>
            <td style="text-align:right;padding:8px 12px;color:var(--success);font-weight:600;">83% cheaper</td>
          </tr>
          <tr style="border-bottom:1px solid var(--border);">
            <td style="padding:8px 12px;font-weight:600;color:var(--accent);">Voyage-3</td>
            <td style="text-align:right;padding:8px 12px;font-family:var(--mono);color:var(--accent);">$0.06</td>
            <td style="text-align:right;padding:8px 12px;color:var(--text-dim);">baseline</td>
          </tr>
          <tr style="border-bottom:1px solid var(--border);">
            <td style="padding:8px 12px;color:var(--text-dim);">OpenAI text-3-large</td>
            <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.13</td>
            <td style="text-align:right;padding:8px 12px;color:var(--error);">2.2x more</td>
          </tr>
          <tr style="border-bottom:1px solid var(--border);">
            <td style="padding:8px 12px;color:var(--text-dim);">OpenAI text-3-small</td>
            <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.02</td>
            <td style="text-align:right;padding:8px 12px;color:var(--text-dim);">same</td>
          </tr>
          <tr>
            <td style="padding:8px 12px;color:var(--text-dim);">Cohere embed-v3</td>
            <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.10</td>
            <td style="text-align:right;padding:8px 12px;color:var(--error);">1.7x more</td>
          </tr>
        </tbody>
      </table>
      <div style="margin-top:16px;padding:12px;background:var(--accent-glow);border-radius:8px;border-left:3px solid var(--accent);">
        <strong style="color:var(--accent);">💡 Pro Tip: Asymmetric Retrieval</strong>
        <p style="margin:8px 0 0 0;font-size:13px;color:var(--text);">
          Embed your document corpus with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4-lite</code> ($0.02/M) and
          query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4</code> ($0.06/M).
          Because all Voyage 4 models share the same embedding space, you get top-tier retrieval quality at a fraction of the cost.
        </p>
      </div>
    </div>

    <!-- Key Differentiators -->
    <div class="card" style="margin-top:16px;">
      <div class="card-title">🚀 Voyage AI Advantages</div>
      <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:16px;margin-top:12px;">
        <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
          <div style="font-size:20px;margin-bottom:8px;">🔗</div>
          <div style="font-weight:600;margin-bottom:4px;">Shared Embedding Space</div>
          <div style="font-size:13px;color:var(--text-dim);">
            All Voyage 4 models produce compatible embeddings. Mix <code>voyage-4-large</code> for queries
            with <code>voyage-4-lite</code> for documents, and they work together seamlessly.
          </div>
        </div>
        <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
          <div style="font-size:20px;margin-bottom:8px;">🏢</div>
          <div style="font-weight:600;margin-bottom:4px;">Domain-Specific Models</div>
          <div style="font-size:13px;color:var(--text-dim);">
            Specialized models for code (<code>voyage-code-3</code>), finance, law, and multilingual content 
            that outperform general-purpose alternatives.
          </div>
        </div>
        <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
          <div style="font-size:20px;margin-bottom:8px;">⚡</div>
          <div style="font-weight:600;margin-bottom:4px;">Two-Stage Retrieval</div>
          <div style="font-size:13px;color:var(--text-dim);">
            Combine embedding search with <code>rerank-2</code> to dramatically boost precision. 
            The cross-encoder re-scores candidates for better relevance.
          </div>
        </div>
        <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
          <div style="font-size:20px;margin-bottom:8px;">🖼️</div>
          <div style="font-weight:600;margin-bottom:4px;">Multimodal Embeddings</div>
          <div style="font-size:13px;color:var(--text-dim);">
            <code>voyage-multimodal-3</code> embeds both text and images in the same space, 
            enabling cross-modal search (find images with text queries and vice versa).
          </div>
        </div>
      </div>
    </div>

    <!-- CTA -->
    <div class="card" style="margin-top:16px;text-align:center;padding:24px;">
      <div style="font-size:18px;font-weight:600;margin-bottom:8px;">Ready to try Voyage AI?</div>
      <p style="color:var(--text-dim);margin-bottom:16px;">Get a free API key and start building in minutes.</p>
      <a href="https://dash.voyageai.com" target="_blank" class="btn" style="display:inline-block;text-decoration:none;">
        Get API Key →
      </a>
      <span style="margin:0 12px;color:var(--text-dim);">or</span>
      <a href="https://docs.voyageai.com" target="_blank" style="color:var(--accent);">Read the Docs</a>
    </div>
  </div>

  <!-- ── Quantization Panel ── -->
  <div class="bench-view" id="bench-quantization">
    <div class="card">
      <div class="card-title">Quantization Benchmark</div>
      <p style="color:var(--text-dim);font-size:13px;margin-bottom:12px;">
        Compare how different output data types (float, int8, binary) affect storage size and ranking quality.
        Embeds the same corpus with each dtype and measures the tradeoff.
      </p>
      <div class="options-row" style="flex-wrap:wrap;">
        <div class="option-group">
          <span class="option-label">Model</span>
          <select id="quantModel"></select>
        </div>
        <div class="option-group">
          <span class="option-label">Dimensions</span>
          <select id="quantDimensions">
            <option value="">Default</option>
            <option value="256">256</option>
            <option value="512">512</option>
            <option value="1024">1024</option>
            <option value="2048">2048</option>
          </select>
        </div>
        <div class="option-group">
          <span class="option-label">Data Types</span>
          <div id="quantDtypeChecks" style="display:flex;gap:8px;flex-wrap:wrap;">
            <label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
              <input type="checkbox" value="float" checked style="accent-color:var(--accent);">float
            </label>
            <label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
              <input type="checkbox" value="int8" checked style="accent-color:var(--accent);">int8
            </label>
            <label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
              <input type="checkbox" value="uint8" style="accent-color:var(--accent);">uint8
            </label>
            <label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
              <input type="checkbox" value="ubinary" checked style="accent-color:var(--accent);">ubinary
            </label>
            <label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
              <input type="checkbox" value="binary" style="accent-color:var(--accent);">binary
            </label>
          </div>
        </div>
      </div>
      <div style="margin-top:12px;">
        <span class="option-label">Query</span>
        <input type="text" id="quantQuery" placeholder="Search query..." value="How do I search for similar documents using embeddings?" style="width:100%;margin-bottom:8px;">
      </div>
      <div>
        <span class="option-label">Corpus (one document per line)</span>
        <textarea id="quantCorpus" rows="5" placeholder="Documents to embed...">Vector search finds documents by computing similarity between embedding vectors in high-dimensional space.
MongoDB Atlas Vector Search lets you index and query vector embeddings alongside your operational data.
Traditional full-text search uses inverted indexes to match keyword terms in documents.
Cosine similarity measures the angle between two vectors, commonly used for semantic search.
Database sharding distributes data across multiple servers for horizontal scalability.
Embedding models convert text into dense numerical vectors that capture meaning.
Approximate nearest neighbor algorithms like HNSW enable fast similarity search at scale.
Reranking models rescore initial search results to improve relevance ordering.</textarea>
      </div>
      <div style="margin-top:12px;">
        <button class="btn" id="quantBtn" onclick="doBenchQuantization()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10"/></svg>Run Quantization Benchmark</button>
      </div>
    </div>

    <div class="error-msg" id="quantError"></div>

    <div class="result-section" id="quantResult">
      <div class="quant-charts">
        <div class="card">
          <div class="card-title">📦 Storage per Vector</div>
          <div id="quantStorageChart"></div>
        </div>
        <div class="card">
          <div class="card-title">⏱ API Latency</div>
          <div id="quantLatencyChart"></div>
        </div>
      </div>
      <div class="card">
        <div class="card-title">🎯 Ranking Quality vs Float Baseline</div>
        <div id="quantQualityMeters" style="margin-bottom:16px;"></div>
        <div id="quantRankGrid"></div>
      </div>
    </div>
  </div>

  <!-- ── Cost Panel ── -->
  <div class="bench-view" id="bench-cost">
    <div class="card">
      <div class="card-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>

      <!-- Mode toggle -->
      <div style="margin-bottom: 20px;">
        <div class="cost-mode-toggle">
          <button class="cost-mode-btn active" data-mode="simple" id="costModeSimple">Simple</button>
          <button class="cost-mode-btn" data-mode="rag" id="costModeRag">RAG Planner</button>
        </div>
      </div>

      <!-- Simple mode (query cost comparison) -->
      <div id="costSimpleMode">
        <div class="cost-controls cost-controls-full">
          <div class="cost-slider-row">
            <span class="cost-slider-label">Tokens / query</span>
            <input type="range" class="cost-slider" id="costTokens" min="50" max="5000" value="500" step="50">
            <span class="cost-slider-value" id="costTokensValue">500</span>
          </div>
          <div class="cost-slider-row">
            <span class="cost-slider-label">Queries / day</span>
            <input type="range" class="cost-slider" id="costQueries" min="10" max="500000" value="1000" step="10">
            <span class="cost-slider-value" id="costQueriesValue">1,000</span>
          </div>
        </div>
        <table class="cost-table" id="costTable">
          <thead>
            <tr>
              <th>Model</th>
              <th>Type</th>
              <th>$/1M tokens</th>
              <th>Daily Cost</th>
              <th>Monthly Cost</th>
              <th style="width:30%">Relative</th>
            </tr>
          </thead>
          <tbody id="costTableBody"></tbody>
        </table>
      </div>

      <!-- RAG Planner mode (full TCO) -->
      <div id="costRagMode" style="display:none;">
        <div class="cost-section-title">📄 Documents (one-time ingestion)</div>
        <div class="cost-controls">
          <div class="cost-slider-row">
            <span class="cost-slider-label">Documents</span>
            <input type="range" class="cost-slider" id="ragDocs" min="1000" max="10000000" value="100000" step="1000">
            <span class="cost-slider-value" id="ragDocsValue">100K</span>
          </div>
          <div class="cost-slider-row">
            <span class="cost-slider-label">Tokens / doc</span>
            <input type="range" class="cost-slider" id="ragDocTokens" min="50" max="5000" value="500" step="50">
            <span class="cost-slider-value" id="ragDocTokensValue">500</span>
          </div>
        </div>

        <div class="cost-section-title">🔍 Queries (recurring)</div>
        <div class="cost-controls">
          <div class="cost-slider-row">
            <span class="cost-slider-label">Queries / month</span>
            <input type="range" class="cost-slider" id="ragQueries" min="1000" max="50000000" value="1000000" step="1000">
            <span class="cost-slider-value" id="ragQueriesValue">1M</span>
          </div>
          <div class="cost-slider-row">
            <span class="cost-slider-label">Tokens / query</span>
            <input type="range" class="cost-slider" id="ragQueryTokens" min="10" max="500" value="30" step="5">
            <span class="cost-slider-value" id="ragQueryTokensValue">30</span>
          </div>
        </div>

        <div class="cost-section-title">⚙️ Configuration</div>
        <div class="cost-controls">
          <div class="cost-model-row">
            <span class="cost-model-label">Doc model</span>
            <select class="cost-select" id="ragDocModel"></select>
          </div>
          <div class="cost-model-row">
            <span class="cost-model-label">Query model</span>
            <select class="cost-select" id="ragQueryModel"></select>
          </div>
          <div class="cost-slider-row">
            <span class="cost-slider-label">Projection</span>
            <input type="range" class="cost-slider" id="ragMonths" min="1" max="36" value="12" step="1">
            <span class="cost-slider-value" id="ragMonthsValue">12 mo</span>
          </div>
        </div>

        <!-- Summary cards -->
        <div class="cost-summary" id="ragSummary"></div>

        <!-- Strategy comparison -->
        <div class="cost-section-title">📊 Strategy Comparison</div>
        <div class="cost-strategy-cards" id="ragStrategies"></div>

        <!-- Per-model table -->
        <div class="cost-section-title">📋 Per-Model Breakdown</div>
        <table class="cost-table" id="ragTable">
          <thead>
            <tr>
              <th>Model</th>
              <th>Doc Cost</th>
              <th>Query $/mo</th>
              <th>Total (projected)</th>
              <th style="width:25%">Relative</th>
            </tr>
          </thead>
          <tbody id="ragTableBody"></tbody>
        </table>

        <div class="cost-tip" id="ragTip"></div>
      </div>
    </div>
  </div>

  <!-- ── History Panel ── -->
  <div class="bench-view" id="bench-history">
    <div class="card">
      <div class="card-title">Benchmark History</div>
      <div id="benchHistoryContent">
        <div class="history-empty">No benchmarks recorded yet. Run a latency benchmark to start tracking.</div>
      </div>
      <div style="margin-top:12px;text-align:right;">
        <button class="btn btn-secondary btn-small" onclick="clearHistory()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>Clear History</button>
      </div>
    </div>
  </div>

</div>

<!-- ========== CHAT TAB ========== -->
<div class="tab-panel" id="tab-chat" role="tabpanel" aria-labelledby="tab-btn-chat" tabindex="0">
  <!-- KB Setup Wizard Modal -->
  <div id="kbWizard" class="kb-wizard">
    <div class="kb-wizard-overlay"></div>
    <div class="kb-wizard-card">
      <button class="kb-wizard-close" id="kbWizardClose">&times;</button>
      
      <!-- Step 1: Select KB Source -->
      <div id="kbWizardSelectStep" class="kb-wizard-step">
        <div class="kb-wizard-icon">📚</div>
        <h2 class="kb-wizard-title">Your Knowledge Base</h2>
        <p class="kb-wizard-desc">Start with existing data or create fresh</p>
        
        <div class="kb-wizard-options">
          <div class="kb-wizard-option">
            <label for="kbWizardExistingSelect" style="display: block; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: var(--text-dim);">Use Existing KB</label>
            <select id="kbWizardExistingSelect" class="kb-wizard-select">
              <option value="">— Loading knowledge bases —</option>
            </select>
            <button class="btn" id="kbWizardUseExisting" style="width: 100%; margin-top: 12px;">Use This KB</button>
          </div>

          <div class="kb-wizard-divider">or</div>

          <div class="kb-wizard-option">
            <p style="font-size: 13px; color: var(--text-dim); margin-bottom: 12px;">Start with a fresh knowledge base</p>
            <button class="btn" id="kbWizardStartFresh" style="width: 100%;">Create New KB</button>
          </div>
        </div>
      </div>

      <!-- Step 2: Add Documents -->
      <div id="kbWizardDocsStep" class="kb-wizard-step" style="display: none;">
        <div class="kb-wizard-icon">📤</div>
        <h2 class="kb-wizard-title">Add Documents</h2>
        <p class="kb-wizard-desc">Upload .txt, .md, or .pdf files (max 10MB each)</p>
        
        <div id="kbWizardDropZone" class="kb-wizard-dropzone">
          <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
            <polyline points="17 8 12 3 7 8"/>
            <line x1="12" y1="3" x2="12" y2="15"/>
          </svg>
          <p style="margin: 12px 0 4px; font-weight: 600; color: var(--text);">Drag files here or click to browse</p>
          <p style="font-size: 12px; color: var(--text-muted);">.txt, .md, .pdf files up to 10MB</p>
        </div>
        
        <div style="display: flex; gap: 8px; margin-top: 16px;">
          <button class="btn" id="kbWizardSkipFiles" style="flex: 1; background: transparent; border: 1px solid var(--border); color: var(--text);">Skip for now</button>
          <button class="btn" id="kbWizardStartChatting" style="flex: 1;">Start Chatting →</button>
        </div>
      </div>
    </div>
  </div>

  <!-- KB Panel (Sidebar) — Redesigned -->
  <div id="kbPanel" class="kb-panel">
    <div class="kb-panel-header">
      <h3 class="kb-panel-title">Knowledge Base</h3>
      <div style="display:flex; align-items:center; gap:4px;">
        <button class="kb-panel-btn kb-panel-btn-secondary" id="kbPanelSwitchBtn" title="Switch KB" style="padding:4px 8px; font-size:11px;">Switch</button>
        <button class="kb-panel-btn kb-panel-btn-danger" id="kbPanelDeleteBtn" title="Delete this knowledge base" style="padding:4px 8px; font-size:11px;">Delete</button>
        <button class="kb-panel-collapse-btn" id="kbPanelCollapseBtn" title="Collapse panel">&#x2039;</button>
      </div>
    </div>
    <div class="kb-panel-content">

      <!-- Section: KB Stats -->
      <div class="kb-section" data-kb-section="stats">
        <div class="kb-section-header expanded" onclick="kbToggleSection('stats')">
          <span class="kb-chevron">&#x25B6;</span>
          <span>KB Info</span>
          <span class="kb-section-badge kb-name-editable" id="kbPanelName" title="Click to rename" onclick="event.stopPropagation(); window.kbUI && window.kbUI.startRename()">--</span>
        </div>
        <div class="kb-section-body" data-kb-section="stats" style="max-height:none;">
          <div class="kb-section-body-inner">
            <div class="kb-panel-kb-info">
              <div class="kb-panel-stat">
                <span class="kb-panel-stat-label">Documents</span>
                <span class="kb-panel-stat-value" id="kbPanelDocCount">0</span>
              </div>
              <div class="kb-panel-stat">
                <span class="kb-panel-stat-label">Chunks</span>
                <span class="kb-panel-stat-value" id="kbPanelChunkCount">0</span>
              </div>
              <div class="kb-panel-stat">
                <span class="kb-panel-stat-label">Size</span>
                <span class="kb-panel-stat-value" id="kbPanelSize">0 KB</span>
              </div>
            </div>
            <div id="kbIndexStatus" style="margin-top:6px; font-size:11px; text-align:center; transition: opacity 0.3s;"></div>
            <button class="kb-panel-btn kb-panel-btn-danger" id="kbPanelClearBtn" style="width:100%; margin-top:8px; font-size:11px;">Clear All Documents</button>
          </div>
        </div>
      </div>

      <!-- Section: Files -->
      <div class="kb-section" data-kb-section="files">
        <div class="kb-section-header expanded" onclick="kbToggleSection('files')">
          <span class="kb-chevron">&#x25B6;</span>
          <span>Files</span>
          <span class="kb-section-badge" id="kbFileCount">0</span>
        </div>
        <div class="kb-section-body" data-kb-section="files" style="max-height:none;">
          <div class="kb-section-body-inner">
            <ul class="kb-file-list" id="kbFileList">
              <li class="kb-file-empty">No documents ingested</li>
            </ul>
          </div>
        </div>
      </div>

      <!-- Section: Upload -->
      <div class="kb-section" data-kb-section="upload">
        <div class="kb-section-header expanded" onclick="kbToggleSection('upload')">
          <span class="kb-chevron">&#x25B6;</span>
          <span>Upload</span>
        </div>
        <div class="kb-section-body" data-kb-section="upload" style="max-height:none;">
          <div class="kb-section-body-inner">
            <!-- Ingest method tabs -->
            <div style="display:flex; gap:0; margin-bottom:8px; border-bottom:1px solid var(--border);">
              <span class="kb-ingest-tab active" data-kb-ingest-tab="files" onclick="kbSwitchIngestTab('files')" style="padding:4px 10px; font-size:11px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid var(--accent);">Files</span>
              <span class="kb-ingest-tab" data-kb-ingest-tab="paste" onclick="kbSwitchIngestTab('paste')" style="padding:4px 10px; font-size:11px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid transparent;">Paste</span>
              <span class="kb-ingest-tab" data-kb-ingest-tab="url" onclick="kbSwitchIngestTab('url')" style="padding:4px 10px; font-size:11px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid transparent;">URL</span>
            </div>

            <!-- Files tab (default) -->
            <div id="kbIngestTabFiles">
              <div id="kbPanelDropZone" class="kb-panel-dropzone-compact">
                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                  <polyline points="17 8 12 3 7 8"/>
                  <line x1="12" y1="3" x2="12" y2="15"/>
                </svg>
                <p style="margin:4px 0 0; font-weight:600; font-size:12px; color:var(--text);">Drop files here</p>
                <p style="font-size:10px; color:var(--text-muted); margin:2px 0 0;">.txt, .md, .pdf &mdash; max 10MB</p>
              </div>
              <input type="file" id="kbPanelFileInput" multiple accept=".txt,.md,.pdf" style="display:none;">
            </div>

            <!-- Paste tab -->
            <div id="kbIngestTabPaste" style="display:none;">
              <textarea id="kbPasteText" rows="5" placeholder="Paste text content here..." style="width:100%; resize:vertical; font-size:12px; padding:8px; border:1px solid var(--border); border-radius:6px; background:var(--bg-secondary); color:var(--text); box-sizing:border-box;"></textarea>
              <input id="kbPasteTitle" type="text" placeholder="Title (optional)" style="width:100%; font-size:12px; padding:6px 8px; margin-top:4px; border:1px solid var(--border); border-radius:6px; background:var(--bg-secondary); color:var(--text); box-sizing:border-box;">
              <button id="kbPasteIngestBtn" class="kb-panel-btn kb-panel-btn-primary" style="margin-top:6px; width:100%;">Ingest Text</button>
            </div>

            <!-- URL tab -->
            <div id="kbIngestTabURL" style="display:none;">
              <input id="kbURLInput" type="url" placeholder="https://example.com/page" style="width:100%; font-size:12px; padding:8px; border:1px solid var(--border); border-radius:6px; background:var(--bg-secondary); color:var(--text); box-sizing:border-box;">
              <button id="kbURLIngestBtn" class="kb-panel-btn kb-panel-btn-primary" style="margin-top:6px; width:100%;">Fetch & Ingest</button>
            </div>

            <!-- Progress bar (shared across all tabs) -->
            <div id="kbPanelProgress" class="kb-panel-progress" style="display:none;">
              <div class="kb-panel-progress-label" id="kbPanelProgressLabel">Ingesting...</div>
              <div class="kb-panel-progress-bar">
                <div class="kb-panel-progress-fill" id="kbPanelProgressBar"></div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- Section: Instructions -->
      <div class="kb-section" data-kb-section="instructions">
        <div class="kb-section-header" onclick="kbToggleSection('instructions')">
          <span class="kb-chevron">&#x25B6;</span>
          <span>Instructions</span>
        </div>
        <div class="kb-section-body" data-kb-section="instructions" style="max-height:0px;">
          <div class="kb-section-body-inner">
            <p style="font-size:10px; color:var(--text-muted); margin:0 0 6px;">Custom instructions appended to the system prompt</p>
            <textarea class="kb-instructions-textarea" id="kbSystemPrompt" rows="3" placeholder="e.g. You are a movie expert. Be enthusiastic about recommendations."></textarea>
          </div>
        </div>
      </div>

      <!-- Section: Configuration -->
      <div class="kb-section" data-kb-section="config">
        <div class="kb-section-header" onclick="kbToggleSection('config')">
          <span class="kb-chevron">&#x25B6;</span>
          <span>Configuration</span>
        </div>
        <div class="kb-section-body" data-kb-section="config" style="max-height:0px;">
          <div class="kb-section-body-inner">
            <div class="kb-config-row">
              <label class="kb-config-label">LLM Provider<span class="health-dot" id="healthDotOllama" title="Ollama status"></span></label>
              <select class="settings-select" id="chatProvider" onchange="chatProviderChanged()">
                <option value="">Not configured</option>
                <option value="anthropic">Anthropic</option>
                <option value="openai">OpenAI</option>
                <option value="ollama">Ollama</option>
              </select>
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Model</label>
              <select class="settings-select" id="chatModel">
                <option value="">Select provider first</option>
              </select>
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Embedding</label>
              <div style="display:flex;align-items:center;gap:4px;flex:1;">
                <select class="settings-select" id="chatEmbeddingModel" style="flex:1;">
                  <option value="voyage-4-nano" data-local="true" data-price="Free">voyage-4-nano (Free)</option>
                  <option value="voyage-4-lite" data-price="$0.02/1M">voyage-4-lite ($0.02/1M)</option>
                  <option value="voyage-4" data-price="$0.06/1M">voyage-4 ($0.06/1M)</option>
                  <option value="voyage-4-large" data-price="$0.12/1M">voyage-4-large ($0.12/1M)</option>
                </select>
                <span class="embed-badge embed-badge-local" id="chatEmbedBadge">LOCAL</span>
                <span class="health-dot" id="healthDotNano" title="Nano bridge status"></span>
              </div>
            </div>
            <div class="kb-config-row" id="chatApiKeyRow" style="display:none">
              <label class="kb-config-label">API Key<span class="health-dot" id="healthDotApiKey" title="Voyage API key status"></span></label>
              <div style="display:flex;gap:4px;align-items:center">
                <input type="password" class="settings-input" id="chatApiKey" placeholder="API key" style="flex:1;font-size:12px;">
                <button class="kb-panel-btn kb-panel-btn-secondary" id="chatApiKeyToggle" onclick="toggleChatApiKeyVisibility()" title="Show/hide" style="padding:4px 6px;min-width:auto;font-size:11px;">
                  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
                </button>
                <button class="kb-panel-btn kb-panel-btn-primary" id="chatApiKeySave" onclick="saveChatApiKey()" title="Save" style="padding:4px 6px;min-width:auto;font-size:11px;">Save</button>
              </div>
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Database</label>
              <input type="text" class="settings-input" id="chatDb" placeholder="database">
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Collection</label>
              <input type="text" class="settings-input" id="chatCollection" placeholder="collection">
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Max Context Docs</label>
              <input type="number" class="settings-input" id="chatMaxDocs" value="5" min="1" max="20" style="width:70px;">
            </div>
            <div class="kb-config-row">
              <div class="kb-config-toggle-row">
                <label class="kb-config-label">Rerank Results</label>
                <button class="settings-toggle active" id="chatRerank" type="button" onclick="toggleRerank()"></button>
              </div>
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Chat Mode</label>
              <select class="settings-select" id="chatMode">
                <option value="pipeline">Pipeline (fixed RAG)</option>
                <option value="agent">Agent (tool-calling)</option>
              </select>
            </div>
            <div class="kb-config-row">
              <label class="kb-config-label">Memory Strategy</label>
              <select class="settings-select" id="chatMemoryStrategy">
                <option value="sliding_window" selected>Sliding Window (default)</option>
                <option value="summarization">Summarization</option>
                <option value="hierarchical">Hierarchical</option>
              </select>
              <p style="font-size:0.75em;color:var(--text-muted);margin-top:4px;margin-bottom:0;">
                Controls how conversation history is managed within the token budget.
              </p>
            </div>
          </div>
        </div>
      </div>

    </div>
  </div>

  <div class="chat-container">
    <div class="chat-header">
      <div style="display:flex; align-items:center; gap:8px;">
        <button class="chat-kb-toggle active" id="chatKbToggle" onclick="toggleKBPanel()" title="Toggle Knowledge Base panel">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
          KB
        </button>
        <div class="chat-header-status" id="chatHeaderStatus">
          <span id="chatStatusProvider">No provider</span>
          <span>&middot;</span>
          <span id="chatStatusDb">No database</span>
        </div>
      </div>
      <div style="display:flex; align-items:center; gap:4px;">
        <button class="export-toolbar-btn" id="chatExportBtn" onclick="openExportDialog('chat')" style="display:none;" title="Export chat session">
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h10M8 14V5M5 8l3-3 3 3"/></svg> Export
        </button>
        <button class="chat-config-toggle" id="chatOpenSettings" onclick="kbOpenConfigSection()" title="Configure">
          <svg width="14" height="14" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
          Configure
        </button>
      </div>
    </div>
    <div id="onboardingBanner" class="onboarding-banner" style="display:none;">
      <div class="onboarding-banner-inner">
        <div class="onboarding-banner-title">Welcome to VAI Chat</div>
        <div class="onboarding-banner-services" id="onboardingServices"></div>
        <div class="onboarding-banner-recommendation" id="onboardingRecommendation"></div>
        <div class="onboarding-banner-actions">
          <button class="btn btn-primary" id="onboardingApply" onclick="applyRecommendedConfig()">Start Chatting</button>
          <button class="btn btn-secondary" id="onboardingDismiss" onclick="dismissOnboarding()">Configure Manually</button>
        </div>
      </div>
    </div>
    <div class="chat-not-configured" id="chatNotConfigured" style="display:none;">
      <div class="chat-not-configured-icon">💬</div>
      <p>Chat requires an LLM provider and database to be configured.</p>
      <button class="btn" onclick="openChatSettings()">Open Chat Settings</button>
    </div>
    <div class="chat-messages" id="chatMessages">
      <div class="chat-message system-msg">Chat with your knowledge base. Click <strong>Configure</strong> above to adjust your LLM provider and database.</div>
    </div>
    <div class="chat-state-indicator" id="chatStateIndicator">
      <span class="chat-state-dot"></span>
      <span id="chatStateLabel">Ready</span>
    </div>
    <div class="chat-memory-bar" id="chatMemoryBar">
      <span>Memory:</span>
      <div class="chat-memory-bar-track">
        <div class="chat-memory-bar-fill" id="chatMemoryFill" style="width: 0%"></div>
      </div>
      <span id="chatMemoryPercent">0%</span>
    </div>
    <div class="chat-input-area">
      <textarea class="chat-input" id="chatInput" placeholder="Ask a question about your documents..." rows="1" onkeydown="chatInputKeydown(event)"></textarea>
      <button class="chat-send-btn" id="chatSendBtn" onclick="sendChatMessage()">Send</button>
    </div>
  </div>
</div>

<!-- ========== WORKFLOWS TAB ========== -->
<div class="tab-panel" id="tab-workflows" role="tabpanel" aria-labelledby="tab-btn-workflows" tabindex="0">
  <div class="wf-container">
    <div class="wf-library" id="wfLibrary">
      <div class="wf-library-header">
        <div class="wf-library-tabs" style="flex:1;">
          <button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
          <button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
        </div>
        <button class="wf-store-btn" id="wfStoreBtn" onclick="wfStoreOpen()" title="Browse Workflow Store">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
        </button>
        <button class="wf-library-collapse-btn" onclick="wfToggleLibrary()" title="Collapse library">&#x2039;</button>
      </div>
      <div class="wf-library-search-wrap">
        <input type="text" id="wfLibrarySearch" placeholder="Search workflows..." oninput="wfOnLibrarySearch()">
        <button class="wf-library-search-clear" id="wfLibrarySearchClear" onclick="wfClearLibrarySearch()">✕</button>
      </div>
      <div class="wf-library-list" id="wfLibraryList">
        <div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
      </div>
      <div class="wf-palette-list" id="wfPaletteList" style="display:none; flex:1; overflow-y:auto; padding:8px;"></div>
      <div class="wf-library-footer">
        <button class="wf-load-file-btn" onclick="wfLoadFromFile()" title="Load workflow JSON from file">
          <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 14h10M8 2v9M5 8l3 3 3-3"/></svg>
          Load File
        </button>
        <input type="file" id="wfFileInput" accept=".json" style="display:none;" onchange="wfHandleFileLoad(event)">
      </div>
    </div>
    <div class="wf-canvas-area">
      <div class="wf-canvas-toolbar" id="wfToolbar">
        <button class="wf-new-btn" onclick="wfNewWorkflow()" title="Create new workflow">+ New</button>
        <button class="wf-edit-btn" id="wfEditBtn" onclick="wfEditWorkflow()" disabled title="Edit current workflow">&#9998; Edit</button>
        <span class="wf-toolbar-sep"></span>
        <button onclick="wfZoom(1)" title="Zoom in">+</button>
        <button onclick="wfZoom(-1)" title="Zoom out">&minus;</button>
        <button onclick="wfFitToView()" title="Fit to view">&#8862;</button>
        <button onclick="wfRelayout()" title="Auto-layout (reorder nodes to minimize crossings)">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><line x1="10" y1="6.5" x2="14" y2="6.5"/><line x1="10" y1="17.5" x2="14" y2="17.5"/></svg>
        </button>
        <button onclick="wfResetExecution()" title="Reset">&#8635;</button>
        <span class="wf-toolbar-sep"></span>
        <button class="wf-plan-btn" onclick="wfDryRun()" id="wfDryRunBtn" disabled title="Dry run: show execution plan">&#9881; Plan</button>
        <button class="wf-run-btn" id="wfRunBtn" onclick="wfExecute()" disabled title="Run workflow">&#9654; Run</button>
        <button class="wf-stop-btn" id="wfStopBtn" onclick="wfStopExecution()" style="display:none;" title="Stop workflow">&#9632; Stop</button>
        <span class="wf-toolbar-sep"></span>
        <button onclick="openExportDialog('workflow')" id="wfExportBtn" disabled title="Export workflow (⌘E)">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
        </button>
        <button class="wf-toolbar-info-btn" id="wfInfoBtn" onclick="wfShowWorkflowInfo()" disabled title="Workflow info">&#9432;</button>
        <span class="wf-toolbar-spacer"></span>
        <div class="wf-toolbar-toggle-group">
          <button id="wfToggleLibBtn" class="active" onclick="wfToggleLibrary()">Library</button>
          <button id="wfTogglePropsBtn" class="active" onclick="wfToggleInspector()">Props</button>
        </div>
      </div>
      <!-- Draft validation status bar -->
      <div class="wf-validation-bar" id="wfValidationBar">
        <span id="wfValidationBarText"></span>
        <button class="wf-validation-close" onclick="event.stopPropagation(); document.getElementById('wfValidationBar').style.display='none';" title="Dismiss">&times;</button>
      </div>
      <!-- Edge handles for collapsed panels -->
      <div class="wf-edge-handle wf-edge-handle--left" id="wfEdgeHandleLeft" onclick="wfToggleLibrary()" title="Expand library">&#x203A;</div>
      <div class="wf-edge-handle wf-edge-handle--right" id="wfEdgeHandleRight" onclick="wfToggleInspector()" title="Expand properties">&#x2039;</div>
      <!-- Execution status bar -->
      <div class="wf-exec-status" id="wfExecStatus" style="display:none;">
        <span class="wf-exec-status-dot"></span>
        <span class="wf-exec-status-text" id="wfExecStatusText">Running...</span>
        <span class="wf-exec-status-time" id="wfExecStatusTime"></span>
      </div>
      <div class="wf-canvas-empty" id="wfCanvasEmpty">
        <div class="wf-canvas-empty-icon"><img src="/icons/watermark.png" alt="Vai"></div>
        <div class="wf-canvas-empty-text">Select a workflow from the library</div>
      </div>
      <div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
      <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg" ondragover="event.preventDefault()" ondrop="wfCanvasDrop(event)"></svg>
      <!-- Workflow Store Overlay -->
      <div class="wf-store-overlay" id="wfStoreOverlay">
        <div class="wf-store-header">
          <button class="wf-store-back" onclick="wfStoreClose()">← Library</button>
          <div class="wf-store-title-area">
            <span class="wf-store-title">Workflow Store</span>
            <span class="wf-store-badge">@vaicli</span>
          </div>
          <div class="wf-store-search-wrap">
            <span class="wf-store-search-icon">⌕</span>
            <input class="wf-store-search" id="wfStoreSearch" placeholder="Search..." oninput="wfStoreRender()">
          </div>
          <select class="wf-store-sort" id="wfStoreSort" onchange="wfStoreRender()">
            <option value="downloads">Popular</option>
            <option value="name">Name A–Z</option>
            <option value="complexity">Complex</option>
          </select>
        </div>
        <div class="wf-store-body">
          <div class="wf-store-inner" id="wfStoreContent">
            <div style="padding:40px;text-align:center;color:var(--text-muted);">Loading catalog...</div>
          </div>
        </div>
      </div>
    </div>
    <div class="wf-inspector collapsed" id="wfInspector">
      <div class="wf-inspector-content">
        <div class="wf-inspector-header" id="wfInspectorHeader">Inspector</div>
        <div class="wf-inspector-body" id="wfInspectorBody">
          <div class="wf-inspector-empty">Click a node to inspect</div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- ── Workflow Output Modal ── -->
<div class="wf-output-modal-backdrop" id="wfOutputModalBackdrop" style="display:none;" onclick="wfCloseOutputModal()">
  <div class="wf-output-modal" onclick="event.stopPropagation()">
    <div class="wf-output-modal-header">
      <span class="wf-output-modal-title" id="wfOutputModalTitle">Output</span>
      <div class="wf-output-modal-actions">
        <button class="wf-output-modal-btn" onclick="wfCopyOutput()" title="Copy to clipboard" id="wfOutputCopyBtn">
          <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>
          <span id="wfOutputCopyLabel">Copy</span>
        </button>
        <button class="wf-output-modal-btn close" onclick="wfCloseOutputModal()" title="Close">&times;</button>
      </div>
    </div>
    <pre class="wf-output-modal-body" id="wfOutputModalBody"></pre>
  </div>
</div>

<!-- ── Workflow Node Help Modal ── -->
<div class="wf-help-modal-backdrop" id="wfHelpModalBackdrop" style="display:none;" onclick="wfCloseHelpModal()">
  <div class="wf-help-modal" onclick="event.stopPropagation()">
    <div class="wf-help-modal-header" id="wfHelpModalHeader"></div>
    <div class="wf-help-modal-body" id="wfHelpModalBody"></div>
  </div>
</div>

<!-- ── Workflow Input Modal (pre-execution) ── -->
<div class="wf-input-modal-backdrop" id="wfInputModalBackdrop" style="display:none;" onclick="wfCloseInputModal()">
  <div class="wf-input-modal" onclick="event.stopPropagation()">
    <div class="wf-input-modal-header">
      <span class="wf-input-modal-title" id="wfInputModalTitle">Workflow Inputs</span>
      <button class="wf-output-modal-btn close" onclick="wfCloseInputModal()" title="Close">&times;</button>
    </div>
    <div class="wf-input-modal-body" id="wfInputModalBody"></div>
    <div class="wf-input-modal-footer">
      <button class="wf-input-modal-cancel" onclick="wfCloseInputModal()">Cancel</button>
      <button class="wf-input-modal-run" onclick="wfInputModalSubmit()">Run Workflow</button>
    </div>
  </div>
</div>

<!-- Workflow Info Modal -->
<div class="wf-info-modal-backdrop" id="wfInfoModalBackdrop" style="display:none;" onclick="wfCloseInfoModal()">
  <div class="wf-info-modal" onclick="event.stopPropagation()">
    <div id="wfInfoModalContent"></div>
  </div>
</div>

<!-- ========== ABOUT TAB ========== -->
<div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
  <div class="about-container">
    <div class="card">
      <div class="about-header">
        <img src="https://avatars.githubusercontent.com/u/192552?v=4" alt="Michael Lynn" class="about-avatar">
        <div>
          <div class="about-name">Michael Lynn</div>
          <div class="about-role">Founder, Advisor, Engineer, Developer Advocate, MongoDB</div>
          <div class="about-links">
            <a href="https://github.com/mrlynn" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>GitHub</a>
            <a href="https://mlynn.org" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>mlynn.org</a>
            <a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12"/></svg>npm</a>
          </div>
        </div>
      </div>

      <div class="about-section">
        <div class="about-section-title">About This Project</div>
        <div class="about-text">
          <strong>voyageai-cli</strong> (<code style="color:var(--accent);">vai</code>) is a community-built command-line tool for working with
          <a href="https://docs.vaicli.com/" target="_blank">Voyage AI</a> embeddings, reranking, and
          <a href="https://www.mongodb.com/products/platform/atlas-vector-search" target="_blank">MongoDB Atlas Vector Search</a>.
          It was created to make it easier for developers to explore, benchmark, and integrate
          Voyage AI models into their applications — right from the terminal or this playground.
        </div>
      </div>

      <div class="about-section">
        <div class="about-section-title">About Michael</div>
        <div class="about-text">
          Michael Lynn is a Principal Staff Developer Advocate at MongoDB with 25+ years in enterprise
          infrastructure and over a decade at MongoDB. He focuses on strategic developer relations,
          creating educational content around Vector Search, AI enablement, and developer tooling.
          He builds tools like this to help developers get hands-on with new technology faster.
        </div>
      </div>

      <div class="about-section">
        <div class="about-section-title">What You Can Do Here</div>
        <div class="about-text">
          <strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
          <strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product &amp; euclidean distance<br>
          <strong>🔍 Rerank</strong> — Semantic search with optional reranking<br>
          <strong>🔮 Multimodal</strong> — Compare images and text in the same vector space with voyage-multimodal-3.5<br>
          <strong>🛠️ Generate</strong> — Generate code snippets and scaffold full projects with templates<br>
          <strong>💬 Chat</strong> — RAG-powered chat with your documents, configurable system prompts<br>
          <strong>🔄 Workflows</strong> — Multi-step agent workflows with thinking panels and tool orchestration<br>
          <strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
          <strong>📚 Explore</strong> — 23 interactive concepts covering embeddings, vector search, multimodal, RAG, and more
        </div>
      </div>

      <div class="about-section">
        <div class="about-section-title">Supported Models</div>
        <div class="about-text" style="font-size:13px;">
          <strong style="color:var(--accent);">Text Embeddings</strong> — voyage-4-large, voyage-4, voyage-4-lite, voyage-code-3, voyage-finance-2, voyage-law-2<br>
          <strong style="color:var(--accent);">Multimodal</strong> — voyage-multimodal-3.5 (text + images + video in one vector space)<br>
          <strong style="color:var(--accent);">Reranking</strong> — rerank-2.5, rerank-2.5-lite<br><br>
          All models are accessible via the <a href="https://docs.voyageai.com/" target="_blank" style="color:var(--blue);">Voyage AI API</a>
          or through <a href="https://www.mongodb.com/docs/atlas/atlas-vector-search/" target="_blank" style="color:var(--blue);">MongoDB Atlas Vector Search</a>.
        </div>
      </div>

      <div class="about-disclaimer">
        <div class="about-disclaimer-title">⚠️ Community Tool Disclaimer</div>
        <div class="about-disclaimer-text">
          This tool is <strong>not</strong> an official product of MongoDB, Inc. or Voyage AI.
          It is independently built and maintained by Michael Lynn as a community resource.
          It is not supported, endorsed, or guaranteed by either company. Use at your own discretion.
          For official documentation, visit
          <a href="https://docs.vaicli.com/" target="_blank" style="color:var(--warning);">mongodb.com/docs/voyageai</a>.
        </div>
      </div>
    </div>

    <div class="card" style="margin-top:16px;">
      <div class="about-section" style="padding-bottom:0;">
        <div class="about-section-title">What's New</div>
        <div class="about-changelog">
          <details open>
            <summary><strong>Recent</strong> — Local inference playground expansion</summary>
            <p>The Nano tab now goes far beyond setup checks: you can compare texts with an NxN similarity heatmap, inspect embedding behavior across 256/512/1024/2048 dimensions, and run cross-bridge comparisons between local <code>voyage-4-nano</code> embeddings and cloud API results.</p>
          </details>
          <details>
            <summary><strong>Recent</strong> — Explore and About page refresh</summary>
            <p>The Explore experience has been polished with cleaner cards, updated visual styling, and Avi as the lightweight guide in the header and explainer modal. The About page has also been refreshed to better reflect the current product direction.</p>
          </details>
          <details>
            <summary><strong>Recent</strong> — Better bug reporting and diagnostics</summary>
            <p>Bug reports from both the CLI and Electron app now include richer environment context, validated contact details, and cleaner GitHub issue handoff. The desktop bridge exposes platform and runtime metadata directly to improve issue triage.</p>
          </details>
          <details>
            <summary><strong>Recent</strong> — Expanded explainers for local inference</summary>
            <p>The learning content now includes a dedicated <code>local-inference</code> explainer covering the Python bridge, shared embedding space, and when to use local versus hosted Voyage models, with new aliases that make the topic easier to discover.</p>
          </details>
          <details>
            <summary><strong>Earlier</strong> — Generate, scaffold, and workflow setup</summary>
            <p>Added <code>vai generate</code> and <code>vai scaffold</code> for production-ready RAG code and starter apps, plus related maintenance workflows like <code>purge</code>, <code>refresh</code>, and evaluation comparisons.</p>
          </details>
          <details>
            <summary><strong>Earlier</strong> — Desktop app, multimodal, and auto-update</summary>
            <p>Introduced the signed and notarized macOS desktop app with auto-update support, secure keychain-backed credentials, a settings cog, the Multimodal tab, telemetry controls, and the hidden Vector Space Invaders easter egg.</p>
          </details>
          <details>
            <summary><strong>Earlier</strong> — Benchmarking, evaluation, and retrieval tooling</summary>
            <p>Built out the benchmark, estimate, eval, query, pipeline, chunk, and explain workflows that power the broader CLI and playground experience for retrieval, ranking, and cost analysis.</p>
          </details>
        </div>
      </div>
    </div>

    <div style="text-align:center;margin-top:16px;font-size:12px;color:var(--text-muted);">
      Made with ☕ and curiosity · <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" style="color:var(--text-dim);">Source on GitHub</a>
      · <a href="https://github.com/mrlynn/voyageai-cli/releases" target="_blank" style="color:var(--text-dim);">Releases</a>
    </div>
  </div>
</div>

<!-- ========== GENERATE TAB ========== -->
<div class="tab-panel" id="tab-generate" role="tabpanel" aria-labelledby="tab-btn-generate" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Generate</h2>
    <p class="page-header-subtitle">Generate code and scaffold projects</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/project-setup/generate" target="_blank" rel="noopener" title="Generate documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>

  <!-- Mode Toggle -->
  <div class="subtabs" role="tablist">
    <button class="subtab active" id="genModeCode" onclick="setGenerateMode('code')" role="tab" aria-selected="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
      Generate Code
    </button>
    <button class="subtab" id="genModeScaffold" onclick="setGenerateMode('scaffold')" role="tab" aria-selected="false">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 13h4"/><path d="M10 17h4"/><path d="M10 9h1"/></svg>
      Scaffold Project
    </button>
  </div>

  <!-- Generate Code Panel -->
  <div id="generateCodePanel" style="display:block;">
    <div class="card">
      <div class="card-header"><h3>Generate Code Snippet</h3></div>
      <div class="card-body">
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Target Framework</span>
            <select id="genTarget" onchange="updateGenComponents()" style="width:100%;">
              <option value="vanilla">Node.js + Express</option>
              <option value="nextjs">Next.js + MUI</option>
              <option value="python">Python + Flask</option>
            </select>
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Component</span>
            <select id="genComponent" style="width:100%;">
              <option value="client">API Client</option>
              <option value="connection">MongoDB Connection</option>
              <option value="retrieval">RAG Retrieval</option>
              <option value="ingest">Document Ingest</option>
              <option value="search-api">Search API</option>
            </select>
          </label>
        </div>
        
        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px;">
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Model</span>
            <select id="genModel" style="width:100%;">
              <option value="voyage-4-large">voyage-4-large</option>
              <option value="voyage-4">voyage-4</option>
              <option value="voyage-4-lite">voyage-4-lite</option>
              <option value="voyage-code-3">voyage-code-3</option>
            </select>
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Database</span>
            <input type="text" id="genDb" value="myapp" style="width:100%;">
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Collection</span>
            <input type="text" id="genCollection" value="documents" style="width:100%;">
          </label>
        </div>

        <label style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
          <input type="checkbox" id="genRerank" checked>
          <span>Include reranking</span>
        </label>

        <button class="btn btn-primary" onclick="generateCode()" id="genCodeBtn">
          Generate Code
        </button>
      </div>
    </div>

    <!-- Generated Code Output -->
    <div class="card" id="genOutputCard" style="margin-top:16px;display:none;">
      <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
        <h3 id="genOutputFilename">Generated Code</h3>
        <button class="btn btn-outline" onclick="copyGeneratedCode()" style="padding:4px 12px;">Copy</button>
      </div>
      <div class="card-body" style="padding:0;">
        <pre id="genOutputCode" style="margin:0;padding:16px;overflow:auto;max-height:500px;background:var(--surface-darker);border-radius:0 0 8px 8px;font-size:13px;"></pre>
      </div>
    </div>
  </div>

  <!-- Scaffold Project Panel -->
  <div id="scaffoldPanel" style="display:none;">
    <div class="card">
      <div class="card-header"><h3>Scaffold New Project</h3></div>
      <div class="card-body">
        <p style="margin-bottom:16px;color:var(--text-dim);">Create a complete, ready-to-run project with all files pre-configured.</p>
        
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Project Name</span>
            <input type="text" id="scaffoldName" value="my-rag-app" placeholder="my-rag-app" style="width:100%;">
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Target Framework</span>
            <select id="scaffoldTarget" onchange="updateScaffoldPreview()" style="width:100%;">
              <option value="vanilla">Node.js + Express (9 files)</option>
              <option value="nextjs">Next.js + MUI (13 files)</option>
              <option value="python">Python + Flask (8 files)</option>
            </select>
          </label>
        </div>

        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px;">
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Model</span>
            <select id="scaffoldModel" style="width:100%;">
              <option value="voyage-4-large">voyage-4-large</option>
              <option value="voyage-4">voyage-4</option>
              <option value="voyage-4-lite">voyage-4-lite</option>
            </select>
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Database</span>
            <input type="text" id="scaffoldDb" value="myapp" style="width:100%;">
          </label>
          <label>
            <span style="display:block;margin-bottom:4px;font-weight:500;">Collection</span>
            <input type="text" id="scaffoldCollection" value="documents" style="width:100%;">
          </label>
        </div>

        <!-- Location Picker (Electron only) -->
        <div id="scaffoldLocationSection" style="margin-bottom:16px;display:none;">
          <span style="display:block;margin-bottom:4px;font-weight:500;">Location</span>
          <div style="display:flex;gap:8px;">
            <input type="text" id="scaffoldPath" readonly placeholder="Choose a folder..." style="flex:1;background:var(--surface-darker);">
            <button class="btn btn-outline" onclick="pickScaffoldDirectory()">Browse...</button>
          </div>
        </div>

        <!-- Web Mode: Download ZIP -->
        <div id="scaffoldWebSection" style="margin-bottom:16px;display:none;">
          <div style="padding:12px;background:var(--surface-darker);border-radius:8px;margin-bottom:12px;">
            <p style="margin:0;font-size:14px;">📦 <strong>Web Mode</strong> — Download as ZIP or use CLI</p>
            <p style="margin:8px 0 0;font-size:13px;color:var(--text-dim);">
              CLI: <code style="padding:4px 8px;background:var(--surface);border-radius:4px;">vai scaffold <span id="scaffoldCliName">my-rag-app</span> --target <span id="scaffoldCliTarget">vanilla</span></code>
            </p>
          </div>
        </div>

        <!-- Buttons: different for Electron vs Web -->
        <div id="scaffoldElectronButtons" style="display:none;">
          <button class="btn btn-primary" onclick="createScaffold()" id="scaffoldBtn" disabled>
            Create Project
          </button>
        </div>
        <div id="scaffoldWebButtons" style="display:none;">
          <button class="btn btn-primary" onclick="downloadScaffoldZip()" id="scaffoldDownloadBtn">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
            Download ZIP
          </button>
        </div>
      </div>
    </div>

    <!-- Scaffold Progress/Result -->
    <div class="card" id="scaffoldResultCard" style="margin-top:16px;display:none;">
      <div class="card-header"><h3>Project Created</h3></div>
      <div class="card-body">
        <div id="scaffoldResult"></div>
      </div>
    </div>
  </div>
</div>

<!-- ========== MODELS TAB ========== -->
<div class="tab-panel" id="tab-models" role="tabpanel" aria-labelledby="tab-btn-models" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Models</h2>
    <p class="page-header-subtitle">Voyage AI model showcase</p>
    <p class="page-header-hint">Explore all Voyage AI models, compare specs, and find the right model for your use case.</p>
    <a class="page-header-docs" href="https://docs.voyageai.com/docs/embeddings" target="_blank" rel="noopener" title="Voyage AI model docs"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>

  <!-- Search + Filter -->
  <div class="models-toolbar">
    <input type="text" id="modelsSearch" placeholder="Search models..." oninput="filterModels()" style="max-width:280px;" aria-label="Search models">
    <div class="models-filter-pills" id="modelsFilterPills">
      <button class="models-filter-pill active" data-filter="all" onclick="setModelFilter('all')">All</button>
      <button class="models-filter-pill" data-filter="embedding" onclick="setModelFilter('embedding')">Embedding</button>
      <button class="models-filter-pill" data-filter="reranking" onclick="setModelFilter('reranking')">Reranking</button>
      <button class="models-filter-pill" data-filter="multimodal" onclick="setModelFilter('multimodal')">Multimodal</button>
    </div>
  </div>

  <!-- Shared Embedding Space Hero -->
  <div class="models-hero card" id="modelsHero">
    <div class="models-hero-shape" id="modelsHeroShape"></div>
    <div class="models-hero-content">
      <div class="models-hero-badge">Voyage 4 Family</div>
      <div class="models-hero-title">Shared Embedding Space</div>
      <div class="models-hero-desc">
        voyage-4-large, voyage-4, and voyage-4-lite produce compatible embeddings in the same vector space.
        Index documents with the large model for best quality, then query with the lite model at a fraction of the cost.
        No re-indexing required.
      </div>
      <div class="models-hero-spotlight">
        <div>
          <div class="models-hero-spotlight-badge">Major Announcement</div>
          <div class="models-hero-spotlight-title">voyage-4-nano is open-weight and local-capable</div>
          <div class="models-hero-spotlight-copy">
            The Voyage 4 family now includes <code>voyage-4-nano</code>, an open-weight model for local development and prototyping. It lives in the same shared embedding space as voyage-4-large, voyage-4, and voyage-4-lite, so you can prototype locally and move into asymmetric retrieval workflows without rebuilding your index.
          </div>
          <div class="models-hero-spotlight-note">
            Run locally and compare local vs cloud vectors, and use it as a zero-query-cost option in hybrid retrieval workflows.
          </div>
        </div>
        <div class="models-hero-spotlight-actions">
          <button class="models-hero-spotlight-link primary" onclick="switchTab('nano')">Try Local Nano</button>
          <button class="models-hero-spotlight-link" onclick="scrollToModelCard('voyage-4-nano')">View Model Card</button>
          <a class="models-hero-spotlight-link" href="https://blog.voyageai.com/2026/01/15/voyage-4/" target="_blank" rel="noopener">Read Announcement</a>
        </div>
      </div>
      <div class="models-hero-models" id="modelsHeroModels"></div>
    </div>
  </div>

  <!-- Model Groups (dynamically rendered) -->
  <div id="modelsGroups"></div>

  <!-- RTEB Benchmark Chart -->
  <div class="card" id="modelsRtebCard" style="margin-top:24px;">
    <div class="card-title">RTEB Retrieval Benchmark (NDCG@10)</div>
    <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
      Industry-standard benchmark for retrieval quality across 29 datasets. Higher is better.
    </p>
    <div id="modelsRtebChart"></div>
  </div>
</div>

<!-- ========== OPTIMIZE TAB ========== -->
<div class="tab-panel" id="tab-optimize" role="tabpanel" aria-labelledby="tab-btn-optimize" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Cost Optimizer</h2>
    <p class="page-header-subtitle">Prove that asymmetric retrieval saves money &mdash; on your data</p>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/guides/demos/cost-optimizer" target="_blank" rel="noopener" title="Cost Optimizer documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>
  <!-- Landing / Intro (shown by default, replaced by JS when user proceeds) -->
  <div id="optimize-landing">
    <div class="card">
      <div class="card-title">How This Demo Works</div>
      <p style="color: var(--text-dim); margin-bottom: 16px; line-height: 1.6;">
        Voyage AI's <strong>shared embedding space</strong> means documents embedded with
        <code>voyage-4-large</code> can be queried with <code>voyage-4-lite</code> at a fraction
        of the cost &mdash; with nearly identical retrieval results. This demo proves it.
      </p>
      <div style="display: grid; grid-template-columns: auto 1fr; gap: 10px 16px; margin-bottom: 20px; font-size: 13px; color: var(--text-dim);">
        <div style="font-weight: 600; color: var(--text);">Step 1</div>
        <div><strong>Prepare data</strong> &mdash; Embed 65 sample documents with voyage-4-large and store in MongoDB Atlas</div>
        <div style="font-weight: 600; color: var(--text);">Step 2</div>
        <div><strong>Compare models</strong> &mdash; Run the same queries with voyage-4-large and voyage-4-lite, measure retrieval overlap</div>
        <div style="font-weight: 600; color: var(--text);">Step 3</div>
        <div><strong>See savings</strong> &mdash; Project annual cost savings at your scale</div>
      </div>
      <div style="background: rgba(255, 193, 7, 0.08); border: 1px solid rgba(255, 193, 7, 0.25); border-radius: 6px; padding: 12px; margin-bottom: 20px; font-size: 13px; color: var(--text-dim);">
        <strong style="color: #ffc107;">Heads up:</strong> This demo uses your Voyage AI API key to embed documents and run queries.
        The sample data requires approximately <strong>~50K tokens</strong> for embedding and
        <strong>~1.5K tokens</strong> per analysis run. All data is stored in your MongoDB Atlas cluster
        under <code>vai_demo.cost_optimizer_demo</code>.
      </div>
      <button class="btn" id="optimize-get-started-btn" style="width: 100%;">Get Started</button>
    </div>
  </div>

  <!-- Configuration Panel (hidden until user proceeds past landing) -->
  <div id="optimize-config-panel" style="display: none;">
    <div class="card">
      <div class="card-title">Configuration</div>

      <div style="margin-bottom: 16px;">
        <label style="display: block; font-size: 13px; margin-bottom: 6px; font-weight: 500;">Database</label>
        <input type="text" id="optimize-db" placeholder="vai_demo" value="vai_demo" style="width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 13px;">
      </div>

      <div style="margin-bottom: 16px;">
        <label style="display: block; font-size: 13px; margin-bottom: 6px; font-weight: 500;">Collection</label>
        <input type="text" id="optimize-collection" placeholder="cost_optimizer_demo" value="cost_optimizer_demo" style="width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 13px;">
      </div>

      <div style="margin-bottom: 16px;">
        <button class="btn" id="optimize-run-btn" style="width: 100%;" disabled>Run Analysis</button>
      </div>
    </div>
  </div>

  <!-- Results Panel -->
  <div id="optimize-results" style="margin-top: 16px;"></div>
</div>

<!-- ========== EXPLORE TAB ========== -->
<div class="tab-panel" id="tab-explore" role="tabpanel" aria-labelledby="tab-btn-explore" tabindex="0">
  <div class="page-header explore-page-header">
    <div class="explore-header-copy">
      <h2 class="page-header-title">Explore</h2>
      <p class="page-header-subtitle">Learn embedding and vector search concepts</p>
      <p class="page-header-hint">Browse interactive explanations of key topics, from cosine similarity to quantization to RAG pipelines.</p>
    </div>
    <div class="explore-header-guide" id="exploreHeaderGuide" aria-hidden="true">
      <div class="explore-header-robot" id="exploreHeaderRobot"></div>
      <div>
        <div class="explore-header-guide-eyebrow">VAI Guide</div>
        <div class="explore-header-guide-text">Pick a concept, learn the idea, then jump into a live playground demo.</div>
      </div>
    </div>
    <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/tools-and-learning/explain" target="_blank" rel="noopener" title="Explore documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
  </div>
  <div style="margin-bottom:16px;">
    <input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;" aria-label="Search concepts">
  </div>
  <div class="explore-grid" id="exploreGrid"></div>
</div>

<!-- Explore Concept Modal -->
<!-- Agent Mode Info Modal -->
<div class="agent-info-overlay" id="agentInfoModal">
  <div class="agent-info-modal">
    <h3><span class="mode-icon">&#x2699;&#xFE0F;</span> Switching to Agent Mode</h3>
    <div class="mode-compare">
      <div class="mode-card">
        <h4>Pipeline Mode</h4>
        <ul>
          <li>Fixed RAG: retrieve &rarr; generate</li>
          <li>Always searches your KB first</li>
          <li>Fast, predictable responses</li>
          <li>Best for direct Q&amp;A from your docs</li>
        </ul>
      </div>
      <div class="mode-card active">
        <h4>Agent Mode</h4>
        <ul>
          <li>LLM decides which tools to call</li>
          <li>Can search, compare, embed, explore</li>
          <li>Uses <code>vai_query</code>, <code>vai_search</code>, etc.</li>
          <li>Best for complex, multi-step questions</li>
        </ul>
      </div>
    </div>
    <div class="mode-note">
      In Agent mode, the LLM chooses how to answer &mdash; it may call <strong>vai_query</strong> to search your KB, <strong>vai_collections</strong> to discover data, or answer from its own knowledge. You'll see a <em>Thinking</em> panel showing each tool call in real time. Responses may take longer but can be more thorough.
    </div>
    <div class="mode-actions">
      <label class="mode-dismiss-label">
        <input type="checkbox" id="agentInfoDismissCheck"> Don't show this again
      </label>
      <button class="mode-ok-btn" id="agentInfoOkBtn">Got it</button>
    </div>
  </div>
</div>

<div class="explore-modal-overlay" id="exploreModal" role="dialog" aria-modal="true" aria-labelledby="exploreModalTitle" aria-describedby="exploreModalSummary">
  <div class="explore-modal">
    <button class="explore-modal-close" id="exploreModalClose" aria-label="Close modal">&times;</button>
    <div class="explore-modal-header">
      <div class="explore-modal-icon" id="exploreModalIcon" aria-hidden="true"></div>
      <div class="explore-modal-heading">
        <div class="explore-modal-title" id="exploreModalTitle"></div>
        <div class="explore-modal-summary" id="exploreModalSummary"></div>
      </div>
      <div class="explore-modal-guide" id="exploreModalGuide" aria-hidden="true">
        <div class="explore-modal-robot-sprite" id="exploreModalRobot"></div>
        <div>
          <div class="explore-modal-guide-label">Guide</div>
          <div class="explore-modal-guide-copy" id="exploreModalGuideCopy">Learn the concept here, then try it live in the playground.</div>
        </div>
      </div>
    </div>
    <div class="explore-modal-body" id="exploreModalBody"></div>
    <div class="explore-modal-actions" id="exploreModalActions"></div>
  </div>
</div>

<!-- Hidden global model select (used by JS for model sync) -->
<select id="globalModel" style="display:none;"></select>

<!-- ========== MODEL PICKER OVERLAY ========== -->
<div class="model-picker-overlay" id="modelPickerOverlay" role="dialog" aria-modal="true" aria-labelledby="modelPickerTitle">
  <div class="model-picker">
    <div class="model-picker-header">
      <div>
        <div class="model-picker-title" id="modelPickerTitle">Choose a Model</div>
        <div class="model-picker-context" id="modelPickerContext"></div>
      </div>
      <button class="model-picker-close" id="modelPickerClose" aria-label="Close">&times;</button>
    </div>
    <div class="model-picker-search">
      <input type="text" id="modelPickerSearch" placeholder="Search models..." oninput="filterModelPicker()" aria-label="Search models">
    </div>
    <div class="model-picker-recommendation" id="modelPickerRecommendation"></div>
    <div class="model-picker-list" id="modelPickerList" role="listbox"></div>
    <div class="model-picker-footer">
      <a href="#" onclick="event.preventDefault(); closeModelPicker(); switchTab('models');">View all models in catalog</a>
    </div>
  </div>
</div>

<!-- ========== LOCAL INFERENCE TAB ========== -->
<div class="tab-panel" id="tab-nano" role="tabpanel" aria-labelledby="tab-btn-nano" tabindex="0">
  <div class="page-header">
    <h2 class="page-header-title">Local Inference</h2>
    <p class="page-header-subtitle">Generate embeddings locally with voyage-4-nano</p>
    <p class="page-header-hint">Run embeddings on your machine — no API key required. Uses the voyage-4-nano model via Python bridge.</p>
  </div>

  <!-- Setup Status Section -->
  <div class="card" id="nanoStatusCard">
    <div class="card-title">Setup Status</div>
    <div id="nanoStatusContent">
      <div class="nano-status-loading">Checking nano bridge status...</div>
    </div>
  </div>

  <!-- Setup Required Banner (shown when not ready) -->
  <div class="nano-setup-banner" id="nanoSetupBanner" style="display:none;">
    <div class="nano-setup-banner-icon">&#9888;</div>
    <div class="nano-setup-banner-text">
      <strong>Nano bridge not ready</strong>
      <p>Run <code>vai nano setup</code> in your terminal to install Python dependencies and download the model.</p>
    </div>
  </div>

  <!-- Ready Banner (shown when all components are ok) -->
  <div class="nano-ready-banner" id="nanoReadyBanner" style="display:none;">
    &#10003; Nano bridge ready — generate embeddings locally below
  </div>

  <!-- Embed Section (shown when nano bridge is ready) -->
  <div id="nanoEmbedSection" style="display:none;">
    <div class="card" style="margin-top:16px;">
      <div class="card-title">Embed Text</div>
      <textarea id="nanoEmbedInput" rows="4" placeholder="Enter text to embed locally..." aria-label="Text to embed locally">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
    </div>

    <div class="options-row">
      <div class="option-group">
        <span class="option-label">Dimension</span>
        <select id="nanoEmbedDimension">
          <option value="256">256</option>
          <option value="512">512</option>
          <option value="1024" selected>1024</option>
          <option value="2048">2048</option>
        </select>
      </div>
      <div class="option-group">
        <span class="option-label">Quantization</span>
        <select id="nanoEmbedQuantization">
          <option value="float32" selected>float32</option>
          <option value="int8">int8 (4x smaller)</option>
          <option value="uint8">uint8 (4x smaller)</option>
          <option value="binary">binary (32x smaller)</option>
        </select>
      </div>
      <button class="btn" id="nanoEmbedBtn" onclick="nanoDoEmbed()">
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
        Embed Locally
      </button>
    </div>

    <div class="error-msg" id="nanoEmbedError"></div>

    <div class="result-section" id="nanoEmbedResult">
      <div class="card">
        <div class="card-title">Result</div>
        <div id="nanoEmbedStats"></div>
        <div class="vector-preview" id="nanoEmbedVector"></div>
        <div style="margin-top:8px;">
          <button class="btn btn-secondary btn-small" onclick="nanoCopyVector()">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z"/></svg>
            Copy Full Vector
          </button>
        </div>
        <div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
        <div class="heatmap" id="nanoEmbedHeatmap"></div>
      </div>
    </div>
  </div>

  <!-- Similarity Matrix Section (shown when nano bridge is ready) -->
  <div id="nanoSimilaritySection" style="display:none;">
    <div class="card" style="margin-top:16px;">
      <div class="card-title">Similarity Matrix</div>
      <textarea id="nanoSimTexts" rows="6" placeholder="Enter 2-10 texts, one per line..." aria-label="Texts for similarity comparison">Vector search finds similar documents using embeddings
Embedding models convert text into numerical vectors
Cosine similarity measures the angle between two vectors
Retrieval augmented generation combines search with LLMs</textarea>
    </div>

    <div class="options-row">
      <div class="option-group">
        <span class="option-label">Dimension</span>
        <select id="nanoSimDimension">
          <option value="256">256</option>
          <option value="512">512</option>
          <option value="1024" selected>1024</option>
          <option value="2048">2048</option>
        </select>
      </div>
      <button class="btn" id="nanoSimBtn" onclick="nanoDoSimilarity()">
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
        Compare
      </button>
    </div>

    <div class="error-msg" id="nanoSimError"></div>

    <div class="result-section" id="nanoSimResult">
      <div class="card">
        <div class="card-title">Results</div>
        <div id="nanoSimStats"></div>
        <div id="nanoSimHeatmap"></div>
        <div id="nanoSimHighlights"></div>
      </div>
    </div>
  </div>

  <!-- Dimension Comparison Section (shown when nano bridge is ready) -->
  <div id="nanoDimensionSection" style="display:none;">
    <div class="card" style="margin-top:16px;">
      <div class="card-title">MRL Dimension Comparison</div>
      <textarea id="nanoDimText" rows="3" placeholder="Enter text to compare across dimensions..." aria-label="Text for dimension comparison">Vector search finds similar documents using high-dimensional embeddings.</textarea>
    </div>

    <div class="options-row">
      <button class="btn" id="nanoDimBtn" onclick="nanoDoDimensions()">
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
        Compare Dimensions
      </button>
    </div>

    <div class="error-msg" id="nanoDimError"></div>

    <div class="result-section" id="nanoDimResult">
      <div class="card">
        <div class="card-title">Results</div>
        <div id="nanoDimStats"></div>
        <div id="nanoDimCards"></div>
      </div>
    </div>
  </div>

  <!-- Cross-Bridge Comparison Section (shown when nano bridge is ready) -->
  <div id="nanoCrossbridgeSection" style="display:none;">
    <div class="card" style="margin-top:16px;">
      <div class="card-title">Cross-Bridge Comparison</div>
      <p style="color:var(--text-dim);font-size:0.9em;margin-bottom:12px;">Compare nano (local) vs API (cloud) embeddings for the same text to verify they share the same vector space.</p>
      <textarea id="nanoXbridgeText" rows="3" placeholder="Enter text to compare across bridges..." aria-label="Text for cross-bridge comparison">MongoDB Atlas Vector Search enables semantic search over your data using embeddings.</textarea>
    </div>

    <div class="options-row">
      <div class="option-group">
        <span class="option-label">Dimension</span>
        <select id="nanoXbridgeDimension">
          <option value="256">256</option>
          <option value="512">512</option>
          <option value="1024" selected>1024</option>
          <option value="2048">2048</option>
        </select>
      </div>
      <button class="btn" id="nanoXbridgeBtn" onclick="nanoDoCrossbridge()">
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M4 12h16M4 12l4-4M4 12l4 4M20 12l-4-4M20 12l-4 4"/></svg>
        Compare Bridges
      </button>
    </div>

    <div class="error-msg" id="nanoXbridgeError"></div>

    <div class="result-section" id="nanoXbridgeResult">
      <div class="card">
        <div class="card-title">Results</div>
        <div id="nanoXbridgeNoKey" class="nano-xbridge-no-key" style="display:none;">
          &#128273; No API key configured. Add your Voyage AI API key in Settings to enable cross-bridge comparison.
        </div>
        <div id="nanoXbridgeContent"></div>
      </div>
    </div>
  </div>
</div>

<!-- ========== SETTINGS TAB ========== -->
<div class="tab-panel" id="tab-settings" role="tabpanel" aria-labelledby="tab-btn-settings" tabindex="0">
  <div class="settings-layout">
    <!-- Settings left nav -->
    <nav class="settings-nav">
      <div class="settings-nav-header">Settings</div>
      <button class="settings-nav-item active" data-settings-section="general">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
        <span>General</span>
      </button>
      <button class="settings-nav-item" data-settings-section="database">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-database"/></svg>
        <span>Database</span>
      </button>
      <button class="settings-nav-item" data-settings-section="appearance">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-palette"/></svg>
        <span>Appearance</span>
      </button>
      <button class="settings-nav-item" data-settings-section="models">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-cube"/></svg>
        <span>Models</span>
      </button>
      <button class="settings-nav-item" data-settings-section="chat">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-chat"/></svg>
        <span>Chat</span>
      </button>
      <button class="settings-nav-item" data-settings-section="benchmark">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-gauge"/></svg>
        <span>Benchmark</span>
      </button>
      <button class="settings-nav-item" data-settings-section="privacy">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-shield"/></svg>
        <span>Data &amp; Privacy</span>
      </button>
      <button class="settings-nav-item" data-settings-section="mcp">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-plug"/></svg>
        <span>MCP</span>
      </button>
      <button class="settings-nav-item" data-settings-section="health">
        <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-pulse"/></svg>
        <span>Health Check</span>
      </button>
    </nav>

    <!-- Settings content panels -->
    <div class="settings-content">

      <!-- ── General ── -->
      <div class="settings-panel active" id="settings-general">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">General</h3>
          <p class="settings-panel-subtitle">API access and connection settings</p>
        </div>
        <div class="settings-section">
          <div class="settings-api-banner" id="settingsApiBanner">
            <span class="settings-api-banner-icon" id="settingsApiBannerIcon">⚠️</span>
            <span id="settingsApiKeyMsg">No API key configured — add your key below to get started.</span>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">API Key <span class="settings-origin" data-origin-key="apiKey"></span></span>
              <span class="settings-label-hint">Encrypted via OS keychain · <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:2px;"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>Get a key</a></span>
            </div>
            <div class="settings-control" style="min-width:260px;">
              <div class="settings-api-field">
                <input type="password" id="settingsApiKey" placeholder="pa-..." autocomplete="off" spellcheck="false">
                <button type="button" id="settingsApiKeyToggle" title="Show/hide key"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></button>
                <button type="button" id="settingsApiKeySave" class="save-btn" title="Save key">Save</button>
              </div>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">API Base URL <span class="settings-origin" data-origin-key="apiBase"></span></span>
              <span class="settings-label-hint">Override the default endpoint (leave empty for default)</span>
            </div>
            <div class="settings-control">
              <input type="text" class="settings-input" id="settingsApiBase" placeholder="https://api.voyageai.com/v1">
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Request Timeout</span>
              <span class="settings-label-hint">Max seconds to wait for API responses</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsTimeout">
                <option value="15">15 seconds</option>
                <option value="30" selected>30 seconds</option>
                <option value="60">60 seconds</option>
                <option value="120">120 seconds</option>
              </select>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Default Tab</span>
              <span class="settings-label-hint">Which tab to open when the app starts</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsDefaultTab">
                <option value="home" selected>Overview</option>
                <option value="embed">Embed</option>
                <option value="compare">Compare</option>
                <option value="search">Rerank</option>
                <option value="multimodal">Multimodal</option>
                <option value="generate">Generate</option>
                <option value="chat">Chat</option>
                <option value="workflows">Workflows</option>
                <option value="benchmark">Benchmark</option>
                <option value="explore">Explore</option>
                <option value="nano">Local Inference</option>
                <option value="models">Models</option>
                <option value="about">About</option>
                <option value="settings">Settings</option>
              </select>
            </div>
          </div>
        </div>
      </div>

      <!-- ── Database ── -->
      <div class="settings-panel" id="settings-database">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Database</h3>
          <p class="settings-panel-subtitle">MongoDB Atlas connection and default targets</p>
        </div>
        <div class="settings-section">
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">MongoDB URI <span class="settings-origin" data-origin-key="mongodbUri"></span></span>
              <span class="settings-label-hint">Connection string for MongoDB Atlas · <a href="https://cloud.mongodb.com" target="_blank" class="settings-key-link"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:2px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Atlas Console</a></span>
            </div>
            <div class="settings-control" style="min-width:260px;">
              <div class="settings-api-field">
                <input type="password" id="settingsMongoUri" placeholder="mongodb+srv://user:pass@cluster.mongodb.net/" autocomplete="off" spellcheck="false">
                <button type="button" id="settingsMongoUriToggle" title="Show/hide"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></button>
                <button type="button" id="settingsMongoUriSave" class="save-btn" title="Save">Save</button>
              </div>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Default Database <span class="settings-origin" data-origin-key="db"></span></span>
              <span class="settings-label-hint">Used by workflows and commands when no database is specified</span>
            </div>
            <div class="settings-control">
              <input type="text" class="settings-input" id="settingsDefaultDb" placeholder="e.g. my_knowledge_base">
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Default Collection <span class="settings-origin" data-origin-key="collection"></span></span>
              <span class="settings-label-hint">Used by workflows and commands when no collection is specified</span>
            </div>
            <div class="settings-control">
              <input type="text" class="settings-input" id="settingsDefaultCollection" placeholder="e.g. documents">
            </div>
          </div>
        </div>
      </div>

      <!-- ── Appearance ── -->
      <div class="settings-panel" id="settings-appearance">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Appearance</h3>
          <p class="settings-panel-subtitle">Theme and visualization options</p>
        </div>
        <div class="settings-section">
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Theme</span>
              <span class="settings-label-hint">Controls the color scheme of the interface</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsTheme">
                <option value="dark">Dark</option>
                <option value="light">Light</option>
              </select>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Vector Heatmap Colors</span>
              <span class="settings-label-hint">Color palette for embedding visualizations</span>
            </div>
            <div class="settings-control" style="display:flex;align-items:center;gap:8px;">
              <select class="settings-select" id="settingsHeatmap">
                <option value="viridis">Viridis</option>
                <option value="plasma">Plasma</option>
                <option value="inferno">Inferno</option>
                <option value="magma">Magma</option>
                <option value="cividis">Cividis</option>
                <option value="turbo">Turbo</option>
              </select>
              <span class="heatmap-preview" id="heatmapPreview"></span>
            </div>
          </div>
        </div>
      </div>

      <!-- ── Models ── -->
      <div class="settings-panel" id="settings-models">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Models</h3>
          <p class="settings-panel-subtitle">Default embedding model preferences</p>
        </div>
        <div class="settings-section">
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Default Embedding Model</span>
              <span class="settings-label-hint">Pre-selected model for Embed and Compare tabs</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsDefaultModel"></select>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Default Input Type</span>
              <span class="settings-label-hint">Pre-selected input type for embedding requests</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsInputType">
                <option value="document">document</option>
                <option value="query">query</option>
              </select>
            </div>
          </div>
        </div>
      </div>

      <!-- ── Chat ── -->
      <div class="settings-panel" id="settings-chat">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Chat</h3>
          <p class="settings-panel-subtitle">Chat configuration has moved to the Knowledge Base panel in the Chat tab.</p>
        </div>
        <div class="settings-section" style="text-align:center; padding:24px;">
          <p style="color:var(--text-muted); font-size:13px; margin-bottom:16px;">All chat settings (LLM provider, model, database, instructions) are now accessible directly from the KB panel sidebar when using Chat.</p>
          <button class="btn" onclick="switchTab('chat')">Go to Chat</button>
        </div>
      </div>

      <!-- ── Benchmark ── -->
      <div class="settings-panel" id="settings-benchmark">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Benchmark</h3>
          <p class="settings-panel-subtitle">Benchmark iteration and display defaults</p>
        </div>
        <div class="settings-section">
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Iterations per Model</span>
              <span class="settings-label-hint">Number of runs when benchmarking latency</span>
            </div>
            <div class="settings-control">
              <select class="settings-select" id="settingsBenchIter">
                <option value="3">3 runs</option>
                <option value="5" selected>5 runs</option>
                <option value="10">10 runs</option>
                <option value="20">20 runs</option>
              </select>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Show Detailed Timings</span>
              <span class="settings-label-hint">Display p50/p95/p99 in benchmark results</span>
            </div>
            <div class="settings-control">
              <button class="settings-toggle" id="settingsDetailedTimings" type="button"></button>
            </div>
          </div>
        </div>
      </div>

      <!-- ── Data & Privacy ── -->
      <div class="settings-panel" id="settings-privacy">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Data &amp; Privacy</h3>
          <p class="settings-panel-subtitle">Caching, analytics, and data management</p>
        </div>
        <div class="settings-section">
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Persist Embeddings Locally</span>
              <span class="settings-label-hint">Cache embedding results in browser storage to avoid re-fetching</span>
            </div>
            <div class="settings-control">
              <button class="settings-toggle active" id="settingsCacheEmbeddings" type="button"></button>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Clear Cached Data</span>
              <span class="settings-label-hint">Remove all locally stored embeddings and preferences</span>
            </div>
            <div class="settings-control">
              <button class="settings-reset-btn" id="settingsClearCache">Clear All Data</button>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Anonymous Usage Analytics</span>
              <span class="settings-label-hint">Help improve Vai by sharing anonymous usage data (version, platform, features used). No API keys or personal data.</span>
            </div>
            <div class="settings-control">
              <button class="settings-toggle active" id="settingsTelemetry" type="button"></button>
            </div>
          </div>
        </div>
        <!-- Version Info (visible in Electron only) -->
        <div class="settings-section" id="settingsVersionSection" style="display:none;">
          <div class="settings-section-title">Version</div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Vai Desktop</span>
              <span class="settings-label-hint">Electron desktop application</span>
            </div>
            <div class="settings-control">
              <span class="version-badge" id="settingsAppVersion">—</span>
            </div>
          </div>
          <div class="settings-row">
            <div class="settings-label">
              <span class="settings-label-text">Vai CLI Engine</span>
              <span class="settings-label-hint">Underlying CLI powering embeddings, search &amp; reranking</span>
            </div>
            <div class="settings-control">
              <span class="version-badge" id="settingsCliVersion">—</span>
            </div>
          </div>
        </div>
      </div>

      <!-- ── MCP Integrations ── -->
      <div class="settings-panel" id="settings-mcp">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">MCP Integrations</h3>
          <p class="settings-panel-subtitle">Install the vai MCP server into your AI tools and editors</p>
        </div>
        <div class="settings-section">
          <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
            <span class="settings-section-title" style="margin:0;">AI Tools</span>
            <button class="btn" id="mcpRefreshBtn" onclick="loadMcpStatus()" style="padding:6px 16px;font-size:13px;">Refresh Status</button>
          </div>
          <div id="mcpLoading" style="display:none;text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">Loading MCP status...</div>
          <div id="mcpGrid" style="display:flex;flex-direction:column;gap:10px;"></div>
        </div>
      </div>

      <!-- ── Health Check ── -->
      <div class="settings-panel" id="settings-health">
        <div class="settings-panel-header">
          <h3 class="settings-panel-title">Health Check</h3>
          <p class="settings-panel-subtitle">Validate your vai setup and connectivity</p>
        </div>
        <div class="settings-section">
          <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
            <span class="settings-section-title" style="margin:0;">Diagnostics</span>
            <button class="btn" id="doctorRunBtn" style="padding:6px 16px;font-size:13px;">Run Health Check</button>
          </div>
          <div id="doctorResults" style="display:none;">
            <div id="doctorCheckList" style="display:flex;flex-direction:column;gap:6px;"></div>
            <div id="doctorSummary" style="margin-top:16px;padding:12px 16px;border-radius:var(--radius);font-size:13px;"></div>
          </div>
          <div id="doctorLoading" style="display:none;text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
            Running health checks...
          </div>
          <div id="doctorEmpty" style="text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
            Click "Run Health Check" to validate your setup.
          </div>
        </div>
      </div>

      <div style="text-align:center;padding:8px 0;">
        <span class="settings-saved" id="settingsSavedMsg">✓ Saved</span>
      </div>

    </div><!-- .settings-content -->
  </div><!-- .settings-layout -->
</div>

</div><!-- .main -->
</div><!-- .content-area -->
</div><!-- .app-shell -->

<script>
// Apply saved theme immediately to prevent flash
(function() {
  var t = localStorage.getItem('vai-theme') || 'dark';
  if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
  // Set correct logo immediately
  var logo = document.getElementById('sidebarLogo');
  if (logo) logo.src = '/icons/' + (t === 'light' ? 'light' : 'dark') + '/64.png';
})();
</script>
<script src="./js/robot-game-assets.js"></script>
<script>
(function() {
'use strict';

// ── Brand Shape Generators ──
// Seeded PRNG (mulberry32) + SVG shape generators per vai-brand-shape-design-spec.md
function seededRandom(seed) {
  let s = seed | 0;
  return function() {
    s = (s + 0x6D2B79F5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function generateNebulaSVG(w, h, seed, opts = {}) {
  const rng = seededRandom(seed);
  const count = opts.count || (opts.variant === 'rich' ? 6 : opts.variant === 'subtle' ? 3 : 4);
  const opMax = opts.variant === 'rich' ? 0.15 : opts.variant === 'subtle' ? 0.06 : 0.10;
  const opMin = opts.variant === 'rich' ? 0.05 : opts.variant === 'subtle' ? 0.03 : 0.04;
  const blur = Math.round(Math.min(w, h) * 0.10);
  const id = 'nb' + seed;
  let els = '';
  for (let i = 0; i < count; i++) {
    const cx = w * (0.15 + rng() * 0.70);
    const cy = h * (0.15 + rng() * 0.70);
    const rx = 30 + rng() * 80 * (w / 300);
    const ry = 25 + rng() * 60 * (h / 300);
    const op = opMin + rng() * (opMax - opMin);
    const color = rng() < 0.55 ? '#00D4AA' : '#40E0FF';
    const rot = Math.round(rng() * 360);
    els += `<ellipse cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" rx="${rx.toFixed(1)}" ry="${ry.toFixed(1)}" fill="${color}" opacity="${op.toFixed(3)}" transform="rotate(${rot} ${cx.toFixed(1)} ${cy.toFixed(1)})" filter="url(#${id})"/>`;
  }
  const sparkleCount = opts.sparkles !== false ? 20 + Math.round(rng() * 15) : 0;
  for (let i = 0; i < sparkleCount; i++) {
    const sx = w * (0.1 + rng() * 0.8);
    const sy = h * (0.1 + rng() * 0.8);
    const sr = 0.5 + rng() * 1.5;
    const so = 0.15 + rng() * 0.45;
    const sc = rng() < 0.5 ? '#00D4AA' : '#40E0FF';
    els += `<circle cx="${sx.toFixed(1)}" cy="${sy.toFixed(1)}" r="${sr.toFixed(1)}" fill="${sc}" opacity="${so.toFixed(2)}"/>`;
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true"><defs><filter id="${id}"><feGaussianBlur stdDeviation="${blur}"/></filter></defs>${els}</svg>`;
}

function generateParticleFieldSVG(w, h, seed, opts = {}) {
  const rng = seededRandom(seed);
  const count = opts.dense ? 100 + Math.round(rng() * 20) : 50 + Math.round(rng() * 20);
  const variant = opts.variant || 'scattered';
  const particles = [];
  let clusters;
  if (variant === 'clustered') {
    const nc = 3 + Math.round(rng() * 3);
    clusters = Array.from({length: nc}, () => ({x: w * (0.15 + rng() * 0.7), y: h * (0.15 + rng() * 0.7)}));
  }
  for (let i = 0; i < count; i++) {
    let px, py;
    if (variant === 'flowing') {
      px = rng() * w;
      py = h * 0.5 + Math.sin((px / w) * Math.PI * 2 + rng() * 0.5) * h * 0.3 + (rng() - 0.5) * h * 0.15;
    } else if (variant === 'clustered' && clusters) {
      const c = clusters[Math.floor(rng() * clusters.length)];
      px = c.x + (rng() - 0.5) * w * 0.25;
      py = c.y + (rng() - 0.5) * h * 0.25;
    } else {
      px = rng() * w;
      py = rng() * h;
    }
    const pr = 1 + rng() * 3;
    const po = 0.2 + rng() * 0.5;
    const pc = rng() < 0.65 ? '#00D4AA' : '#40E0FF';
    particles.push({x: px, y: py, r: pr, o: po, c: pc});
  }
  let els = particles.map(p => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="${p.r.toFixed(1)}" fill="${p.c}" opacity="${p.o.toFixed(2)}"/>`).join('');
  if (variant === 'constellation') {
    const thresh = opts.threshold || 55;
    for (let i = 0; i < particles.length; i++) {
      for (let j = i + 1; j < particles.length; j++) {
        const dx = particles[i].x - particles[j].x, dy = particles[i].y - particles[j].y;
        const d = Math.sqrt(dx * dx + dy * dy);
        if (d < thresh) {
          const lo = (0.3 * (1 - d / thresh)).toFixed(2);
          els += `<line x1="${particles[i].x.toFixed(1)}" y1="${particles[i].y.toFixed(1)}" x2="${particles[j].x.toFixed(1)}" y2="${particles[j].y.toFixed(1)}" stroke="#00D4AA" stroke-width="0.6" opacity="${lo}"/>`;
        }
      }
    }
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true">${els}</svg>`;
}

function generateLatticeSVG(w, h, seed, opts = {}) {
  const rng = seededRandom(seed);
  const cols = opts.columns || 10;
  const warp = opts.warp != null ? opts.warp : 0.4;
  const cellW = w / (cols - 1);
  const rows = Math.max(2, Math.round(h / cellW));
  const cellH = h / (rows - 1);
  const pts = [];
  for (let r = 0; r < rows; r++) {
    pts[r] = [];
    for (let c = 0; c < cols; c++) {
      const edgeDist = Math.min(c, cols - 1 - c, r, rows - 1 - r) / Math.max(cols, rows);
      const warpAmt = warp * cellW * (1 - edgeDist * 2);
      const visible = rng() < 0.85;
      pts[r][c] = {
        x: c * cellW + (rng() - 0.5) * warpAmt,
        y: r * cellH + (rng() - 0.5) * warpAmt,
        visible
      };
    }
  }
  let els = '';
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const p = pts[r][c];
      if (!p.visible) continue;
      const pr = 1 + rng() * 2;
      const po = 0.15 + rng() * 0.5;
      const pc = rng() < 0.8 ? '#00D4AA' : '#40E0FF';
      els += `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="${pr.toFixed(1)}" fill="${pc}" opacity="${po.toFixed(2)}"/>`;
      if (c < cols - 1 && pts[r][c + 1].visible && rng() < 0.78) {
        const eo = (0.06 + rng() * 0.19).toFixed(2);
        els += `<line x1="${p.x.toFixed(1)}" y1="${p.y.toFixed(1)}" x2="${pts[r][c+1].x.toFixed(1)}" y2="${pts[r][c+1].y.toFixed(1)}" stroke="#00D4AA" stroke-width="0.6" opacity="${eo}"/>`;
      }
      if (r < rows - 1 && pts[r + 1][c].visible && rng() < 0.78) {
        const eo = (0.06 + rng() * 0.19).toFixed(2);
        els += `<line x1="${p.x.toFixed(1)}" y1="${p.y.toFixed(1)}" x2="${pts[r+1][c].x.toFixed(1)}" y2="${pts[r+1][c].y.toFixed(1)}" stroke="#00D4AA" stroke-width="0.6" opacity="${eo}"/>`;
      }
      if (c < cols - 1 && r < rows - 1 && rng() < 0.25) {
        const diag = pts[r + 1][c + 1];
        if (diag.visible) {
          const eo = (0.04 + rng() * 0.12).toFixed(2);
          els += `<line x1="${p.x.toFixed(1)}" y1="${p.y.toFixed(1)}" x2="${diag.x.toFixed(1)}" y2="${diag.y.toFixed(1)}" stroke="#00D4AA" stroke-width="0.4" opacity="${eo}"/>`;
        }
      }
    }
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true">${els}</svg>`;
}

function generateOrbitalArcsSVG(w, h, seed, opts = {}) {
  const rng = seededRandom(seed);
  const arcCount = opts.arcs || (2 + Math.round(rng() * 4));
  const cx = w / 2, cy = h / 2;
  let els = '';
  for (let i = 0; i < arcCount; i++) {
    const rx = w * (0.20 + rng() * 0.20);
    const ry = h * (0.15 + rng() * 0.15);
    const startAngle = rng() * Math.PI * 2;
    const sweep = (60 + rng() * 180) * Math.PI / 180;
    const rot = (-30 + rng() * 90);
    const sw = 1 + rng() * 2;
    const so = 0.2 + rng() * 0.5;
    const sc = rng() < 0.65 ? '#00D4AA' : '#40E0FF';
    const x1 = cx + rx * Math.cos(startAngle);
    const y1 = cy + ry * Math.sin(startAngle);
    const x2 = cx + rx * Math.cos(startAngle + sweep);
    const y2 = cy + ry * Math.sin(startAngle + sweep);
    const largeArc = sweep > Math.PI ? 1 : 0;
    els += `<path d="M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${rx.toFixed(1)} ${ry.toFixed(1)} ${rot.toFixed(0)} ${largeArc} 1 ${x2.toFixed(1)} ${y2.toFixed(1)}" fill="none" stroke="${sc}" stroke-width="${sw.toFixed(1)}" opacity="${so.toFixed(2)}" stroke-linecap="round"/>`;
    const dotR = 1.5 + rng() * 2.5;
    els += `<circle cx="${x2.toFixed(1)}" cy="${y2.toFixed(1)}" r="${dotR.toFixed(1)}" fill="${sc}" opacity="${so.toFixed(2)}"/>`;
  }
  const scatterCount = 8 + Math.round(rng() * 7);
  for (let i = 0; i < scatterCount; i++) {
    const angle = rng() * Math.PI * 2;
    const dist = 0.3 + rng() * 0.5;
    const sx = cx + w * dist * 0.4 * Math.cos(angle);
    const sy = cy + h * dist * 0.4 * Math.sin(angle);
    const sr = 0.6 + rng() * 1.4;
    const so = 0.1 + rng() * 0.35;
    const sc = rng() < 0.65 ? '#00D4AA' : '#40E0FF';
    els += `<circle cx="${sx.toFixed(1)}" cy="${sy.toFixed(1)}" r="${sr.toFixed(1)}" fill="${sc}" opacity="${so.toFixed(2)}"/>`;
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true">${els}</svg>`;
}

// ── Theme Toggle ──
function initThemeToggle() {
  const toggle = document.getElementById('themeToggle');
  const saved = localStorage.getItem('vai-theme') || 'dark';
  let current = saved;

  function applyTheme(theme) {
    current = theme;
    const logo = document.getElementById('sidebarLogo');
    if (theme === 'light') {
      document.documentElement.setAttribute('data-theme', 'light');
      toggle.textContent = '☀️';
      toggle.title = 'Switch to dark mode';
      if (logo) logo.src = '/icons/light/64.png';
    } else {
      document.documentElement.removeAttribute('data-theme');
      toggle.textContent = '🌙';
      toggle.title = 'Switch to light mode';
      if (logo) logo.src = '/icons/dark/64.png';
    }
    localStorage.setItem('vai-theme', theme);
  }

  applyTheme(current);
  toggle.addEventListener('click', () => {
    const next = current === 'dark' ? 'light' : 'dark';
    applyTheme(next);
    // Sync settings panel dropdown
    const themeSel = document.getElementById('settingsTheme');
    if (themeSel) themeSel.value = next;
  });
}

// ── State ──
let allModels = [];
let embedModels = [];
let rerankModels = [];
let lastEmbedding = null;

// ── Init ──
// ── Anonymous Telemetry ──
const TELEMETRY_URL = 'https://vaicli.com/api/telemetry';
const TELEMETRY_KEY = 'vai-telemetry-enabled';

function isTelemetryEnabled() {
  const stored = localStorage.getItem(TELEMETRY_KEY);
  return stored === null ? true : stored === 'true'; // default on
}

function uniqueTelemetryModels(values) {
  return Array.from(new Set(values.filter(v => typeof v === 'string' && v.trim())));
}

function inferTelemetryModelRole(event, primaryField, extra) {
  if (typeof extra.modelRole === 'string' && extra.modelRole.trim()) return extra.modelRole;
  if (primaryField === 'embeddingModel') return 'embedding';
  if (primaryField === 'rerankModel') return 'rerank';
  if (primaryField === 'llmModel') return 'llm';
  if (typeof event !== 'string') return undefined;
  if (event.indexOf('rerank') !== -1) return 'rerank';
  if (event.indexOf('embed') !== -1 || event.indexOf('search') !== -1 || event.indexOf('similarity') !== -1) return 'embedding';
  return undefined;
}

function normalizeTelemetryModels(event, extra) {
  const modelEntries = [
    ['model', extra.model],
    ['embeddingModel', extra.embeddingModel],
    ['rerankModel', extra.rerankModel],
    ['llmModel', extra.llmModel],
  ].filter(([, value]) => typeof value === 'string' && value.trim());

  if (modelEntries.length === 0) return {};

  const primaryField = modelEntries[0][0];
  const primaryModel = modelEntries[0][1];
  const models = uniqueTelemetryModels(modelEntries.map(([, value]) => value));
  const normalized = {
    models,
    modelCount: models.length,
  };

  if (!extra.model) normalized.model = primaryModel;

  const modelRole = inferTelemetryModelRole(event, primaryField, extra);
  if (!extra.modelRole && modelRole) normalized.modelRole = modelRole;

  if (extra.local === undefined && models.indexOf('voyage-4-nano') !== -1) {
    normalized.local = true;
  }

  return normalized;
}

function sendTelemetry(event, extra = {}) {
  if (!isTelemetryEnabled()) return;
  try {
    const isElectron = !!(window.vai && window.vai.isElectron);
    const normalizedModels = normalizeTelemetryModels(event, extra);
    const payload = {
      event,
      version: document.getElementById('appVersionLabel')?.textContent?.replace('v','') || 'web',
      context: isElectron ? 'electron' : 'web',
      platform: navigator.platform || undefined,
      locale: navigator.language || undefined,
      ...extra,
      ...normalizedModels,
    };
    // Use sendBeacon for fire-and-forget (survives page unload)
    if (navigator.sendBeacon) {
      navigator.sendBeacon(TELEMETRY_URL, JSON.stringify(payload));
    } else {
      fetch(TELEMETRY_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        keepalive: true,
      }).catch(() => {});
    }
  } catch { /* telemetry should never break the app */ }
}

function telemetryTimer(event, baseFields = {}) {
  const start = performance.now();
  return (extra = {}) => {
    sendTelemetry(event, {
      ...baseFields,
      ...extra,
      durationMs: Math.round(performance.now() - start),
    });
  };
}

function initTelemetryToggle() {
  const toggle = document.getElementById('settingsTelemetry');
  if (!toggle) return;
  const enabled = isTelemetryEnabled();
  toggle.classList.toggle('active', enabled);
  toggle.addEventListener('click', () => {
    const nowEnabled = !toggle.classList.contains('active');
    toggle.classList.toggle('active', nowEnabled);
    localStorage.setItem(TELEMETRY_KEY, String(nowEnabled));
    showSettingsSaved();
  });
}

async function init() {
  // Mark body for Electron vs Web styling
  if (window.vai && window.vai.isElectron) {
    document.body.classList.add('is-electron');
  }
  setupTabs();
  await loadConfig();
  await Promise.all([loadModels(), loadConcepts()]);
  populateModelSelects();
  enhanceModelSelects();
  buildExploreCards();
  renderExploreRobotGuides();

  // Apply default tab setting
  const settings = JSON.parse(localStorage.getItem('vai-settings') || '{}');
  const defaultTab = settings.defaultTab || 'home';
  switchTab(defaultTab);

  // Telemetry
  initTelemetryToggle();
  sendTelemetry('app_launch');
  sendTelemetry('playground_open');
}

// ── Tabs ──
const NAV_SECTION_CONFIG = {
  build: {
    label: 'Build',
    title: 'Create and run core workflows',
    defaultTab: 'embed'
  },
  evaluate: {
    label: 'Evaluate',
    title: 'Inspect, compare, and tune results',
    defaultTab: 'compare'
  },
  reference: {
    label: 'Reference',
    title: 'Browse models and product context',
    defaultTab: 'models'
  },
  settings: {
    label: 'Settings',
    title: 'Configure the app and environment',
    defaultTab: 'settings'
  }
};

const NAV_TAB_TO_SECTION = {
  embed: 'build',
  search: 'build',
  multimodal: 'build',
  generate: 'build',
  chat: 'build',
  workflows: 'build',
  compare: 'evaluate',
  benchmark: 'evaluate',
  optimize: 'evaluate',
  explore: 'evaluate',
  nano: 'evaluate',
  models: 'reference',
  about: 'reference',
  settings: 'settings'
};

let activeNavSection = 'build';
const navLastTabBySection = {
  build: 'embed',
  evaluate: 'compare',
  reference: 'models',
  settings: 'settings'
};

function setActiveNavSection(section) {
  const meta = NAV_SECTION_CONFIG[section];
  if (!meta) return;
  activeNavSection = section;

  document.querySelectorAll('.sidebar-rail-btn').forEach(btn => {
    const isActive = btn.dataset.section === section;
    btn.classList.toggle('active', isActive);
    btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
  });

  document.querySelectorAll('.sidebar-nav-group').forEach(group => {
    const isActive = group.dataset.section === section;
    group.classList.toggle('active', isActive);
    group.hidden = !isActive;
  });

  const eyebrow = document.getElementById('sidebarPanelEyebrow');
  const title = document.getElementById('sidebarPanelTitle');
  if (eyebrow) eyebrow.textContent = meta.label;
  if (title) title.textContent = meta.title;
}

function syncNavSectionForTab(tab) {
  const section = NAV_TAB_TO_SECTION[tab];
  if (section) {
    navLastTabBySection[section] = tab;
    setActiveNavSection(section);
    return;
  }

  setActiveNavSection(activeNavSection || 'build');
}

function navigateToNavSection(section) {
  const meta = NAV_SECTION_CONFIG[section];
  if (!meta) return;
  setActiveNavSection(section);
  switchTab(navLastTabBySection[section] || meta.defaultTab);
}

function getActiveTabId() {
  return document.querySelector('.tab-btn.active')?.dataset?.tab || 'home';
}

function getActiveTabLabel() {
  if (getActiveTabId() === 'home') return 'Overview';
  return document.querySelector('.tab-btn.active span:last-child')?.textContent?.trim() || 'Overview';
}

function setupTabs() {
  const tabBtns = document.querySelectorAll('.tab-btn');
  
  tabBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      switchTab(btn.dataset.tab);
    });
    
    // Keyboard navigation for accessibility
    btn.addEventListener('keydown', (e) => {
      const visibleTabList = Array.from(btn.closest('.sidebar-nav-group')?.querySelectorAll('.tab-btn') || []);
      const currentIndex = visibleTabList.indexOf(btn);
      let nextIndex = -1;
      
      if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
        e.preventDefault();
        nextIndex = (currentIndex + 1) % visibleTabList.length;
      } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
        e.preventDefault();
        nextIndex = (currentIndex - 1 + visibleTabList.length) % visibleTabList.length;
      } else if (e.key === 'Home') {
        e.preventDefault();
        nextIndex = 0;
      } else if (e.key === 'End') {
        e.preventDefault();
        nextIndex = visibleTabList.length - 1;
      }
      
      if (nextIndex >= 0) {
        visibleTabList[nextIndex].focus();
        switchTab(visibleTabList[nextIndex].dataset.tab);
      }
    });
  });

  const homeBtn = document.getElementById('sidebarHomeBtn');
  if (homeBtn) {
    homeBtn.addEventListener('click', () => {
      switchTab('home');
    });
  }

  const railBtns = Array.from(document.querySelectorAll('.sidebar-rail-btn'));
  railBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      navigateToNavSection(btn.dataset.section);
    });

    btn.addEventListener('keydown', (e) => {
      const currentIndex = railBtns.indexOf(btn);
      let nextIndex = -1;

      if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
        e.preventDefault();
        nextIndex = (currentIndex + 1) % railBtns.length;
      } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
        e.preventDefault();
        nextIndex = (currentIndex - 1 + railBtns.length) % railBtns.length;
      } else if (e.key === 'Home') {
        e.preventDefault();
        nextIndex = 0;
      } else if (e.key === 'End') {
        e.preventDefault();
        nextIndex = railBtns.length - 1;
      }

      if (nextIndex >= 0) {
        railBtns[nextIndex].focus();
      }
    });
  });

  setActiveNavSection(activeNavSection);
}

function switchTab(tab) {
  if (!document.getElementById('tab-' + tab)) {
    tab = 'home';
  }

  if (tab !== 'home') {
    localStorage.setItem('vai-last-active-tab', tab);
  }
  document.querySelectorAll('.tab-btn').forEach(b => {
    const isActive = b.dataset.tab === tab;
    b.classList.toggle('active', isActive);
    // Update ARIA attributes for accessibility
    b.setAttribute('aria-selected', isActive ? 'true' : 'false');
    b.setAttribute('tabindex', isActive ? '0' : '-1');
  });
  document.querySelectorAll('.tab-panel').forEach(p => {
    const isActive = p.id === 'tab-' + tab;
    p.classList.toggle('active', isActive);
    // Hide inactive panels from screen readers
    p.setAttribute('aria-hidden', isActive ? 'false' : 'true');
  });
  syncNavSectionForTab(tab);
  // Track tab views
  sendTelemetry('tab_view', { tab });
  sendTelemetry('playground_tab', { tab });
  
  // Initialize Home page if switching to it
  if (tab === 'home') {
    homeInit();
    renderHomeRecentActivity();
  }
  if (tab === 'models') {
    modelsInit();
  }
  if (tab === 'nano') {
    nanoInit();
  }
}
// Expose globally so Electron main process can call it
window.switchTab = switchTab;

// ── Local Inference (Nano) Tab ──

let nanoStatusCache = null;

async function nanoInit() {
  // Always re-fetch status on tab switch (no caching - status may change)
  const content = document.getElementById('nanoStatusContent');
  const banner = document.getElementById('nanoSetupBanner');
  const embedSection = document.getElementById('nanoEmbedSection');
  const simSection = document.getElementById('nanoSimilaritySection');
  const dimSection = document.getElementById('nanoDimensionSection');
  const xbridgeSection = document.getElementById('nanoCrossbridgeSection');

  content.innerHTML = '<div class="nano-status-loading">Checking nano bridge status...</div>';
  banner.style.display = 'none';

  try {
    const res = await fetch('/api/nano/status');
    const data = await res.json();
    nanoStatusCache = data;

    // Render status grid
    const components = data.components;
    const items = [
      { key: 'python', label: 'Python', detail: components.python.version || components.python.hint || '' },
      { key: 'venv', label: 'Virtual Env', detail: components.venv.ok ? 'Installed' : (components.venv.hint || 'Missing') },
      { key: 'model', label: 'Model', detail: components.model.ok ? 'Downloaded' : (components.model.hint || 'Missing') },
      { key: 'bridge', label: 'Bridge', detail: components.bridge.ok ? 'Ready' : (components.bridge.hint || 'Not available') },
    ];

    content.innerHTML = '<div class="nano-status-grid">' + items.map(function(item) {
      var ok = components[item.key].ok;
      return '<div class="nano-status-item ' + (ok ? 'ok' : 'fail') + '">' +
        '<span class="nano-status-dot"></span>' +
        '<span class="nano-status-label">' + item.label + '</span>' +
        '<span class="nano-status-detail">' + item.detail + '</span>' +
      '</div>';
    }).join('') + '</div>';

    if (data.ready) {
      // Show ready banner, enable embed section
      banner.style.display = 'none';
      var readyEl = document.getElementById('nanoReadyBanner');
      if (readyEl) readyEl.style.display = 'flex';
      if (embedSection) embedSection.style.display = 'block';
      if (simSection) simSection.style.display = 'block';
      if (dimSection) dimSection.style.display = 'block';
      if (xbridgeSection) xbridgeSection.style.display = 'block';
    } else {
      // Show setup required banner, disable embed section
      banner.style.display = 'flex';
      if (embedSection) embedSection.style.display = 'none';
      if (simSection) simSection.style.display = 'none';
      if (dimSection) dimSection.style.display = 'none';
      if (xbridgeSection) xbridgeSection.style.display = 'none';
    }
  } catch (err) {
    content.innerHTML = '<div class="nano-status-loading" style="color:var(--error,#ff6b6b)">Failed to check nano status: ' + err.message + '</div>';
    banner.style.display = 'flex';
    if (embedSection) embedSection.style.display = 'none';
    if (simSection) simSection.style.display = 'none';
    if (dimSection) dimSection.style.display = 'none';
    if (xbridgeSection) xbridgeSection.style.display = 'none';
  }
}

let lastNanoEmbedding = null;

window.nanoDoEmbed = async function() {
  hideError('nanoEmbedError');
  const text = document.getElementById('nanoEmbedInput').value.trim();
  if (!text) { showError('nanoEmbedError', 'Enter some text to embed'); return; }

  const dimension = parseInt(document.getElementById('nanoEmbedDimension').value, 10);
  const quantization = document.getElementById('nanoEmbedQuantization').value;

  setLoading('nanoEmbedBtn', true);
  try {
    const res = await fetch('/api/nano/embed', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text, dimension, quantization }),
    });
    const data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || 'Embed failed');
    }

    lastNanoEmbedding = data.embedding;

    // Stats
    const statsEl = document.getElementById('nanoEmbedStats');
    statsEl.innerHTML =
      '<span class="stat"><span class="stat-label">Model</span><span class="stat-value">' + data.model + '</span></span>' +
      '<span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">' + data.dimension + '</span></span>' +
      '<span class="stat"><span class="stat-label">Quantization</span><span class="stat-value">' + data.quantization + '</span></span>' +
      '<span class="stat"><span class="stat-label">Latency</span><span class="stat-value">' + data.latency_ms + 'ms</span></span>';

    // Vector preview
    var preview = data.embedding.slice(0, 20).map(function(v) { return typeof v === 'number' ? v.toFixed(6) : v; }).join(', ');
    document.getElementById('nanoEmbedVector').textContent = '[' + preview + ', ... ] (' + data.embedding.length + ' values)';

    // Heatmap (reuse existing buildHeatmap if available)
    if (typeof buildHeatmap === 'function') {
      buildHeatmap(data.embedding, document.getElementById('nanoEmbedHeatmap'));
    }

    document.getElementById('nanoEmbedResult').classList.add('visible');
  } catch (err) {
    showError('nanoEmbedError', err.message);
  } finally {
    setLoading('nanoEmbedBtn', false);
  }
};

window.nanoCopyVector = function() {
  if (!lastNanoEmbedding) return;
  navigator.clipboard.writeText(JSON.stringify(lastNanoEmbedding))
    .then(function() {
      var btn = document.querySelector('#nanoEmbedResult .btn-secondary');
      if (btn) {
        var orig = btn.innerHTML;
        btn.textContent = 'Copied!';
        setTimeout(function() { btn.innerHTML = orig; }, 1500);
      }
    });
};

window.nanoDoSimilarity = async function() {
  hideError('nanoSimError');
  var rawText = document.getElementById('nanoSimTexts').value;
  var lines = rawText.split('\n');
  var texts = [];
  for (var i = 0; i < lines.length; i++) {
    var trimmed = lines[i].trim();
    if (trimmed.length > 0) {
      texts.push(trimmed);
    }
  }

  if (texts.length < 2 || texts.length > 10) {
    showError('nanoSimError', 'Please enter between 2 and 10 texts (one per line). Currently: ' + texts.length);
    return;
  }

  var dimension = parseInt(document.getElementById('nanoSimDimension').value, 10);
  setLoading('nanoSimBtn', true);

  try {
    var res = await fetch('/api/nano/similarity', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ texts: texts, dimension: dimension }),
    });
    var data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || 'Similarity comparison failed');
    }

    var matrix = data.matrix;
    var n = texts.length;

    // Stats
    var statsEl = document.getElementById('nanoSimStats');
    statsEl.innerHTML =
      '<span class="stat"><span class="stat-label">Texts</span><span class="stat-value">' + n + '</span></span>' +
      '<span class="stat"><span class="stat-label">Dimension</span><span class="stat-value">' + data.dimension + '</span></span>' +
      '<span class="stat"><span class="stat-label">Latency</span><span class="stat-value">' + data.latency_ms + 'ms</span></span>';

    // Build NxN heatmap grid
    var gridCols = n + 1;
    var heatmapHtml = '<div class="nano-sim-heatmap" style="grid-template-columns: repeat(' + gridCols + ', 1fr);">';

    // Corner cell
    heatmapHtml += '<div class="nano-sim-corner"></div>';

    // Column header labels
    for (var c = 0; c < n; c++) {
      heatmapHtml += '<div class="nano-sim-label" title="' + texts[c].replace(/"/g, '&quot;') + '">' + texts[c].substring(0, 12) + '</div>';
    }

    // Rows
    for (var r = 0; r < n; r++) {
      // Row label
      heatmapHtml += '<div class="nano-sim-label" title="' + texts[r].replace(/"/g, '&quot;') + '">' + texts[r].substring(0, 12) + '</div>';
      // Cells
      for (var ci = 0; ci < n; ci++) {
        var score = matrix[r][ci];
        var hue = Math.round(score * 120);
        var bg = 'hsl(' + hue + ', 70%, 35%)';
        heatmapHtml += '<div class="nano-sim-cell" style="background:' + bg + ';" title="' + texts[r].substring(0, 20) + ' vs ' + texts[ci].substring(0, 20) + '">' + score.toFixed(3) + '</div>';
      }
    }
    heatmapHtml += '</div>';
    document.getElementById('nanoSimHeatmap').innerHTML = heatmapHtml;

    // Find highest and lowest off-diagonal pairs
    var highScore = -Infinity;
    var lowScore = Infinity;
    var highPair = [0, 1];
    var lowPair = [0, 1];
    for (var ri = 0; ri < n; ri++) {
      for (var cj = ri + 1; cj < n; cj++) {
        if (matrix[ri][cj] > highScore) {
          highScore = matrix[ri][cj];
          highPair = [ri, cj];
        }
        if (matrix[ri][cj] < lowScore) {
          lowScore = matrix[ri][cj];
          lowPair = [ri, cj];
        }
      }
    }

    var highlightsHtml = '<div class="nano-sim-highlights">';

    // Highest pair card
    highlightsHtml += '<div class="nano-sim-highlight-card">' +
      '<div class="highlight-title">Highest Similarity</div>' +
      '<div class="highlight-score" style="color:var(--success);">' + highScore.toFixed(4) + '</div>' +
      '<div class="highlight-texts">' +
        '<span title="' + texts[highPair[0]].replace(/"/g, '&quot;') + '">' + texts[highPair[0]].substring(0, 50) + '</span>' +
        '<span title="' + texts[highPair[1]].replace(/"/g, '&quot;') + '">' + texts[highPair[1]].substring(0, 50) + '</span>' +
      '</div>' +
    '</div>';

    // Lowest pair card
    highlightsHtml += '<div class="nano-sim-highlight-card lowest">' +
      '<div class="highlight-title">Lowest Similarity</div>' +
      '<div class="highlight-score" style="color:var(--error, #ff6b6b);">' + lowScore.toFixed(4) + '</div>' +
      '<div class="highlight-texts">' +
        '<span title="' + texts[lowPair[0]].replace(/"/g, '&quot;') + '">' + texts[lowPair[0]].substring(0, 50) + '</span>' +
        '<span title="' + texts[lowPair[1]].replace(/"/g, '&quot;') + '">' + texts[lowPair[1]].substring(0, 50) + '</span>' +
      '</div>' +
    '</div>';

    highlightsHtml += '</div>';
    document.getElementById('nanoSimHighlights').innerHTML = highlightsHtml;

    document.getElementById('nanoSimResult').classList.add('visible');
  } catch (err) {
    showError('nanoSimError', err.message);
  } finally {
    setLoading('nanoSimBtn', false);
  }
};

function nanoCosineSim(a, b) {
  var len = Math.min(a.length, b.length);
  var dot = 0;
  var normA = 0;
  var normB = 0;
  for (var i = 0; i < len; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  normA = Math.sqrt(normA);
  normB = Math.sqrt(normB);
  if (normA === 0 || normB === 0) return 0;
  return dot / (normA * normB);
}

window.nanoDoDimensions = async function() {
  hideError('nanoDimError');
  var text = document.getElementById('nanoDimText').value.trim();
  if (!text) {
    showError('nanoDimError', 'Enter some text to compare across dimensions');
    return;
  }

  setLoading('nanoDimBtn', true);

  try {
    var res = await fetch('/api/nano/dimensions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: text }),
    });
    var data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || 'Dimension comparison failed');
    }

    // Stats
    var statsEl = document.getElementById('nanoDimStats');
    statsEl.innerHTML =
      '<span class="stat"><span class="stat-label">Latency</span><span class="stat-value">' + data.latency_ms + 'ms</span></span>';

    var dims = ['256', '512', '1024', '2048'];
    var baseline = data.dimensions['2048'].embedding;
    var cardsHtml = '<div class="nano-dim-grid">';

    for (var di = 0; di < dims.length; di++) {
      var dim = dims[di];
      var info = data.dimensions[dim];
      var isBaseline = dim === '2048';

      cardsHtml += '<div class="nano-dim-card">';
      cardsHtml += '<div class="nano-dim-card-title">' + dim + ' dimensions';
      if (isBaseline) {
        cardsHtml += '<span class="nano-dim-baseline-tag">Baseline</span>';
      }
      cardsHtml += '</div>';

      // Stat rows
      cardsHtml += '<div class="nano-dim-stat-row"><span class="nano-dim-stat-label">Norm</span><span class="nano-dim-stat-value">' + info.norm.toFixed(4) + '</span></div>';
      cardsHtml += '<div class="nano-dim-stat-row"><span class="nano-dim-stat-label">Sparsity</span><span class="nano-dim-stat-value">' + (info.sparsity * 100).toFixed(1) + '%</span></div>';
      cardsHtml += '<div class="nano-dim-stat-row"><span class="nano-dim-stat-label">Vector Length</span><span class="nano-dim-stat-value">' + dim + '</span></div>';

      // Similarity preservation
      if (isBaseline) {
        cardsHtml += '<div class="nano-dim-preservation high">1.0000 (baseline)</div>';
      } else {
        var sim = nanoCosineSim(info.embedding, baseline);
        var cls = sim >= 0.99 ? 'high' : (sim >= 0.95 ? 'mid' : 'low');
        cardsHtml += '<div class="nano-dim-preservation ' + cls + '">' + sim.toFixed(4) + '</div>';
      }

      cardsHtml += '</div>';
    }

    cardsHtml += '</div>';
    document.getElementById('nanoDimCards').innerHTML = cardsHtml;

    document.getElementById('nanoDimResult').classList.add('visible');
  } catch (err) {
    showError('nanoDimError', err.message);
  } finally {
    setLoading('nanoDimBtn', false);
  }
};

// ── Cross-Bridge Comparison ──

window.nanoDoCrossbridge = async function() {
  hideError('nanoXbridgeError');
  var text = document.getElementById('nanoXbridgeText').value.trim();
  if (!text) {
    showError('nanoXbridgeError', 'Enter some text to compare across bridges');
    return;
  }

  var dimension = parseInt(document.getElementById('nanoXbridgeDimension').value, 10);
  setLoading('nanoXbridgeBtn', true);

  try {
    var res = await fetch('/api/nano/crossbridge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: text, dimension: dimension }),
    });
    var data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || 'Cross-bridge comparison failed');
    }

    var noKeyEl = document.getElementById('nanoXbridgeNoKey');
    var contentEl = document.getElementById('nanoXbridgeContent');

    if (data.apiError) {
      // No API key or API error - show message
      noKeyEl.style.display = 'block';
      noKeyEl.innerHTML = '&#128273; ' + data.apiError + '. Add your Voyage AI API key in Settings to enable cross-bridge comparison.';
      contentEl.innerHTML = '';

      // Still show nano result
      var nanoOnlyHtml = '<div class="nano-xbridge-cards">';
      nanoOnlyHtml += '<div class="nano-xbridge-card">';
      nanoOnlyHtml += '<div class="xbridge-source">Nano (Local)</div>';
      nanoOnlyHtml += '<div class="xbridge-model">' + data.nano.model + '</div>';
      nanoOnlyHtml += '<div class="xbridge-latency">' + data.nano.latency_ms + 'ms</div>';
      var nanoPreview = data.nano.embedding.slice(0, 8).map(function(v) { return v.toFixed(6); }).join(', ');
      nanoOnlyHtml += '<div class="xbridge-vector-preview">[' + nanoPreview + ', ...]</div>';
      nanoOnlyHtml += '</div>';
      nanoOnlyHtml += '</div>';
      contentEl.innerHTML = nanoOnlyHtml;
    } else {
      // Both nano and API results available
      noKeyEl.style.display = 'none';

      var html = '';

      // Similarity score
      var score = data.similarity;
      var cls = 'low';
      if (score >= 0.99) cls = 'perfect';
      else if (score >= 0.95) cls = 'high';
      else if (score >= 0.90) cls = 'mid';

      html += '<div class="nano-xbridge-similarity ' + cls + '">';
      html += '<span class="score">' + score.toFixed(6) + '</span>';
      html += '<span class="label">Cosine Similarity</span>';
      html += '</div>';

      // Two-card grid
      html += '<div class="nano-xbridge-cards">';

      // Nano card
      html += '<div class="nano-xbridge-card">';
      html += '<div class="xbridge-source">Nano (Local)</div>';
      html += '<div class="xbridge-model">' + data.nano.model + '</div>';
      html += '<div class="xbridge-latency">' + data.nano.latency_ms + 'ms</div>';
      var nPrev = data.nano.embedding.slice(0, 8).map(function(v) { return v.toFixed(6); }).join(', ');
      html += '<div class="xbridge-vector-preview">[' + nPrev + ', ...]</div>';
      html += '</div>';

      // API card
      html += '<div class="nano-xbridge-card">';
      html += '<div class="xbridge-source">API (Cloud)</div>';
      html += '<div class="xbridge-model">' + data.api.model + '</div>';
      html += '<div class="xbridge-latency">' + data.api.latency_ms + 'ms</div>';
      var aPrev = data.api.embedding.slice(0, 8).map(function(v) { return v.toFixed(6); }).join(', ');
      html += '<div class="xbridge-vector-preview">[' + aPrev + ', ...]</div>';
      html += '</div>';

      html += '</div>';

      // Interpretation
      var interp = '';
      if (score >= 0.99) interp = 'Vectors are nearly identical - nano faithfully reproduces API embeddings';
      else if (score >= 0.95) interp = 'Vectors are highly similar - nano embeddings are interoperable with API';
      else if (score >= 0.90) interp = 'Vectors share the same space with minor differences';
      else interp = 'Significant differences detected';

      html += '<div class="nano-xbridge-interpretation">' + interp + '</div>';

      // Vector alignment visualization
      var nanoEmb = data.nano.embedding;
      var apiEmb = data.api.embedding;
      var dimCount = nanoEmb.length;
      var step = Math.max(1, Math.floor(dimCount / 50));
      var sampledNano = [];
      var sampledApi = [];
      var sampledIdx = [];
      for (var si = 0; si < dimCount; si += step) {
        sampledNano.push(nanoEmb[si]);
        sampledApi.push(apiEmb[si]);
        sampledIdx.push(si);
      }
      var allSampled = sampledNano.concat(sampledApi);
      var vizMin = Math.min.apply(null, allSampled);
      var vizMax = Math.max.apply(null, allSampled);
      var vizRange = vizMax - vizMin || 1;
      var totalBars = sampledNano.length;
      var barWidth = (100 / totalBars);

      html += '<div class="nano-xbridge-viz">';
      html += '<div class="nano-xbridge-viz-title">Vector Alignment (sampled dimensions)</div>';
      html += '<div class="nano-xbridge-viz-chart">';
      for (var bi = 0; bi < totalBars; bi++) {
        var nanoH = ((sampledNano[bi] - vizMin) / vizRange) * 100;
        var apiH = ((sampledApi[bi] - vizMin) / vizRange) * 100;
        var leftPos = (bi / totalBars) * 100;
        var halfBar = barWidth * 0.45;
        html += '<div class="nano-xbridge-viz-bar nano-bar" style="left:' + leftPos + '%;width:' + halfBar + '%;height:' + nanoH + '%"></div>';
        html += '<div class="nano-xbridge-viz-bar api-bar" style="left:' + (leftPos + halfBar) + '%;width:' + halfBar + '%;height:' + apiH + '%"></div>';
      }
      html += '</div>';

      // Legend
      html += '<div class="nano-xbridge-viz-legend">';
      html += '<div class="nano-xbridge-viz-legend-item"><span class="nano-xbridge-viz-legend-dot" style="background:var(--accent)"></span> Nano (local)</div>';
      html += '<div class="nano-xbridge-viz-legend-item"><span class="nano-xbridge-viz-legend-dot" style="background:var(--purple)"></span> API (cloud)</div>';
      html += '</div>';

      // Diff statistics
      var diffs = [];
      for (var di = 0; di < dimCount; di++) {
        diffs.push(Math.abs(nanoEmb[di] - apiEmb[di]));
      }
      var maxDiff = Math.max.apply(null, diffs);
      var avgDiff = 0;
      for (var ai = 0; ai < diffs.length; ai++) {
        avgDiff += diffs[ai];
      }
      avgDiff = avgDiff / diffs.length;
      var matchingDims = 0;
      for (var mi = 0; mi < diffs.length; mi++) {
        if (diffs[mi] < 0.01) matchingDims++;
      }
      var matchPct = ((matchingDims / dimCount) * 100).toFixed(1);

      html += '<div class="nano-xbridge-diff-stats">';
      html += '<div class="nano-xbridge-diff-stat"><div class="stat-value">' + maxDiff.toFixed(6) + '</div><div class="stat-label">Max Diff</div></div>';
      html += '<div class="nano-xbridge-diff-stat"><div class="stat-value">' + avgDiff.toFixed(6) + '</div><div class="stat-label">Avg Diff</div></div>';
      html += '<div class="nano-xbridge-diff-stat"><div class="stat-value">' + matchingDims + '/' + dimCount + ' (' + matchPct + '%)</div><div class="stat-label">Matching Dims</div></div>';
      html += '</div>';
      html += '</div>';

      contentEl.innerHTML = html;
    }

    document.getElementById('nanoXbridgeResult').classList.add('visible');
  } catch (err) {
    showError('nanoXbridgeError', err.message);
  } finally {
    setLoading('nanoXbridgeBtn', false);
  }
};

// ── Home Page ──

let homeData = {
  announcements: null,
  releases: null,
  featuredWorkflows: null,
  communityWorkflows: null,
  currentAnnouncementIndex: 0,
  sortMode: 'trending',
  hasApiKey: false
};

async function homeInit() {
  try {
    await updateHomeVersionInfo();
    await checkApiKeyStatus();
    renderHomeStatusGrid();
    renderHomeRecentActivity();
    renderHomeUpdate();
    renderHomeWorkflowSpotlight();

    const footerLattice = document.getElementById('footerLattice');
    if (footerLattice && !footerLattice.innerHTML) {
      footerLattice.innerHTML = generateLatticeSVG(footerLattice.parentElement.offsetWidth || 900, 80, 780, {variant: 'dense', opacity: 0.05});
    }

    if (homeData.releases === null) {
      await loadReleases();
      renderHomeUpdate();
    }

    if (homeData.featuredWorkflows === null) {
      loadMarketplaceData().then(() => {
        renderHomeWorkflowSpotlight();
      }).catch(() => {
        renderHomeWorkflowSpotlight();
      });
    }
  } catch (err) {
    console.error('Failed to initialize Home page:', err);
  }
}

async function updateHomeVersionInfo() {
  let version = window.appVersion;

  // If version not set yet, try to fetch from Electron
  if (!version && window.vai && window.vai.getVersion) {
    try {
      const v = await window.vai.getVersion();
      if (v) {
        version = typeof v === 'object' ? v.app : v;
        window.appVersion = version;
      }
    } catch (e) {
      console.warn('Failed to get version:', e);
    }
  }

  // Fallback for web mode: try to get from CLI package.json via API
  if (!version) {
    try {
      const res = await fetch('/api/version');
      if (res.ok) {
        const data = await res.json();
        version = data.version || data.app;
        if (version) window.appVersion = version;
      }
    } catch (e) {
      // Ignore fetch errors in web mode
    }
  }

  version = version || 'dev';
  document.getElementById('homeVersionBadge').textContent = `v${version}`;
  const footerEl = document.getElementById('homeFooterVersion');
  if (footerEl) footerEl.textContent = `Version ${version} • © 2026 VAI`;
}

async function checkApiKeyStatus() {
  const warningEl = document.getElementById('homeApiWarning');
  if (warningEl) warningEl.style.display = 'none';

  try {
    const res = await fetch('/api/config');
    const data = await res.json();
    homeData.hasApiKey = Boolean(data.hasKey);
  } catch {
    homeData.hasApiKey = false;
  }

  renderHomeStatusGrid();
  return homeData.hasApiKey;
}

function getHomeLastActiveTab() {
  const lastTab = localStorage.getItem('vai-last-active-tab');
  return lastTab && lastTab !== 'home' ? lastTab : 'embed';
}

function formatHomeTabLabel(tab) {
  const labels = {
    embed: 'Embed',
    compare: 'Compare',
    search: 'Rerank',
    workflows: 'Workflows',
    benchmark: 'Benchmark',
    multimodal: 'Multimodal',
    generate: 'Generate',
    chat: 'Chat',
    explore: 'Explore',
    optimize: 'Optimize',
    nano: 'Local Inference',
    models: 'Models',
    settings: 'Settings',
    about: 'About'
  };
  return labels[tab] || (tab ? tab.charAt(0).toUpperCase() + tab.slice(1) : 'Embed');
}

function formatHomeRelativeTime(timestamp) {
  if (!timestamp) return 'Just now';
  const minutes = Math.max(1, Math.round((Date.now() - timestamp) / 60000));
  if (minutes < 60) return `${minutes} min ago`;
  const hours = Math.round(minutes / 60);
  if (hours < 24) return `${hours} hr ago`;
  const days = Math.round(hours / 24);
  return `${days} day${days === 1 ? '' : 's'} ago`;
}

function resumeLastHomeAction() {
  switchTab(getHomeLastActiveTab());
}

function renderHomeStatusGrid() {
  const container = document.getElementById('homeStatusGrid');
  if (!container) return;

  const lastTab = getHomeLastActiveTab();
  const latestRelease = Array.isArray(homeData.releases) && homeData.releases.length ? homeData.releases[0] : null;
  const version = window.appVersion || 'dev';

  const cards = [
    {
      label: 'API key',
      value: homeData.hasApiKey ? 'Connected' : 'Needs setup',
      description: homeData.hasApiKey
        ? 'Your Voyage AI key is configured and ready for requests.'
        : 'Add your API key to start embedding, reranking, and workflows.',
      statusClass: homeData.hasApiKey ? 'is-ok' : 'is-warning',
      actionLabel: homeData.hasApiKey ? 'View settings' : 'Add key',
      action: "switchTab('settings')"
    },
    {
      label: 'Next action',
      value: `Resume ${formatHomeTabLabel(lastTab)}`,
      description: 'Jump back into the tool you used most recently in this desktop session.',
      statusClass: 'is-ok',
      actionLabel: 'Resume',
      action: 'resumeLastHomeAction()'
    },
    {
      label: 'App version',
      value: `v${version}`,
      description: latestRelease
        ? `Latest release loaded: ${latestRelease.version}`
        : 'Checking the latest release notes for this build.',
      statusClass: 'is-ok',
      actionLabel: 'View updates',
      action: "document.getElementById('homeUpdateContent')?.scrollIntoView({ behavior: 'smooth', block: 'start' })"
    },
    {
      label: 'Command palette',
      value: 'Cmd+K',
      description: 'Search commands, recent actions, and workflow shortcuts from anywhere in the app.',
      statusClass: 'is-ok',
      actionLabel: 'Open palette',
      action: 'toggleCommandPalette()'
    }
  ];

  container.innerHTML = cards.map(card => `
    <div class="home-status-card ${card.statusClass}">
      <div class="home-status-label">${card.label}</div>
      <div class="home-status-value">${card.value}</div>
      <div class="home-status-description">${card.description}</div>
      <button class="home-link-button home-status-action" onclick="${card.action}">${card.actionLabel}</button>
    </div>
  `).join('');
}

function renderHomeRecentActivity() {
  const container = document.getElementById('homeRecentActivity');
  if (!container) return;

  const lastTab = getHomeLastActiveTab();
  const trackerItems = typeof recentItemsTracker !== 'undefined' && Array.isArray(recentItemsTracker.items)
    ? recentItemsTracker.items
    : [];
  const recentItems = trackerItems
    .filter(item => item.id !== 'nav-home')
    .slice(0, 3);

  const recentMarkup = recentItems.length ? `
    <div class="home-recent-list">
      ${recentItems.map(item => `
        <div class="home-recent-item">
          <div class="home-recent-item-main">
            <div class="home-recent-item-title">${escapeHtml(item.action || 'Recent activity')}</div>
            <div class="home-recent-item-meta">${escapeHtml(formatHomeRelativeTime(item.timestamp))}</div>
          </div>
        </div>
      `).join('')}
    </div>
  ` : `
    <div class="home-empty-state">Use a few tools and this section will keep your most recent in-app activity visible.</div>
  `;

  container.innerHTML = `
    <div class="home-resume-main">
      <div>
        <span class="home-resume-label">Last active tool</span>
        <span class="home-resume-title">${escapeHtml(formatHomeTabLabel(lastTab))}</span>
        <div class="home-resume-description">Return to the last workspace you opened instead of hunting through the sidebar.</div>
      </div>
      <button class="btn" onclick="resumeLastHomeAction()">Resume</button>
    </div>
    ${recentMarkup}
  `;
}

function renderHomeUpdate() {
  const container = document.getElementById('homeUpdateContent');
  if (!container) return;

  if (homeData.releases === null) {
    container.innerHTML = '<div class="home-empty-state">Loading the latest release notes...</div>';
    return;
  }

  const release = homeData.releases[0];
  if (!release) {
    container.innerHTML = '<div class="home-empty-state">Release notes are not available right now. You can still open the full releases page from GitHub.</div>';
    return;
  }

  const highlights = Array.isArray(release.highlights) ? release.highlights.slice(0, 3) : [];
  container.innerHTML = `
    <div class="home-update-card">
      <div class="home-update-kicker">Latest release</div>
      <h4 class="home-update-title">${escapeHtml(release.version || 'Current release')}</h4>
      <div class="home-update-date">${escapeHtml(new Date(release.date).toLocaleDateString())}</div>
      <div class="home-update-summary">A focused snapshot of what changed most recently, so returning users can orient quickly without wading through a feed.</div>
      ${highlights.length ? `
        <ul class="home-update-highlights">
          ${highlights.map(highlight => `<li>${escapeHtml(highlight)}</li>`).join('')}
        </ul>
      ` : ''}
    </div>
  `;
}

function renderHomeWorkflowSpotlight() {
  const container = document.getElementById('homeWorkflowSpotlight');
  if (!container) return;

  if (homeData.featuredWorkflows === null) {
    container.innerHTML = '<div class="home-empty-state">Loading featured workflows...</div>';
    return;
  }

  const workflows = (homeData.featuredWorkflows || []).slice(0, 2);
  if (!workflows.length) {
    container.innerHTML = '<div class="home-empty-state">Featured workflow suggestions are not available right now. Open the workflows tab to browse your library or install from the store.</div>';
    return;
  }

  container.innerHTML = workflows.map(workflow => `
    <div class="home-workflow-spotlight">
      <h4>${escapeHtml(workflow.name || 'Untitled workflow')}</h4>
      <p>${escapeHtml(workflow.description || 'No description available.')}</p>
      <div class="home-workflow-spotlight-meta">
        <span class="home-workflow-domain">${escapeHtml(workflow.category || 'utility')}</span>
        <span class="home-workflow-author">by ${escapeHtml(workflow.author?.name || 'unknown')}</span>
      </div>
      <div class="home-workflow-actions">
        <button class="home-workflow-btn" onclick="wfStoreInstall('${workflow.packageName}', this)">Install</button>
        <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
      </div>
    </div>
  `).join('');
}

async function loadAnnouncements() {
  try {
    const res = await fetch('/api/home/announcements');
    const data = await res.json();
    
    // Store total count before filtering
    homeData.totalAnnouncements = data.announcements.length;
    
    // Filter out dismissed announcements
    const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
    homeData.announcements = data.announcements.filter(a => !dismissed.includes(a.id));
    
  } catch (err) {
    console.error('Failed to load announcements:', err);
    homeData.announcements = [];
    homeData.totalAnnouncements = 0;
  }
}

async function loadReleases() {
  try {
    // Try cache first
    const cached = localStorage.getItem('vai-releases-cache');
    if (cached) {
      const { data, timestamp } = JSON.parse(cached);
      // Use cache if less than 30 minutes old AND it has real data (not fallback)
      const hasRealData = data && data.length > 0 && !data[0].version?.includes('1.0.0');
      if (hasRealData && Date.now() - timestamp < 30 * 60 * 1000) {
        homeData.releases = data;
        return;
      }
    }

    const res = await fetch('/api/home/releases');
    const data = await res.json();
    homeData.releases = data.releases;

    // Only cache if we got real data (not fallback)
    const hasRealData = data.releases && data.releases.length > 0 && !data.releases[0].version?.includes('1.0.0');
    if (hasRealData) {
      localStorage.setItem('vai-releases-cache', JSON.stringify({
        data: data.releases,
        timestamp: Date.now()
      }));
    } else {
      // Clear stale cache if we got fallback data
      localStorage.removeItem('vai-releases-cache');
    }

  } catch (err) {
    console.error('Failed to load releases:', err);
    homeData.releases = [];
  }
}

async function loadMarketplaceData() {
  try {
    const res = await fetch('/api/workflows/catalog');
    const data = await res.json();

    // Populate store catalog so detail modal and install work from home page
    if (data.workflows && data.workflows.length > 0) {
      _wfStoreCatalog = data.workflows;
    }
    if (data.icons) _wfStoreIcons = data.icons;

    // Extract featured and community workflows
    homeData.featuredWorkflows = data.workflows.filter(w => w.featured).slice(0, 4);
    homeData.communityWorkflows = data.workflows.filter(w => !w.featured);
    
  } catch (err) {
    console.error('Failed to load marketplace data:', err);
    homeData.featuredWorkflows = [];
    homeData.communityWorkflows = [];
  }
}

function renderAnnouncements() {
  const container = document.getElementById('homeAnnouncements');
  const carousel = document.getElementById('announcementsCarousel');
  const dots = document.getElementById('announcementsDots');
  const restoreBtn = document.getElementById('announcementsRestore');
  
  // Show restore button if any announcements have been dismissed
  const hasDismissed = (homeData.totalAnnouncements || 0) > homeData.announcements.length;
  if (restoreBtn) restoreBtn.style.display = hasDismissed ? 'block' : 'none';
  
  if (!homeData.announcements.length) {
    container.style.display = hasDismissed ? 'block' : 'none';
    carousel.innerHTML = '';
    dots.innerHTML = '';
    return;
  }
  
  // Render cards
  carousel.innerHTML = homeData.announcements.map((ann, i) => {
    const bgClasses = [
      'home-announcement-card',
      i === 0 ? 'active' : '',
      ann.bg_image || ann.bg_color ? 'has-custom-bg' : '',
      ann.bg_image ? 'has-bg-image' : ''
    ].filter(Boolean).join(' ');
    const bgStyle = ann.bg_image
      ? `background-image: url('${ann.bg_image}');`
      : ann.bg_color
        ? `background: ${ann.bg_color};`
        : '';
    return `
    <div class="${bgClasses}" data-id="${ann.id}" style="${bgStyle}">
      <button class="home-announcement-dismiss" onclick="dismissAnnouncement('${ann.id}')">×</button>
      ${ann.icon ? `<div class="home-announcement-icon">${ann.icon}</div>` : ''}
      ${ann.badge ? `<div class="badge">${ann.badge}</div>` : ''}
      <h3>${ann.title}</h3>
      <p>${ann.description}</p>
      ${ann.cta ? `<button class="cta" onclick="${ann.cta.action === 'navigate' ? `switchTab('${ann.cta.target.slice(1)}')` : 'void(0)'}">${ann.cta.label}</button>` : ''}
    </div>`;
  }).join('');

  // Inject brand nebula background layer behind cards
  if (!carousel.querySelector('.brand-shape-layer')) {
    const nebulaLayer = document.createElement('div');
    nebulaLayer.className = 'brand-shape-layer';
    nebulaLayer.innerHTML = generateNebulaSVG(carousel.offsetWidth || 800, carousel.offsetHeight || 220, 210, {variant: 'subtle', sparkles: true});
    carousel.insertBefore(nebulaLayer, carousel.firstChild);
  }

  // Render dots
  if (homeData.announcements.length > 1) {
    dots.innerHTML = homeData.announcements.map((_, i) =>
      `<div class="home-announcement-dot${i === 0 ? ' active' : ''}" onclick="showAnnouncement(${i})"></div>`
    ).join('');
    
    // Start rotation
    startAnnouncementRotation();
  }
  
  container.style.display = 'block';
}

function showAnnouncement(index) {
  homeData.currentAnnouncementIndex = index;
  
  // Update cards
  document.querySelectorAll('.home-announcement-card').forEach((card, i) => {
    if (i === index) {
      card.classList.add('active');
    } else {
      card.classList.remove('active');
    }
  });
  
  // Update dots
  document.querySelectorAll('.home-announcement-dot').forEach((dot, i) => {
    if (i === index) {
      dot.classList.add('active');
    } else {
      dot.classList.remove('active');
    }
  });
  
  // Reset the rotation timer so we get a full interval after manual clicks
  resetAnnouncementRotation();
}

let announcementRotationTimer = null;

function startAnnouncementRotation() {
  stopAnnouncementRotation();
  if (homeData.announcements.length <= 1) return;
  announcementRotationTimer = setInterval(() => {
    const next = (homeData.currentAnnouncementIndex + 1) % homeData.announcements.length;
    homeData.currentAnnouncementIndex = next;
    
    // Update cards directly (don't call showAnnouncement to avoid resetting timer)
    document.querySelectorAll('.home-announcement-card').forEach((card, i) => {
      if (i === next) {
        card.classList.add('active');
      } else {
        card.classList.remove('active');
      }
    });
    document.querySelectorAll('.home-announcement-dot').forEach((dot, i) => {
      if (i === next) {
        dot.classList.add('active');
      } else {
        dot.classList.remove('active');
      }
    });
  }, 6000);
}

function stopAnnouncementRotation() {
  if (announcementRotationTimer) {
    clearInterval(announcementRotationTimer);
    announcementRotationTimer = null;
  }
}

function resetAnnouncementRotation() {
  stopAnnouncementRotation();
  startAnnouncementRotation();
}

function dismissAnnouncement(id) {
  const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
  dismissed.push(id);
  localStorage.setItem('vai-dismissed-announcements', JSON.stringify(dismissed));
  
  // Remove from current data and re-render
  homeData.announcements = homeData.announcements.filter(a => a.id !== id);
  renderAnnouncements();
}

async function restoreAnnouncements() {
  localStorage.removeItem('vai-dismissed-announcements');
  // Re-fetch all announcements
  try {
    const res = await fetch('/api/home/announcements');
    const data = await res.json();
    homeData.announcements = data.announcements;
    renderAnnouncements();
  } catch (err) {
    console.error('Failed to restore announcements:', err);
  }
}

function renderReleases() {
  const container = document.getElementById('releasesTimeline');
  
  const releases = homeData.releases.slice(0, 1); // Show only the latest release
  container.innerHTML = releases.map(release => `
    <div class="home-release-item">
      <div>
        <div class="home-release-version">${release.version}</div>
        <div class="home-release-date">${new Date(release.date).toLocaleDateString()}</div>
        <ul class="home-release-highlights">
          ${release.highlights.map(highlight => `<li>${highlight}</li>`).join('')}
        </ul>
      </div>
    </div>
  `).join('');
}

function renderFeaturedWorkflows() {
  const container = document.getElementById('featuredWorkflows');
  
  if (!homeData.featuredWorkflows.length) {
    container.innerHTML = '<div class="home-community-empty"><h4>Coming Soon</h4><p>Featured workflows launching soon</p></div>';
    return;
  }
  
  container.innerHTML = homeData.featuredWorkflows.map(workflow => `
    <div class="home-workflow-card">
      <h5>${workflow.name || 'Untitled'}</h5>
      <p>${workflow.description || 'No description available'}</p>
      <div class="home-workflow-meta">
        <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
        <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
      </div>
      <div class="home-workflow-actions">
        <button class="home-workflow-btn" onclick="wfStoreInstall('${workflow.packageName}', this)">Install</button>
        <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
      </div>
    </div>
  `).join('');
}

function renderCommunityWorkflows() {
  const container = document.getElementById('communityWorkflows');
  
  if (!homeData.communityWorkflows.length) {
    container.innerHTML = '<div class="home-community-empty"><h4>Be the First!</h4><p>Publish a workflow to get started</p></div>';
    return;
  }
  
  let workflows = [...homeData.communityWorkflows];

  // Sort based on current mode
  switch (homeData.sortMode) {
    case 'trending':
      // Use downloads as proxy for trending
      workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
      break;
    case 'newest':
      // Sort by name alphabetically as fallback (no publish date available)
      workflows.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
      break;
    case 'installs':
      workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
      break;
  }
  
  container.innerHTML = workflows.slice(0, 6).map(workflow => `
    <div class="home-workflow-card">
      <h5>${workflow.name || 'Untitled'}</h5>
      <p>${workflow.description || 'No description available'}</p>
      <div class="home-workflow-meta">
        <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
        <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
      </div>
      <div class="home-workflow-actions">
        <button class="home-workflow-btn" onclick="wfStoreInstall('${workflow.packageName}', this)">Install</button>
        <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
      </div>
    </div>
  `).join('');
}

function sortCommunityWorkflows(mode) {
  homeData.sortMode = mode;
  
  // Update tab appearance
  document.querySelectorAll('.home-sort-tab').forEach(tab => {
    tab.classList.toggle('active', tab.dataset.sort === mode);
  });
  
  renderCommunityWorkflows();
}

function filterAndSwitchWorkflows(domain) {
  switchTab('workflows');
  // Open the store and search for the domain
  wfStoreOpen();
  const searchInput = document.getElementById('wfStoreSearch');
  if (searchInput) {
    searchInput.value = domain;
    searchInput.dispatchEvent(new Event('input'));
  }
}

function viewWorkflowDetails(packageName) {
  // Find the workflow by packageName and show its details
  const allWorkflows = [...(homeData.featuredWorkflows || []), ...(homeData.communityWorkflows || [])];
  const wf = allWorkflows.find(w => w.packageName === packageName);
  if (wf) {
    // Use the workflow store detail modal
    wfStoreShowDetail(wf.name);
  } else {
    // Fallback: switch to workflows tab
    switchTab('workflows');
  }
}

function publishWorkflow() {
  // TODO: Open workflow publishing flow
  console.log('Opening workflow publisher');
  switchTab('workflows');
}

function reportBug() {
  // Open bug report (reuse existing functionality if available)
  const bugButton = document.getElementById('bugButton');
  if (bugButton) {
    bugButton.click();
  } else {
    window.open('https://github.com/mrlynn/voyageai-cli/issues/new', '_blank');
  }
}

// Expose home page functions for inline onclick handlers
window.filterAndSwitchWorkflows = filterAndSwitchWorkflows;
window.sortCommunityWorkflows = sortCommunityWorkflows;
window.viewWorkflowDetails = viewWorkflowDetails;
window.publishWorkflow = publishWorkflow;
window.reportBug = reportBug;
window.dismissAnnouncement = dismissAnnouncement;
window.restoreAnnouncements = restoreAnnouncements;

// ── Config ──
async function loadConfig() {
  try {
    const res = await fetch('/api/config');
    const data = await res.json();
    const dot = document.getElementById('statusDot');
    const label = document.getElementById('statusLabel');
    if (data.hasKey) {
      dot.className = 'status-dot connected';
      label.textContent = 'Connected';
    } else {
      dot.className = 'status-dot error';
      label.textContent = 'No API Key';
    }
  } catch {
    document.getElementById('statusDot').className = 'status-dot error';
    document.getElementById('statusLabel').textContent = 'Error';
  }
}

// ── Models ──
async function loadModels() {
  try {
    const res = await fetch('/api/models');
    const data = await res.json();
    allModels = data.models || [];
    embedModels = allModels.filter(m => m.type === 'embedding');
    rerankModels = allModels.filter(m => m.type === 'reranking');
  } catch {
    console.error('Failed to load models');
  }
}

function populateModelSelects() {
  const saved = localStorage.getItem('vai-playground-model');
  const embedSelects = ['globalModel', 'embedModel', 'compareModel', 'searchEmbedModel'];
  embedSelects.forEach(id => {
    const sel = document.getElementById(id);
    sel.innerHTML = '';
    embedModels.forEach(m => {
      const opt = document.createElement('option');
      opt.value = m.name;
      opt.textContent = m.name + ' — ' + m.shortFor;
      sel.appendChild(opt);
    });
    if (saved) sel.value = saved;
  });

  const rerankSel = document.getElementById('searchRerankModel');
  rerankSel.innerHTML = '';
  rerankModels.forEach(m => {
    const opt = document.createElement('option');
    opt.value = m.name;
    opt.textContent = m.name + ' — ' + m.shortFor;
    rerankSel.appendChild(opt);
  });

  // Sync global model to others
  document.getElementById('globalModel').addEventListener('change', function() {
    const v = this.value;
    localStorage.setItem('vai-playground-model', v);
    ['embedModel', 'compareModel', 'searchEmbedModel'].forEach(id => {
      document.getElementById(id).value = v;
    });
  });
}

// ── API Helpers ──
function formatBytesUI(bytes) {
  if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
  if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
  return bytes + ' B';
}

async function apiPost(url, body) {
  let res;
  try {
    res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
  } catch (fetchErr) {
    throw new Error('Failed to connect to server. Is the playground still running?');
  }
  const data = await res.json();
  if (!res.ok) {
    if (data.code === 'NO_API_KEY') {
      throw new Error('🔑 No API key configured. Open Settings (⚙) to add your Voyage AI API key, or run: vai config set api-key <your-key>');
    }
    if (data.code === 'API_KEY_BASE_MISMATCH') {
      const details = data.details || {};
      const lines = [
        'Stored API key does not match the configured endpoint.',
        details.keyLabel ? `Key type: ${details.keyLabel}` : '',
        details.base ? `Current endpoint: ${details.base}` : '',
        details.expectedBase ? `Expected endpoint: ${details.expectedBase}` : '',
        data.hint || 'Open Settings (⚙) to update the saved key or endpoint.',
      ].filter(Boolean);
      throw new Error(lines.join('\n'));
    }
    if ((data.code === 'API_FORBIDDEN' || data.code === 'API_UNAUTHORIZED') && data.hint) {
      throw new Error(`${data.error || 'API error'}\n\n${data.hint}`);
    }
    throw new Error(data.error || data.detail || 'API error');
  }
  return data;
}

function showError(id, msg) {
  const el = document.getElementById(id);
  el.textContent = '⚠ ' + msg;
  el.classList.add('visible');
}

function hideError(id) {
  document.getElementById(id).classList.remove('visible');
}

function setLoading(btnId, loading) {
  const btn = document.getElementById(btnId);
  if (loading) {
    btn.disabled = true;
    btn._origHTML = btn.innerHTML;
    btn.innerHTML = '<span class="spinner"></span> Working...';
  } else {
    btn.disabled = false;
    btn.innerHTML = btn._origHTML || btn.innerHTML;
  }
}

// ── Embed ──
window.doEmbed = async function() {
  hideError('embedError');
  const text = document.getElementById('embedInput').value.trim();
  if (!text) { showError('embedError', 'Enter some text to embed'); return; }
  const _embedModel = document.getElementById('embedModel').value;
  sendTelemetry('api_call', { endpoint: 'embed', model: _embedModel });
  const _embedDone = telemetryTimer('playground_embed', { model: _embedModel, inputType: document.getElementById('embedInputType').value || undefined });

  setLoading('embedBtn', true);
  try {
    const model = _embedModel;
    const inputType = document.getElementById('embedInputType').value || undefined;
    const dims = document.getElementById('embedDimensions').value;
    const dimensions = dims ? parseInt(dims, 10) : undefined;

    const outputDtype = document.getElementById('embedOutputDtype').value;
    const body = { texts: [text], model, inputType, dimensions };
    if (outputDtype && outputDtype !== 'float') body.output_dtype = outputDtype;

    const data = await apiPost('/api/embed', body);
    const emb = data.data[0].embedding;
    lastEmbedding = emb;

    // Stats
    const dtype = outputDtype || 'float';
    const bytesPerDim = (dtype === 'binary' || dtype === 'ubinary') ? 0.125 : (dtype === 'int8' || dtype === 'uint8') ? 1 : 4;
    const totalBytes = emb.length * bytesPerDim;
    const storageLine = dtype !== 'float'
      ? `<br><span style="color:var(--success)">📦 ${dtype}: ${formatBytesUI(totalBytes)}/vector (${(4 * emb.length / totalBytes).toFixed(0)}× smaller than float)</span>`
      : '';

    const statsEl = document.getElementById('embedStats');
    statsEl.innerHTML = `
      <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model}</span></span>
      <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${emb.length}</span></span>
      <span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || '—'}</span></span>
      <span class="stat"><span class="stat-label">Type</span><span class="stat-value">${dtype}</span></span>
      ${storageLine}
    `;

    // Vector preview
    const preview = emb.slice(0, 20).map(v => v.toFixed(6)).join(', ');
    document.getElementById('embedVector').textContent = `[${preview}, ... ] (${emb.length} values)`;

    // Heatmap
    buildHeatmap(emb, document.getElementById('embedHeatmap'));

    document.getElementById('embedResult').classList.add('visible');
    CostTracker.addOperation('embed', model, data.usage?.total_tokens || 0);
    _embedDone();
  } catch (err) {
    showError('embedError', err.message);
  } finally {
    setLoading('embedBtn', false);
  }
};

window.copyVector = function() {
  if (!lastEmbedding) return;
  navigator.clipboard.writeText(JSON.stringify(lastEmbedding)).then(() => {
    const btn = document.querySelector('[onclick="copyVector()"]');
    const orig = btn.textContent;
    btn.textContent = '✅ Copied!';
    setTimeout(() => btn.textContent = orig, 1500);
  });
};

function buildHeatmap(vec, container) {
  container.innerHTML = '';
  // Sample down to ~200 bars for visual
  const step = Math.max(1, Math.floor(vec.length / 200));
  const sampled = [];
  for (let i = 0; i < vec.length; i += step) sampled.push(vec[i]);

  const min = Math.min(...sampled);
  const max = Math.max(...sampled);
  const range = max - min || 1;

  sampled.forEach(v => {
    const bar = document.createElement('div');
    bar.className = 'heatmap-bar';
    const norm = (v - min) / range; // 0-1
    const h = Math.round(norm * 160); // hue: 0=red, 80=yellow-green, 160=cyan/teal
    bar.style.background = `hsl(${h}, 80%, 50%)`;
    container.appendChild(bar);
  });
}

// ── Compare ──
window.doCompare = async function() {
  hideError('compareError');
  sendTelemetry('api_call', { endpoint: 'compare', model: document.getElementById('compareModel').value });
  const _compareDone = telemetryTimer('playground_similarity', { model: document.getElementById('compareModel').value });
  const a = document.getElementById('compareA').value.trim();
  const b = document.getElementById('compareB').value.trim();
  if (!a || !b) { showError('compareError', 'Enter text in both fields'); return; }

  setLoading('compareBtn', true);
  try {
    const model = document.getElementById('compareModel').value;
    const dims = document.getElementById('compareDimensions').value;
    const dimensions = dims ? parseInt(dims, 10) : undefined;

    const data = await apiPost('/api/similarity', { texts: [a, b], model, dimensions });

    // Get raw embeddings for all metrics
    const vecA = data.embeddings[0].embedding;
    const vecB = data.embeddings[1].embedding;

    const cosine = cosineSim(vecA, vecB);
    const dot = dotProduct(vecA, vecB);
    const euclid = euclideanDist(vecA, vecB);

    // Hero display — cosine similarity
    const cosinePct = Math.max(0, cosine * 100);
    let cosineColor;
    if (cosine > 0.7) cosineColor = 'var(--green)';
    else if (cosine > 0.4) cosineColor = 'var(--yellow)';
    else cosineColor = 'var(--red)';

    const scoreEl = document.getElementById('simScore');
    scoreEl.textContent = cosine.toFixed(4);
    scoreEl.style.color = cosineColor;

    const barEl = document.getElementById('simBar');
    barEl.style.width = cosinePct + '%';
    barEl.style.background = cosineColor;

    // Metric cards — all three
    const dotColor = dot > 0.7 ? 'var(--green)' : dot > 0.4 ? 'var(--yellow)' : 'var(--red)';
    // Euclidean: 0 = identical, ~2 = max for normalized vectors. Invert for color.
    const euclidColor = euclid < 0.6 ? 'var(--green)' : euclid < 1.0 ? 'var(--yellow)' : 'var(--red)';
    // For euclidean bar, invert: 0 dist = 100% bar, 2.0 dist = 0%
    const euclidPct = Math.max(0, Math.min(100, (1 - euclid / 2) * 100));

    const metricsEl = document.getElementById('metricsGrid');
    metricsEl.innerHTML = `
      <div class="metric-card primary">
        <div class="metric-card-value" style="color:${cosineColor}">${cosine.toFixed(4)}</div>
        <div class="metric-card-name">Cosine Similarity</div>
        <div class="metric-card-desc">Angle between vectors (−1 to 1). Standard for semantic search.</div>
        <div class="metric-bar"><div class="metric-bar-fill" style="width:${cosinePct}%;background:${cosineColor}"></div></div>
      </div>
      <div class="metric-card">
        <div class="metric-card-value" style="color:${dotColor}">${dot.toFixed(4)}</div>
        <div class="metric-card-name">Dot Product</div>
        <div class="metric-card-desc">Sum of element-wise products. Equals cosine for normalized vectors.</div>
        <div class="metric-bar"><div class="metric-bar-fill" style="width:${Math.max(0, dot * 100)}%;background:${dotColor}"></div></div>
      </div>
      <div class="metric-card">
        <div class="metric-card-value" style="color:${euclidColor}">${euclid.toFixed(4)}</div>
        <div class="metric-card-name">Euclidean Distance</div>
        <div class="metric-card-desc">Straight-line distance (0 = identical). Lower is more similar.</div>
        <div class="metric-bar"><div class="metric-bar-fill" style="width:${euclidPct}%;background:${euclidColor}"></div></div>
      </div>
    `;

    // Insight note
    const noteEl = document.getElementById('metricNote');
    const diff = Math.abs(cosine - dot);
    if (diff < 0.001) {
      noteEl.innerHTML = '💡 <strong>Cosine ≈ Dot Product</strong> — these vectors are L2-normalized (as Voyage AI models produce), so cosine similarity and dot product give identical results. Euclidean distance is <code>√(2 − 2·cosine)</code> for normalized vectors.';
    } else {
      noteEl.innerHTML = '💡 Cosine and dot product differ because these vectors are not perfectly L2-normalized. Atlas Vector Search uses cosine by default.';
    }

    // Stats
    const statsEl = document.getElementById('compareStats');
    statsEl.innerHTML = `
      <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model}</span></span>
      <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
      <span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || '—'}</span></span>
    `;

    document.getElementById('compareResult').classList.add('visible');
    CostTracker.addOperation('compare', data.model || model, data.usage?.total_tokens || 0);
    _compareDone();
  } catch (err) {
    showError('compareError', err.message);
  } finally {
    setLoading('compareBtn', false);
  }
};

// ── Search ──
window.doSearch = async function(withRerank) {
  hideError('searchError');
  sendTelemetry('api_call', {
    endpoint: withRerank ? 'rerank' : 'search',
    model: document.getElementById('searchEmbedModel').value,
    rerankModel: withRerank ? document.getElementById('searchRerankModel').value : undefined,
  });
  const _searchDone = telemetryTimer(withRerank ? 'playground_rerank' : 'playground_search', {
    model: document.getElementById('searchEmbedModel').value,
    rerankModel: withRerank ? document.getElementById('searchRerankModel').value : undefined,
  });
  const query = document.getElementById('searchQuery').value.trim();
  const docsText = document.getElementById('searchDocs').value.trim();
  if (!query || !docsText) { showError('searchError', 'Enter a query and documents'); return; }

  const documents = docsText.split('\n').map(d => d.trim()).filter(d => d);
  if (documents.length < 2) { showError('searchError', 'Enter at least 2 documents'); return; }

  const btnId = withRerank ? 'searchRerankBtn' : 'searchBtn';
  setLoading('searchBtn', true);
  setLoading('searchRerankBtn', true);

  try {
    const embedModel = document.getElementById('searchEmbedModel').value;
    const topK = parseInt(document.getElementById('searchTopK').value, 10);

    // Embed query + docs
    const allTexts = [query, ...documents];
    const embedData = await apiPost('/api/embed', { texts: allTexts, model: embedModel });
    const vecs = embedData.data.map(d => d.embedding);
    const queryVec = vecs[0];
    const docVecs = vecs.slice(1);

    // Compute similarity
    const scores = docVecs.map((dv, i) => ({
      index: i,
      text: documents[i],
      score: cosineSim(queryVec, dv),
    }));
    scores.sort((a, b) => b.score - a.score);
    const embeddingResults = scores.slice(0, topK);

    let rerankResults = null;
    let rerankData = null;
    if (withRerank) {
      const rerankModel = document.getElementById('searchRerankModel').value;
      rerankData = await apiPost('/api/rerank', { query, documents, model: rerankModel, topK });
      rerankResults = rerankData.data.map(r => ({
        index: r.index,
        text: documents[r.index],
        score: r.relevance_score,
      }));
    }

    renderSearchResults(embeddingResults, rerankResults);
    document.getElementById('searchResult').classList.add('visible');
    // Capture for export
    window._lastSearchResults = {
      query,
      model: embedModel,
      results: (rerankResults || embeddingResults).map((r, i) => ({
        rank: i + 1, score: r.score, text: r.text, source: `document-${r.index + 1}`,
        ...(rerankResults ? { rerankedScore: r.score } : {}),
      })),
    };
    CostTracker.addOperation('search', embedModel, embedData.usage?.total_tokens || 0);
    if (withRerank && rerankResults) {
      const rerankModel = document.getElementById('searchRerankModel').value;
      CostTracker.addOperation('rerank', rerankModel, rerankData?.usage?.total_tokens || 0);
    }
    _searchDone();
  } catch (err) {
    showError('searchError', err.message);
  } finally {
    setLoading('searchBtn', false);
    setLoading('searchRerankBtn', false);
  }
};

function cosineSim(a, b) {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

function dotProduct(a, b) {
  let sum = 0;
  for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
  return sum;
}

function euclideanDist(a, b) {
  let sum = 0;
  for (let i = 0; i < a.length; i++) sum += (a[i] - b[i]) ** 2;
  return Math.sqrt(sum);
}

function renderSearchResults(embResults, rerankResults) {
  const grid = document.getElementById('searchResultGrid');
  grid.innerHTML = '';
  grid.className = rerankResults ? 'search-results' : 'search-results single-col';

  const maxEmbScore = Math.max(...embResults.map(r => r.score));

  // Embedding column
  const embCol = document.createElement('div');
  embCol.innerHTML = '<div class="card-title">Embedding Search</div>';
  embResults.forEach((r, i) => {
    embCol.appendChild(createResultItem(i + 1, r, maxEmbScore));
  });
  grid.appendChild(embCol);

  // Rerank column
  if (rerankResults) {
    const maxRerankScore = Math.max(...rerankResults.map(r => r.score));
    const embRankMap = {};
    embResults.forEach((r, i) => { embRankMap[r.index] = i + 1; });

    const rerankCol = document.createElement('div');
    rerankCol.innerHTML = '<div class="card-title">After Reranking</div>';
    rerankResults.forEach((r, i) => {
      const newRank = i + 1;
      const oldRank = embRankMap[r.index];
      const movement = oldRank !== undefined ? oldRank - newRank : 0;
      rerankCol.appendChild(createResultItem(newRank, r, maxRerankScore, movement));
    });
    grid.appendChild(rerankCol);
  }
}

function createResultItem(rank, result, maxScore, movement) {
  const item = document.createElement('div');
  let moveClass = '';
  if (movement > 0) moveClass = ' moved-up';
  else if (movement < 0) moveClass = ' moved-down';

  const pct = maxScore > 0 ? (result.score / maxScore * 100) : 0;
  const truncText = result.text.length > 80 ? result.text.slice(0, 77) + '...' : result.text;
  let moveHTML = '';
  if (movement !== undefined && movement !== 0) {
    const arrow = movement > 0 ? '↑' : '↓';
    const color = movement > 0 ? 'var(--green)' : 'var(--red)';
    moveHTML = `<div class="result-movement" style="color:${color}">${arrow}${Math.abs(movement)}</div>`;
  }

  item.className = 'result-item' + moveClass;
  item.innerHTML = `
    <div class="result-rank">#${rank}</div>
    <div class="result-body">
      <div class="result-text" title="${result.text.replace(/"/g, '&quot;')}">${truncText}</div>
      <div class="result-score-bar"><div class="result-score-fill" style="width:${pct}%"></div></div>
      <div class="result-score-text">${result.score.toFixed(4)}</div>
      ${moveHTML}
    </div>
  `;
  return item;
}

// ── Explore ──
// ── Generate & Scaffold ──

let generateMode = 'code';

function setGenerateMode(mode) {
  generateMode = mode;
  const codeBtn = document.getElementById('genModeCode');
  const scaffoldBtn = document.getElementById('genModeScaffold');
  codeBtn.classList.toggle('active', mode === 'code');
  codeBtn.setAttribute('aria-selected', mode === 'code');
  scaffoldBtn.classList.toggle('active', mode === 'scaffold');
  scaffoldBtn.setAttribute('aria-selected', mode === 'scaffold');
  document.getElementById('generateCodePanel').style.display = mode === 'code' ? 'block' : 'none';
  document.getElementById('scaffoldPanel').style.display = mode === 'scaffold' ? 'block' : 'none';
}

function updateGenComponents() {
  const target = document.getElementById('genTarget').value;
  const select = document.getElementById('genComponent');
  const components = {
    vanilla: ['client', 'connection', 'retrieval', 'ingest', 'search-api'],
    nextjs: ['client', 'connection', 'retrieval', 'ingest', 'search-page'],
    python: ['client', 'connection', 'retrieval', 'ingest'],
  };
  const labels = {
    client: 'API Client',
    connection: 'MongoDB Connection',
    retrieval: 'RAG Retrieval',
    ingest: 'Document Ingest',
    'search-api': 'Search API',
    'search-page': 'Search Page (MUI)',
  };
  select.innerHTML = (components[target] || []).map(c =>
    `<option value="${c}">${labels[c] || c}</option>`
  ).join('');
}

async function generateCode() {
  const btn = document.getElementById('genCodeBtn');
  const outputCard = document.getElementById('genOutputCard');
  
  btn.disabled = true;
  btn.textContent = 'Generating...';
  
  try {
    const config = {
      model: document.getElementById('genModel').value,
      db: document.getElementById('genDb').value,
      collection: document.getElementById('genCollection').value,
      rerank: document.getElementById('genRerank').checked,
    };
    
    const target = document.getElementById('genTarget').value;
    const component = document.getElementById('genComponent').value;
    
    // Check if running in Electron with vai.generate
    if (window.vai && window.vai.generate) {
      const result = await window.vai.generate.code({ target, component, config });
      if (result.error) throw new Error(result.error);
      
      document.getElementById('genOutputFilename').textContent = result.filename;
      document.getElementById('genOutputCode').textContent = result.code;
      outputCard.style.display = 'block';
    } else {
      // Web fallback - call the API
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ target, component, config }),
      });
      const result = await res.json();
      if (result.error) throw new Error(result.error);
      
      document.getElementById('genOutputFilename').textContent = result.filename || `${component}.js`;
      document.getElementById('genOutputCode').textContent = result.code;
      outputCard.style.display = 'block';
    }
  } catch (err) {
    alert('Error generating code: ' + err.message);
  } finally {
    btn.disabled = false;
    btn.textContent = 'Generate Code';
  }
}

function copyGeneratedCode() {
  const code = document.getElementById('genOutputCode').textContent;
  navigator.clipboard.writeText(code);
  // Brief feedback
  const btn = event.target;
  btn.textContent = 'Copied!';
  setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
}

// Scaffold functions
function initScaffold() {
  const isElectron = window.vai && window.vai.scaffold;
  
  // Show/hide mode-specific sections
  document.getElementById('scaffoldLocationSection').style.display = isElectron ? 'block' : 'none';
  document.getElementById('scaffoldWebSection').style.display = isElectron ? 'none' : 'block';
  document.getElementById('scaffoldElectronButtons').style.display = isElectron ? 'block' : 'none';
  document.getElementById('scaffoldWebButtons').style.display = isElectron ? 'none' : 'block';
  
  // Update CLI command preview on input
  document.getElementById('scaffoldName').addEventListener('input', updateScaffoldPreview);
  document.getElementById('scaffoldTarget').addEventListener('change', updateScaffoldPreview);
}

function updateScaffoldPreview() {
  const name = document.getElementById('scaffoldName').value || 'my-rag-app';
  const target = document.getElementById('scaffoldTarget').value;
  document.getElementById('scaffoldCliName').textContent = name;
  document.getElementById('scaffoldCliTarget').textContent = target;
}

async function downloadScaffoldZip() {
  const btn = document.getElementById('scaffoldDownloadBtn');
  const projectName = document.getElementById('scaffoldName').value || 'my-rag-app';
  const target = document.getElementById('scaffoldTarget').value;
  const config = {
    model: document.getElementById('scaffoldModel').value,
    db: document.getElementById('scaffoldDb').value,
    collection: document.getElementById('scaffoldCollection').value,
  };
  
  btn.disabled = true;
  btn.innerHTML = '<span style="margin-right:6px;">⏳</span> Generating...';
  
  try {
    const res = await fetch('/api/scaffold', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ projectName, target, config }),
    });
    
    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.error || 'Failed to generate scaffold');
    }
    
    // Download the ZIP
    const blob = await res.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${projectName}.zip`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    
    // Show success
    btn.innerHTML = '<span style="margin-right:6px;">✓</span> Downloaded!';
    setTimeout(() => {
      btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
      btn.disabled = false;
    }, 2000);
  } catch (err) {
    alert('Error: ' + err.message);
    btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
    btn.disabled = false;
  }
}

async function pickScaffoldDirectory() {
  if (!window.vai || !window.vai.scaffold) return;
  
  const result = await window.vai.scaffold.pickDirectory();
  if (!result.canceled) {
    document.getElementById('scaffoldPath').value = result.path;
    document.getElementById('scaffoldBtn').disabled = false;
  }
}

async function createScaffold() {
  if (!window.vai || !window.vai.scaffold) return;
  
  const btn = document.getElementById('scaffoldBtn');
  const resultCard = document.getElementById('scaffoldResultCard');
  const resultDiv = document.getElementById('scaffoldResult');
  
  const dirPath = document.getElementById('scaffoldPath').value;
  const projectName = document.getElementById('scaffoldName').value || 'my-rag-app';
  const target = document.getElementById('scaffoldTarget').value;
  
  if (!dirPath) {
    alert('Please choose a location first.');
    return;
  }
  
  btn.disabled = true;
  btn.textContent = 'Creating...';
  resultCard.style.display = 'none';
  
  try {
    // Check if directory exists
    const check = await window.vai.scaffold.checkDirectory({ dirPath, projectName });
    if (check.exists && check.fileCount > 0) {
      const confirm = await window.vai.scaffold.confirmOverwrite({ projectName });
      if (!confirm.confirmed) {
        btn.disabled = false;
        btn.textContent = 'Create Project';
        return;
      }
    }
    
    // Listen for progress
    const cleanup = window.vai.scaffold.onProgress((data) => {
      resultDiv.innerHTML = `<p>Creating: ${data.file} (${data.current}/${data.total})</p>`;
      resultCard.style.display = 'block';
    });
    
    // Create the project
    const config = {
      model: document.getElementById('scaffoldModel').value,
      db: document.getElementById('scaffoldDb').value,
      collection: document.getElementById('scaffoldCollection').value,
    };
    
    const result = await window.vai.scaffold.create({ dirPath, projectName, target, config });
    cleanup();
    
    if (result.error) throw new Error(result.error);
    
    // Show success
    resultDiv.innerHTML = `
      <p style="color:var(--primary);">✓ Created ${result.files.length} files in <code>${projectName}/</code></p>
      <div style="margin:12px 0;padding:12px;background:var(--surface-darker);border-radius:8px;">
        <p style="margin:0 0 8px;font-weight:500;">Next steps:</p>
        <code style="display:block;margin-bottom:4px;">cd ${projectName}</code>
        <code style="display:block;margin-bottom:4px;">cp .env.example .env</code>
        <code style="display:block;margin-bottom:4px;">${result.postInstall}</code>
        <code style="display:block;">${result.startCommand}</code>
      </div>
      <button class="btn btn-outline" onclick="window.vai.scaffold.openDirectory({ dirPath: '${result.projectDir.replace(/'/g, "\\'")}' })">
        Open in Finder
      </button>
    `;
    resultCard.style.display = 'block';
    
  } catch (err) {
    resultDiv.innerHTML = `<p style="color:var(--error);">Error: ${err.message}</p>`;
    resultCard.style.display = 'block';
  } finally {
    btn.disabled = false;
    btn.textContent = 'Create Project';
  }
}

// Initialize scaffold on load
document.addEventListener('DOMContentLoaded', () => {
  setTimeout(initScaffold, 100);
});

// ── Explore: icons and tab mappings per concept ──
// Lucide SVG icon helper — returns an inline <svg> string
function lucideIcon(d, size = 16) {
  return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
}

// Lucide path constants for concept/button icons
const LI = {
  zap:        'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
  search:     'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
  trophy:     'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z',
  bot:        'M12 8V4H8M8 2h8M2 14a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zM6 14v4M10 14v4M14 14v4M18 14v4',
  ruler:      'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z M15 5l4 4',
  target:     'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0',
  tag:        'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42zM7.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
  brain:      'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2zM14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z',
  key:        'M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4zM16.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
  globe:      'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z',
  package:    'M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12',
  timer:      'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
  flask:      'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10',
  puzzle:     'M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.878.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.968 1.02z',
  link:       'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
  barChart:   'M12 20V10M18 20V4M6 20v-4',
  microscope: 'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z',
  image:      'M3 3h18a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21',
  shuffle:    'M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22M18 2l4 4-4 4M2 6h1.9c1.5 0 2.9.9 3.6 2.2M22 18l-4 4-4-4M19 14h3',
  circle:     'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0',
  fileTxt:    'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v4a2 2 0 0 0 2 2h4M10 13h4M10 17h4M10 9h1',
  scale:      'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22',
  laptop:     'M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9M2 20h20M12 12h.01',
  blocks:     'M2 12h10v10H2zM14 4l6 3.5v7L14 18l-6-3.5v-7zM12 2l10 6',
  refresh:    'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16',
  sparkle:    'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z',
  download:   'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
  copy:       'M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z',
  filter:     'M3 6h18M7 12h10M10 18h4',
};

const CONCEPT_META = {
  embeddings:          { icon: LI.zap,        tab: 'embed' },
  reranking:           { icon: LI.trophy,     tab: 'search' },
  'vector-search':     { icon: LI.search,     tab: 'search' },
  rag:                 { icon: LI.bot,        tab: 'search' },
  'cosine-similarity': { icon: LI.ruler,      tab: 'compare' },
  'two-stage-retrieval': { icon: LI.target,   tab: 'search' },
  'input-type':        { icon: LI.tag,        tab: 'embed' },
  models:              { icon: LI.brain,      tab: 'embed' },
  'api-keys':          { icon: LI.key,        tab: 'embed' },
  'api-access':        { icon: LI.globe,      tab: 'embed' },
  'batch-processing':  { icon: LI.package,    tab: 'embed' },
  benchmarking:        { icon: LI.timer,      tab: 'benchmark' },
  quantization:        { icon: LI.flask,      tab: 'benchmark' },
  'mixture-of-experts': { icon: LI.puzzle,    tab: 'embed' },
  'shared-embedding-space': { icon: LI.link,  tab: 'compare' },
  'rteb-benchmarks':   { icon: LI.barChart,   tab: 'benchmark' },
  'voyage-4-nano':     { icon: LI.microscope, tab: 'nano' },
  'local-inference':   { icon: LI.laptop,     tab: 'nano' },
  'rerank-eval':       { icon: LI.ruler,      tab: 'benchmark' },
  'multimodal-embeddings': { icon: LI.image,  tab: 'multimodal' },
  'cross-modal-search':    { icon: LI.shuffle, tab: 'multimodal' },
  'modality-gap':          { icon: LI.circle,  tab: 'multimodal' },
  'multimodal-rag':        { icon: LI.fileTxt, tab: 'multimodal' },
  'provider-comparison':   { icon: LI.scale,   tab: 'explore' },
  'code-generation':   { icon: LI.laptop,     tab: 'explore' },
  scaffolding:         { icon: LI.blocks,     tab: 'explore' },
  'eval-comparison':   { icon: LI.barChart,   tab: 'benchmark' },
  'auto-embedding':        { icon: LI.zap,    tab: 'explore' },
  'vai-vs-auto-embedding': { icon: LI.refresh, tab: 'explore' },
};

let exploreConcepts = {};

async function loadConcepts() {
  try {
    const res = await fetch('/api/concepts');
    const data = await res.json();
    exploreConcepts = data.concepts || {};
  } catch {
    console.error('Failed to load concepts');
  }
}

function escapeHtml(str) {
  if (str == null) return '';
  const s = typeof str === 'string' ? str : String(str);
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function renderExploreRobotGuides() {
  const robot = window.VAIRobotAssets;
  const headerGuide = document.getElementById('exploreHeaderGuide');
  const modalGuide = document.getElementById('exploreModalGuide');
  if (!headerGuide || !modalGuide) return;

  if (!robot || typeof robot.getFrame !== 'function' || typeof robot.renderSvg !== 'function') {
    headerGuide.classList.add('is-hidden');
    modalGuide.classList.add('is-hidden');
    return;
  }

  const headerRobot = document.getElementById('exploreHeaderRobot');
  const modalRobot = document.getElementById('exploreModalRobot');

  if (headerRobot) {
    headerRobot.innerHTML = robot.renderSvg(robot.getFrame('thinking', 1), 3);
  }

  if (modalRobot) {
    modalRobot.innerHTML = robot.renderSvg(robot.getFrame('blink', 0), 2);
  }
}

function updateExploreModalGuide(meta) {
  const guideCopy = document.getElementById('exploreModalGuideCopy');
  if (!guideCopy) return;
  const target = meta && meta.tab ? String(meta.tab).replace(/-/g, ' ') : 'the playground';
  const label = target.charAt(0).toUpperCase() + target.slice(1);
  guideCopy.textContent = `Learn the concept here, then try it live in ${label}.`;
}

function buildExploreCards() {
  const grid = document.getElementById('exploreGrid');
  grid.innerHTML = '';

  for (const [key, concept] of Object.entries(exploreConcepts)) {
    const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
    const card = document.createElement('div');
    card.className = 'explore-card';
    card.dataset.key = key;

    card.innerHTML = `
      <div class="explore-card-icon">${lucideIcon(meta.icon, 28)}</div>
      <div class="explore-card-title">${escapeHtml(concept.title)}</div>
      <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
    `;
    card.addEventListener('click', () => openExploreModal(key));
    grid.appendChild(card);
  }

  // Modal close handlers
  const modal = document.getElementById('exploreModal');
  document.getElementById('exploreModalClose').addEventListener('click', closeExploreModal);
  modal.addEventListener('click', (e) => { if (e.target === modal) closeExploreModal(); });
  
  // Close modal on Escape key for accessibility
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && modal.classList.contains('open')) {
      closeExploreModal();
    }
  });
}

// Store previously focused element for modal focus management
let exploreModalPreviousFocus = null;

function openExploreModal(key) {
  const concept = exploreConcepts[key];
  if (!concept) return;
  const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };

  // Save the currently focused element to restore when modal closes
  exploreModalPreviousFocus = document.activeElement;

  document.getElementById('exploreModalIcon').innerHTML = lucideIcon(meta.icon, 32);
  document.getElementById('exploreModalTitle').textContent = concept.title;
  document.getElementById('exploreModalSummary').textContent = concept.summary;
  updateExploreModalGuide(meta);

  // Build body: content + links + tryIt
  let bodyHtml = escapeHtml(concept.content);

  if (concept.links && concept.links.length > 0) {
    bodyHtml += '<div class="explore-modal-links">' +
      '<div class="explore-modal-links-title">Learn More</div>' +
      concept.links.map(url =>
        `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`
      ).join('') + '</div>';
  }

  if (concept.tryIt && concept.tryIt.length > 0) {
    bodyHtml += '<div class="explore-modal-tryit">' +
      '<div class="explore-modal-tryit-title">Try It</div>' +
      concept.tryIt.map(cmd =>
        `<div class="explore-modal-tryit-cmd">$ ${escapeHtml(cmd)}</div>`
      ).join('') + '</div>';
  }

  document.getElementById('exploreModalBody').innerHTML = bodyHtml;

  // Actions
  const actionsEl = document.getElementById('exploreModalActions');
  actionsEl.innerHTML = `<button class="btn btn-small" id="exploreModalTry">Try it in playground →</button>`;
  actionsEl.querySelector('#exploreModalTry').addEventListener('click', () => {
    closeExploreModal();
    if (meta.tab) switchTab(meta.tab);
  });

  const modal = document.getElementById('exploreModal');
  modal.classList.add('open');
  
  // Focus the close button for accessibility
  setTimeout(() => {
    document.getElementById('exploreModalClose').focus();
  }, 100);
}

function closeExploreModal() {
  document.getElementById('exploreModal').classList.remove('open');
  
  // Restore focus to the previously focused element
  if (exploreModalPreviousFocus && typeof exploreModalPreviousFocus.focus === 'function') {
    exploreModalPreviousFocus.focus();
    exploreModalPreviousFocus = null;
  }
}

window.tryTopic = function(key) {
  const meta = CONCEPT_META[key];
  if (meta) switchTab(meta.tab);
};

window.filterExplore = function() {
  const q = document.getElementById('exploreSearch').value.toLowerCase().trim();
  document.querySelectorAll('#exploreGrid .explore-card').forEach(card => {
    if (!q) { card.style.display = ''; return; }
    const text = card.textContent.toLowerCase();
    card.style.display = text.includes(q) ? '' : 'none';
  });
};

// ── Models Tab ──
let modelsTabData = {
  catalog: null,
  benchmarks: null,
  currentFilter: 'all',
  initialized: false
};

async function loadModelsCatalog() {
  if (modelsTabData.catalog) return;
  try {
    const res = await fetch('/api/models/catalog');
    const data = await res.json();
    modelsTabData.catalog = data.models || [];
    modelsTabData.benchmarks = data.benchmarks || [];
  } catch {
    modelsTabData.catalog = allModels;
    modelsTabData.benchmarks = [];
  }
}

function modelsInit() {
  if (modelsTabData.initialized) return;
  modelsTabData.initialized = true;
  loadModelsCatalog().then(() => {
    renderModelsHero();
    renderModelsGroups();
    renderModelsRtebChart();
  });
}

function renderModelsHero() {
  const heroModels = document.getElementById('modelsHeroModels');
  const sharedModels = (modelsTabData.catalog || []).filter(
    m => m.sharedSpace === 'voyage-4' && !m.legacy && !m.unreleased
  );
  heroModels.innerHTML = sharedModels.map(m =>
    '<div class="model-chip" onclick="scrollToModelCard(\'' + m.name + '\')">' +
      '<span>' + escapeHtml(m.name) + '</span>' +
      '<span class="model-chip-price">' + escapeHtml(m.price) + '</span>' +
    '</div>'
  ).join('');
}

function getModelGroups(models) {
  const groups = [
    {
      id: 'voyage-4',
      title: 'Voyage 4 Flagship',
      icon: LI.zap,
      models: models.filter(m => m.family === 'voyage-4' && !m.legacy)
    },
    {
      id: 'domain',
      title: 'Domain-Specific',
      icon: LI.target,
      models: models.filter(m => !m.family && m.type === 'embedding' && !m.legacy && !m.multimodal)
    },
    {
      id: 'multimodal',
      title: 'Multimodal',
      icon: LI.image,
      models: models.filter(m => m.multimodal && !m.legacy)
    },
    {
      id: 'reranking',
      title: 'Reranking',
      icon: LI.shuffle,
      models: models.filter(m => m.type === 'reranking' && !m.legacy)
    },
    {
      id: 'legacy',
      title: 'Previous Generation',
      icon: LI.timer,
      models: models.filter(m => m.legacy === true)
    }
  ];
  return groups.filter(g => g.models.length > 0);
}

function renderModelsGroups() {
  const container = document.getElementById('modelsGroups');
  const catalog = modelsTabData.catalog || [];
  const groups = getModelGroups(catalog);

  container.innerHTML = groups.map(function(group, gi) {
    return '<div class="models-group" id="models-group-' + group.id + '">' +
      '<div class="models-group-header">' +
        '<span class="models-group-icon">' + lucideIcon(group.icon, 18) + '</span>' +
        '<span class="models-group-title">' + escapeHtml(group.title) + '</span>' +
        '<span class="models-group-count">' + group.models.length + '</span>' +
      '</div>' +
      '<div class="models-group-grid">' +
        group.models.map(function(m, mi) { return renderModelCard(m, gi * 100 + mi); }).join('') +
      '</div>' +
    '</div>';
  }).join('');
}

function renderModelCard(model, seedOffset) {
  var typeClass = model.multimodal ? 'multimodal' :
                  model.type === 'reranking' ? 'reranking' : 'embedding';
  var typeLabel = model.multimodal ? 'Multimodal' :
                  model.type === 'reranking' ? 'Reranker' : 'Embedding';

  var specs = [];
  if (model.context) specs.push({label: 'Context', value: model.context});
  if (model.dimensions && model.dimensions !== '\u2014') {
    var dimDefault = model.dimensions.split('(')[0].trim().split(',')[0];
    specs.push({label: 'Dims', value: dimDefault});
  }
  specs.push({label: 'Price', value: model.price});
  if (model.architecture) specs.push({label: 'Arch', value: model.architecture === 'moe' ? 'MoE' : 'Dense'});

  var sharedBadge = model.sharedSpace ?
    '<div class="model-card-shared-badge">' + lucideIcon(LI.link, 12) + ' Shared: ' + model.sharedSpace + '</div>' : '';

  var rtebBadge = model.rtebScore ?
    '<div class="model-card-rteb">' + model.rtebScore.toFixed(1) + '</div>' : '';

  var tryActions = '';
  if (model.multimodal) {
    tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'multimodal\')">Try in Multimodal</button>';
  } else if (model.type === 'reranking') {
    tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Rerank</button>';
  } else if (!model.legacy) {
    tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'embed\')">Try in Embed</button>' +
                 '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Rerank</button>';
  }

  return '<div class="model-card ' + (model.legacy ? 'legacy' : '') + '" data-model="' + model.name + '" data-type="' + typeClass + '" id="model-card-' + model.name + '">' +
    rtebBadge +
    '<div class="model-card-header">' +
      '<span class="model-card-name">' + escapeHtml(model.name) + '</span>' +
      '<span class="model-card-type-badge ' + typeClass + '">' + typeLabel + '</span>' +
    '</div>' +
    sharedBadge +
    '<div class="model-card-desc">' + escapeHtml(model.bestFor) + '</div>' +
    '<div class="model-card-specs">' +
      specs.map(function(s) {
        return '<div class="model-card-spec">' +
          '<span class="model-card-spec-label">' + s.label + '</span>' +
          '<span class="model-card-spec-value">' + escapeHtml(s.value) + '</span>' +
        '</div>';
      }).join('') +
    '</div>' +
    (tryActions ? '<div class="model-card-actions">' + tryActions + '</div>' : '') +
  '</div>';
}

function renderModelsRtebChart() {
  var chart = document.getElementById('modelsRtebChart');
  var benchmarks = modelsTabData.benchmarks || [];
  if (!benchmarks.length) {
    chart.innerHTML = '<p style="color:var(--text-muted);font-size:13px;">Benchmark data unavailable.</p>';
    return;
  }
  var maxScore = Math.max.apply(null, benchmarks.map(function(b) { return b.score; }));
  var minDisplay = 55;

  chart.innerHTML = benchmarks.map(function(b) {
    var isVoyage = b.provider === 'Voyage AI';
    var barPct = ((b.score - minDisplay) / (maxScore - minDisplay)) * 100;
    var color = isVoyage ? 'var(--accent)' : '#555';
    var labelColor = isVoyage ? 'var(--accent)' : 'var(--text-dim)';
    var fontWeight = isVoyage ? '600' : '400';

    return '<div class="models-rteb-bar">' +
      '<span class="models-rteb-label" style="color:' + labelColor + ';font-weight:' + fontWeight + ';">' + escapeHtml(b.model) + '</span>' +
      '<div class="models-rteb-track">' +
        '<div class="models-rteb-fill" style="width:' + barPct.toFixed(1) + '%;background:' + color + ';' + (isVoyage ? '' : 'opacity:0.6;') + '"></div>' +
      '</div>' +
      '<span class="models-rteb-score" style="color:' + labelColor + ';">' + b.score.toFixed(2) + '</span>' +
    '</div>';
  }).join('');
}

function scrollToModelCard(name) {
  var card = document.getElementById('model-card-' + name);
  if (card) {
    card.scrollIntoView({ behavior: 'smooth', block: 'center' });
    card.style.outline = '2px solid var(--accent)';
    setTimeout(function() { card.style.outline = ''; }, 2000);
  }
}

window.filterModels = function() {
  var q = document.getElementById('modelsSearch').value.toLowerCase().trim();
  document.querySelectorAll('.model-card').forEach(function(card) {
    if (!q) { card.style.display = ''; return; }
    var text = card.textContent.toLowerCase();
    card.style.display = text.includes(q) ? '' : 'none';
  });
  document.querySelectorAll('.models-group').forEach(function(group) {
    var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
    group.style.display = visible.length ? '' : 'none';
  });
};

window.setModelFilter = function(filter) {
  modelsTabData.currentFilter = filter;
  document.querySelectorAll('.models-filter-pill').forEach(function(p) {
    p.classList.toggle('active', p.dataset.filter === filter);
  });
  document.querySelectorAll('.model-card').forEach(function(card) {
    if (filter === 'all') { card.style.display = ''; return; }
    var cardType = card.dataset.type;
    card.style.display = cardType === filter ? '' : 'none';
  });
  document.querySelectorAll('.models-group').forEach(function(group) {
    var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
    group.style.display = visible.length ? '' : 'none';
  });
};

// ── Model Picker ──
var modelPickerState = {
  isOpen: false,
  targetSelectId: null,
  context: null,
  selectedModel: null,
  previousFocus: null,
  modelType: 'embedding'
};

function openModelPicker(selectId, context) {
  modelPickerState.isOpen = true;
  modelPickerState.targetSelectId = selectId;
  modelPickerState.context = context;
  modelPickerState.previousFocus = document.activeElement;

  var sel = document.getElementById(selectId);
  modelPickerState.selectedModel = sel ? sel.value : null;
  modelPickerState.modelType = selectId === 'searchRerankModel' ? 'reranking' : 'embedding';

  var contextHints = {
    embed: 'Choose an embedding model for vectorizing text.',
    compare: 'Choose a model for similarity scoring.',
    search: 'Choose an embedding model for document search.',
    multimodal: 'Only multimodal models are available here.'
  };
  document.getElementById('modelPickerContext').textContent = contextHints[context] || '';

  var rec = document.getElementById('modelPickerRecommendation');
  var recommendations = {
    embed: 'Tip: voyage-4 offers balanced quality and cost.',
    compare: 'Tip: voyage-4-large gives highest accuracy comparisons.',
    search: 'Tip: Use voyage-4-large for docs, voyage-4-lite for queries (shared space saves cost).',
    multimodal: 'Tip: voyage-multimodal-3.5 supports text, images, and video.'
  };
  if (recommendations[context]) {
    rec.textContent = recommendations[context];
    rec.classList.add('visible');
  } else {
    rec.classList.remove('visible');
  }

  renderModelPickerList();
  document.getElementById('modelPickerOverlay').classList.add('open');
  setTimeout(function() {
    document.getElementById('modelPickerSearch').focus();
  }, 100);
}

function closeModelPicker() {
  modelPickerState.isOpen = false;
  document.getElementById('modelPickerOverlay').classList.remove('open');
  document.getElementById('modelPickerSearch').value = '';
  if (modelPickerState.previousFocus) {
    modelPickerState.previousFocus.focus();
    modelPickerState.previousFocus = null;
  }
}

function selectModelFromPicker(name) {
  var sel = document.getElementById(modelPickerState.targetSelectId);
  if (sel) {
    sel.value = name;
    sel.dispatchEvent(new Event('change'));
    localStorage.setItem('vai-playground-model', name);
    if (modelPickerState.modelType === 'embedding') {
      ['embedModel', 'compareModel', 'searchEmbedModel'].forEach(function(id) {
        var otherSel = document.getElementById(id);
        if (otherSel && otherSel !== sel) otherSel.value = name;
      });
    }
  }
  closeModelPicker();
  sendTelemetry('model_picker_select', { model: name, context: modelPickerState.context });
}

function renderModelPickerList() {
  var list = document.getElementById('modelPickerList');
  var type = modelPickerState.modelType;
  var context = modelPickerState.context;
  var selected = modelPickerState.selectedModel;

  var models;
  if (context === 'multimodal') {
    models = allModels.filter(function(m) { return m.multimodal; });
  } else if (type === 'reranking') {
    models = rerankModels;
  } else {
    models = embedModels;
  }

  list.innerHTML = models.map(function(m) {
    var isSelected = m.name === selected;
    var tags = [];
    if (m.architecture === 'moe') tags.push({text: 'MoE', cls: 'model-picker-item-tag'});
    if (m.sharedSpace) tags.push({text: 'Shared Space', cls: 'model-picker-item-shared'});
    if (m.rtebScore) tags.push({text: 'RTEB ' + m.rtebScore, cls: 'model-picker-item-tag'});

    var meta = [m.bestFor, m.context, m.price].filter(Boolean).join(' \u00b7 ');

    return '<div class="model-picker-item ' + (isSelected ? 'selected' : '') + '" ' +
      'role="option" aria-selected="' + isSelected + '" tabindex="0" ' +
      'data-model="' + m.name + '" ' +
      'onclick="selectModelFromPicker(\'' + m.name + '\')" ' +
      'onkeydown="if(event.key===\'Enter\') selectModelFromPicker(\'' + m.name + '\')">' +
      '<div class="model-picker-item-radio"></div>' +
      '<div class="model-picker-item-info">' +
        '<div class="model-picker-item-name">' + escapeHtml(m.name) + '</div>' +
        '<div class="model-picker-item-meta">' + escapeHtml(meta) + '</div>' +
        (tags.length ? '<div class="model-picker-item-tags">' +
          tags.map(function(t) { return '<span class="' + t.cls + '">' + t.text + '</span>'; }).join('') +
        '</div>' : '') +
      '</div>' +
    '</div>';
  }).join('');
}

window.filterModelPicker = function() {
  var q = document.getElementById('modelPickerSearch').value.toLowerCase().trim();
  document.querySelectorAll('#modelPickerList .model-picker-item').forEach(function(item) {
    if (!q) { item.style.display = ''; return; }
    var text = item.textContent.toLowerCase();
    item.style.display = text.includes(q) ? '' : 'none';
  });
};

function enhanceModelSelects() {
  var selectConfigs = [
    { id: 'embedModel', context: 'embed' },
    { id: 'compareModel', context: 'compare' },
    { id: 'searchEmbedModel', context: 'search' },
    { id: 'searchRerankModel', context: 'search' }
  ];

  selectConfigs.forEach(function(cfg) {
    var sel = document.getElementById(cfg.id);
    if (!sel || sel.dataset.pickerEnhanced) return;
    sel.dataset.pickerEnhanced = 'true';

    var pickerBtn = document.createElement('button');
    pickerBtn.className = 'model-picker-trigger';
    pickerBtn.type = 'button';
    pickerBtn.title = 'Browse models';
    pickerBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>';
    pickerBtn.addEventListener('click', function(e) {
      e.preventDefault();
      e.stopPropagation();
      openModelPicker(cfg.id, cfg.context);
    });

    sel.parentNode.insertBefore(pickerBtn, sel.nextSibling);
  });
}

// Model picker close handlers
document.getElementById('modelPickerClose').addEventListener('click', closeModelPicker);
document.getElementById('modelPickerOverlay').addEventListener('click', function(e) {
  if (e.target === document.getElementById('modelPickerOverlay')) closeModelPicker();
});
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape' && modelPickerState.isOpen) {
    e.preventDefault();
    closeModelPicker();
  }
});

// ── Benchmark: Sub-panel switching ──
document.querySelectorAll('.bench-panel-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.bench-panel-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.bench-view').forEach(v => v.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('bench-' + btn.dataset.bench).classList.add('active');
  });
});

// ── Benchmark: Model checkboxes ──
function buildModelCheckboxes() {
  const container = document.getElementById('benchModelChecks');
  container.innerHTML = '';
  const defaults = ['voyage-4-large', 'voyage-4', 'voyage-4-lite'];
  embedModels.forEach(m => {
    const label = document.createElement('label');
    label.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);';
    const cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.value = m.name;
    cb.checked = defaults.includes(m.name);
    cb.style.accentColor = 'var(--accent)';
    label.appendChild(cb);
    label.appendChild(document.createTextNode(m.name));
    container.appendChild(label);
  });
}

function populateBenchRankSelects() {
  const selA = document.getElementById('benchRankModelA');
  const selB = document.getElementById('benchRankModelB');
  [selA, selB].forEach(sel => {
    sel.innerHTML = '';
    [...embedModels, ...rerankModels].forEach(m => {
      const opt = document.createElement('option');
      opt.value = m.name;
      opt.textContent = m.name;
      sel.appendChild(opt);
    });
  });
  // Sensible defaults
  if (embedModels.length >= 2) {
    selA.value = embedModels[0].name;
    selB.value = embedModels[embedModels.length > 2 ? 2 : 1].name;
  }
}

// ── Benchmark: Latency ──
const BENCH_SAMPLE_TEXTS = [
  'MongoDB Atlas provides a fully managed cloud database service with built-in vector search capabilities.',
  'Kubernetes orchestrates containerized applications across clusters of machines for high availability.',
  'Machine learning models transform raw data into embeddings that capture semantic meaning.',
  'RESTful APIs use HTTP methods like GET, POST, PUT, and DELETE to manage resources.',
  'Natural language processing enables computers to understand and generate human language.',
];

const MODEL_COLORS = [
  '#00D4AA', '#5CE8CC', '#40E0FF', '#B45AF2', '#FFC010',
  '#FF6960', '#B45AF2', '#FFC010', '#0088CC', '#A8F0E0',
];

window.doBenchLatency = async function() {
  hideError('benchLatencyError');
  const checks = document.querySelectorAll('#benchModelChecks input:checked');
  const models = Array.from(checks).map(c => c.value);
  if (models.length === 0) { showError('benchLatencyError', 'Select at least one model'); return; }

  const rounds = parseInt(document.getElementById('benchRounds').value, 10);
  const customText = document.getElementById('benchLatencyInput').value.trim();
  const texts = customText ? [customText] : BENCH_SAMPLE_TEXTS;

  setLoading('benchLatencyBtn', true);
  document.getElementById('benchLatencyResult').classList.add('visible');

  const chart = document.getElementById('benchLatencyChart');
  const statsEl = document.getElementById('benchLatencyStats');

  // Build placeholder bars
  chart.innerHTML = '';
  models.forEach((model, i) => {
    const row = document.createElement('div');
    row.className = 'latency-row';
    row.innerHTML = `
      <span class="latency-model">${model}</span>
      <div class="latency-bar-outer">
        <div class="latency-bar-inner running" id="bench-bar-${i}" style="background:${MODEL_COLORS[i % MODEL_COLORS.length]}"></div>
      </div>
      <span class="latency-stats" id="bench-stats-${i}">Running...</span>
      <span class="latency-badge" id="bench-badge-${i}"></span>
    `;
    chart.appendChild(row);
  });
  statsEl.innerHTML = `<span class="stat"><span class="stat-label">Texts</span><span class="stat-value">${texts.length}</span></span>
    <span class="stat"><span class="stat-label">Rounds</span><span class="stat-value">${rounds}</span></span>`;

  const results = [];

  for (let mi = 0; mi < models.length; mi++) {
    const model = models[mi];
    const latencies = [];
    let tokens = 0, dims = 0;

    for (let r = 0; r < rounds; r++) {
      try {
        const data = await apiPost('/api/benchmark/embed', { texts, model, inputType: 'document' });
        latencies.push(data.elapsed);
        tokens = data.tokens;
        dims = data.dimensions;
        CostTracker.addOperation('bench-latency', model, data.tokens || 0);
      } catch (err) {
        document.getElementById(`bench-stats-${mi}`).textContent = 'Error';
        document.getElementById(`bench-bar-${mi}`).classList.remove('running');
        document.getElementById(`bench-bar-${mi}`).style.width = '0%';
        break;
      }
    }

    if (latencies.length > 0) {
      const avg = latencies.reduce((a, b) => a + b, 0) / latencies.length;
      const sorted = [...latencies].sort((a, b) => a - b);
      const p50 = sorted[Math.floor(sorted.length / 2)];
      results.push({ model, avg, p50, min: sorted[0], max: sorted[sorted.length - 1], tokens, dims, idx: mi });
    }
  }

  // Animate bars to final widths
  if (results.length > 0) {
    const maxAvg = Math.max(...results.map(r => r.avg));
    const minAvg = Math.min(...results.map(r => r.avg));

    results.forEach(r => {
      const bar = document.getElementById(`bench-bar-${r.idx}`);
      const stats = document.getElementById(`bench-stats-${r.idx}`);
      const badge = document.getElementById(`bench-badge-${r.idx}`);
      bar.classList.remove('running');
      const pct = Math.max(15, (r.avg / maxAvg) * 100);
      bar.style.width = pct + '%';
      bar.textContent = r.avg.toFixed(0) + 'ms';
      stats.textContent = `p50: ${r.p50.toFixed(0)}ms`;
      stats.title = `min: ${r.min.toFixed(0)}ms, max: ${r.max.toFixed(0)}ms`;
      if (r.avg === minAvg) badge.textContent = '⚡';
    });

    // Update stats
    const fastest = results.reduce((a, b) => a.avg < b.avg ? a : b);
    const price = getModelPrice(fastest.model);
    statsEl.innerHTML += `
      <span class="stat"><span class="stat-label">Fastest</span><span class="stat-value">${fastest.model}</span></span>
      <span class="stat"><span class="stat-label">Avg</span><span class="stat-value">${fastest.avg.toFixed(0)}ms</span></span>
      <span class="stat"><span class="stat-label">Dims</span><span class="stat-value">${fastest.dims}</span></span>
      ${price ? `<span class="stat"><span class="stat-label">Price</span><span class="stat-value">${price}</span></span>` : ''}
    `;

    // Save to history
    saveBenchHistory(results, texts.length, rounds);
    renderHistory();

    // Capture for export
    window._lastBenchmarkResults = {
      name: 'Latency Benchmark',
      date: new Date().toISOString(),
      results: results.map(r => ({
        model: r.model, avg_ms: Math.round(r.avg), p50_ms: Math.round(r.p50),
        min_ms: Math.round(r.min), max_ms: Math.round(r.max),
        dimensions: r.dims, tokens: r.tokens,
      })),
    };
    const benchBtn = document.getElementById('benchExportBtn');
    if (benchBtn) benchBtn.style.display = '';
  }

  setLoading('benchLatencyBtn', false);
};

function getModelPrice(name) {
  const m = allModels.find(m => m.name === name);
  return m ? m.price : null;
}

function getModelPriceNum(name) {
  const m = allModels.find(m => m.name === name);
  if (!m) return null;
  const match = m.price.match(/\$([0-9.]+)\/1M/);
  return match ? parseFloat(match[1]) : null;
}

// ── Benchmark: Ranking Comparison ──
window.doBenchRanking = async function() {
  hideError('benchRankError');
  const query = document.getElementById('benchRankQuery').value.trim();
  const docsText = document.getElementById('benchRankDocs').value.trim();
  if (!query || !docsText) { showError('benchRankError', 'Enter a query and documents'); return; }

  const documents = docsText.split('\n').map(d => d.trim()).filter(Boolean);
  if (documents.length < 2) { showError('benchRankError', 'Enter at least 2 documents'); return; }

  const modelA = document.getElementById('benchRankModelA').value;
  const modelB = document.getElementById('benchRankModelB').value;
  const mode = document.getElementById('benchRankMode').value;
  const topK = parseInt(document.getElementById('benchRankTopK').value, 10);

  setLoading('benchRankBtn', true);

  try {
    let rankedA, rankedB;

    if (mode === 'embed') {
      // Embedding similarity mode
      const [dataA, dataB] = await Promise.all([
        apiPost('/api/benchmark/embed', { texts: [query, ...documents], model: modelA, inputType: 'document' }),
        apiPost('/api/benchmark/embed', { texts: [query, ...documents], model: modelB, inputType: 'document' }),
      ]);

      rankedA = rankBySimilarity(dataA.embeddings, documents, topK);
      rankedB = rankBySimilarity(dataB.embeddings, documents, topK);
      CostTracker.addOperation('bench-ranking', modelA, dataA.tokens || 0);
      CostTracker.addOperation('bench-ranking', modelB, dataB.tokens || 0);
    } else {
      // Rerank mode
      const [dataA, dataB] = await Promise.all([
        apiPost('/api/benchmark/rerank', { query, documents, model: modelA, topK }),
        apiPost('/api/benchmark/rerank', { query, documents, model: modelB, topK }),
      ]);

      rankedA = dataA.results.slice(0, topK).map(r => ({
        index: r.index,
        text: documents[r.index],
        score: r.relevance_score,
      }));
      rankedB = dataB.results.slice(0, topK).map(r => ({
        index: r.index,
        text: documents[r.index],
        score: r.relevance_score,
      }));
      CostTracker.addOperation('bench-ranking', modelA, dataA.usage?.total_tokens || 0);
      CostTracker.addOperation('bench-ranking', modelB, dataB.usage?.total_tokens || 0);
    }

    // Render comparison
    renderRankComparison(modelA, modelB, rankedA, rankedB, topK);
    document.getElementById('benchRankResult').classList.add('visible');
  } catch (err) {
    showError('benchRankError', err.message);
  } finally {
    setLoading('benchRankBtn', false);
  }
};

function rankBySimilarity(embeddings, documents, topK) {
  const queryVec = embeddings[0];
  const docVecs = embeddings.slice(1);
  const scores = docVecs.map((dv, i) => ({
    index: i,
    text: documents[i],
    score: cosineSim(queryVec, dv),
  }));
  scores.sort((a, b) => b.score - a.score);
  return scores.slice(0, topK);
}

function renderRankComparison(modelA, modelB, rankedA, rankedB, topK) {
  const grid = document.getElementById('benchRankGrid');
  const verdict = document.getElementById('benchRankVerdict');
  grid.innerHTML = '';

  // Header
  const header = document.createElement('div');
  header.className = 'rank-row';
  header.style.background = 'none';
  header.style.fontWeight = '600';
  header.style.fontSize = '13px';
  header.style.color = 'var(--accent)';
  header.innerHTML = `<div></div><div>${modelA}</div><div></div><div>${modelB}</div>`;
  grid.appendChild(header);

  const orderA = rankedA.map(r => r.index);
  const orderB = rankedB.map(r => r.index);
  let matches = 0;

  const k = Math.min(topK, rankedA.length, rankedB.length);
  for (let i = 0; i < k; i++) {
    const a = rankedA[i];
    const b = rankedB[i];
    const same = a.index === b.index;
    if (same) matches++;

    const truncA = a.text.length > 60 ? a.text.slice(0, 57) + '...' : a.text;
    const truncB = b.text.length > 60 ? b.text.slice(0, 57) + '...' : b.text;

    const row = document.createElement('div');
    row.className = 'rank-row';
    row.innerHTML = `
      <div class="rank-num">${i + 1}</div>
      <div class="rank-item ${same ? 'rank-match' : 'rank-differ'}">
        <div title="${a.text.replace(/"/g, '&quot;')}">${truncA}</div>
        <div class="rank-score">${a.score.toFixed(4)} [doc ${a.index}]</div>
      </div>
      <div class="rank-arrow">${same ? '=' : '≠'}</div>
      <div class="rank-item ${same ? 'rank-match' : 'rank-differ'}">
        <div title="${b.text.replace(/"/g, '&quot;')}">${truncB}</div>
        <div class="rank-score">${b.score.toFixed(4)} [doc ${b.index}]</div>
      </div>
    `;
    grid.appendChild(row);
  }

  // Compute overlap
  const setA = new Set(orderA.slice(0, k));
  const setB = new Set(orderB.slice(0, k));
  const overlap = [...setA].filter(x => setB.has(x)).length;
  const overlapPct = ((overlap / k) * 100).toFixed(0);
  const agreePct = ((matches / k) * 100).toFixed(0);

  if (matches === k) {
    verdict.innerHTML = `<span style="color:var(--green)">✓ Models agree on all ${k} positions — the cheaper model is likely sufficient.</span>`;
  } else if (overlap === k) {
    verdict.innerHTML = `<span style="color:var(--yellow)">⚠ Same ${k} documents in top-${k}, but in different order (${agreePct}% exact match).</span>`;
  } else {
    verdict.innerHTML = `<span style="color:var(--warning)">⚠ ${overlapPct}% overlap in top-${k} results — models see different relevance signals.</span>`;
  }
}

// ── Benchmark: Quantization ──
function populateQuantModelSelect() {
  const sel = document.getElementById('quantModel');
  sel.innerHTML = '';
  embedModels.forEach(m => {
    const opt = document.createElement('option');
    opt.value = m.name;
    opt.textContent = m.name;
    sel.appendChild(opt);
  });
  // Default to voyage-4-large if available
  const preferred = embedModels.find(m => m.name === 'voyage-4-large');
  if (preferred) sel.value = preferred.name;
}

function hammingSimUI(a, b) {
  // For binary/ubinary packed embeddings, compute agreement via dot product
  let dot = 0;
  for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
  return dot;
}

window.doBenchQuantization = async function() {
  hideError('quantError');
  const model = document.getElementById('quantModel').value;
  const dimsVal = document.getElementById('quantDimensions').value;
  const dimensions = dimsVal ? parseInt(dimsVal, 10) : undefined;
  const query = document.getElementById('quantQuery').value.trim();
  const corpusText = document.getElementById('quantCorpus').value.trim();

  if (!query) { showError('quantError', 'Enter a query'); return; }
  if (!corpusText) { showError('quantError', 'Enter at least 2 documents'); return; }

  const corpus = corpusText.split('\n').map(d => d.trim()).filter(Boolean);
  if (corpus.length < 2) { showError('quantError', 'Enter at least 2 documents'); return; }

  const checks = document.querySelectorAll('#quantDtypeChecks input:checked');
  const dtypes = Array.from(checks).map(c => c.value);
  if (dtypes.length === 0) { showError('quantError', 'Select at least one data type'); return; }

  setLoading('quantBtn', true);

  try {
    const allTexts = [query, ...corpus];
    const resultsByDtype = {};

    for (const dtype of dtypes) {
      const body = { texts: allTexts, model, inputType: 'document' };
      if (dimensions) body.dimensions = dimensions;
      if (dtype !== 'float') body.output_dtype = dtype;

      const start = performance.now();
      const data = await apiPost('/api/embed', body);
      const elapsed = performance.now() - start;
      CostTracker.addOperation('bench-quantization', model, data.usage?.total_tokens || 0);

      const embeddings = data.data.map(d => d.embedding);
      const queryEmbed = embeddings[0];
      const dims = embeddings[0].length;
      const isBinary = (dtype === 'binary' || dtype === 'ubinary');

      // Rank corpus documents by similarity
      const ranked = corpus.map((text, i) => {
        const docEmbed = embeddings[i + 1];
        let sim;
        if (isBinary) {
          sim = hammingSimUI(queryEmbed, docEmbed);
        } else {
          sim = cosineSim(queryEmbed, docEmbed);
        }
        return { index: i, text, similarity: sim };
      }).sort((a, b) => b.similarity - a.similarity);

      // Calculate storage
      const actualDims = isBinary ? dims * 8 : dims;
      let bytesPerVec;
      if (dtype === 'float') bytesPerVec = dims * 4;
      else if (dtype === 'int8' || dtype === 'uint8') bytesPerVec = dims * 1;
      else bytesPerVec = dims; // binary/ubinary: dims is already 1/8th

      resultsByDtype[dtype] = {
        dtype, latency: elapsed, dims, actualDims, bytesPerVec,
        tokens: data.usage?.total_tokens || 0, ranked,
      };
    }

    const completed = Object.values(resultsByDtype);
    if (completed.length === 0) {
      showError('quantError', 'No data types completed successfully');
      return;
    }

    // ── Render Charts ──
    const baseline = completed.find(r => r.dtype === 'float') || completed[0];
    const maxBytes = Math.max(...completed.map(r => r.bytesPerVec));
    const maxLatency = Math.max(...completed.map(r => r.latency));
    const DTYPE_COLORS = { float: '#00D4AA', int8: '#5CE8CC', uint8: '#40E0FF', ubinary: '#FFC010', binary: '#FF6960' };

    // ── Storage Bar Chart ──
    let storageHTML = '';
    for (const r of completed) {
      const pct = Math.max(8, (r.bytesPerVec / maxBytes) * 100);
      const totalMB = (r.bytesPerVec * 1_000_000) / (1024 * 1024);
      const sizeStr = totalMB >= 1024 ? `${(totalMB / 1024).toFixed(1)} GB` : `${totalMB.toFixed(0)} MB`;
      const savings = r.bytesPerVec < baseline.bytesPerVec
        ? `${(baseline.bytesPerVec / r.bytesPerVec).toFixed(0)}× smaller`
        : 'baseline';
      const color = DTYPE_COLORS[r.dtype] || '#40E0FF';
      storageHTML += `<div class="quant-bar-group">
        <div class="quant-bar-label">
          <span class="dtype-name">${r.dtype}</span>
          <span class="dtype-value">${formatBytesUI(r.bytesPerVec)}/vec · ${sizeStr} @ 1M</span>
        </div>
        <div class="quant-bar-track">
          <div class="quant-bar-fill storage" style="width:${pct}%;background:linear-gradient(90deg, ${color}, ${color}cc);">${savings}</div>
        </div>
      </div>`;
    }
    document.getElementById('quantStorageChart').innerHTML = storageHTML;

    // ── Latency Bar Chart ──
    let latencyHTML = '';
    const minLatency = Math.min(...completed.map(r => r.latency));
    for (const r of completed) {
      const pct = Math.max(8, (r.latency / maxLatency) * 100);
      const color = DTYPE_COLORS[r.dtype] || '#40E0FF';
      const badge = r.latency === minLatency ? ' ⚡' : '';
      latencyHTML += `<div class="quant-bar-group">
        <div class="quant-bar-label">
          <span class="dtype-name">${r.dtype}</span>
          <span class="dtype-value">${r.latency.toFixed(0)}ms${badge}</span>
        </div>
        <div class="quant-bar-track">
          <div class="quant-bar-fill latency" style="width:${pct}%;background:linear-gradient(90deg, ${color}, ${color}cc);">${r.latency.toFixed(0)}ms</div>
        </div>
      </div>`;
    }
    document.getElementById('quantLatencyChart').innerHTML = latencyHTML;

    // ── Quality Meters + Ranking Grid ──
    const topK = Math.min(5, corpus.length);
    const metersEl = document.getElementById('quantQualityMeters');
    const gridEl = document.getElementById('quantRankGrid');
    gridEl.innerHTML = '';
    metersEl.innerHTML = '';

    if (completed.length >= 2 && baseline) {
      const baselineRanking = baseline.ranked.slice(0, topK).map(r => r.index);

      // Quality meters for each non-baseline dtype
      let metersHTML = '';
      for (const r of completed) {
        if (r.dtype === baseline.dtype) continue;
        const otherRanking = r.ranked.slice(0, topK).map(x => x.index);
        const overlap = baselineRanking.filter(idx => otherRanking.includes(idx)).length;
        const overlapPct = (overlap / topK) * 100;
        const exactMatch = baselineRanking.every((idx, pos) => otherRanking[pos] === idx);
        const positionMatches = baselineRanking.filter((idx, pos) => otherRanking[pos] === idx).length;
        const posMatchPct = (positionMatches / topK) * 100;

        let grade, gradeLabel, detail;
        if (exactMatch) {
          grade = 'perfect'; gradeLabel = '✓ Perfect';
          detail = `Identical ranking — all ${topK} positions match float baseline`;
        } else if (overlap === topK) {
          grade = 'good'; gradeLabel = '≈ Reordered';
          detail = `Same ${topK} documents, ${positionMatches}/${topK} in same position`;
        } else {
          grade = overlap >= topK * 0.6 ? 'good' : 'degraded';
          gradeLabel = `${overlapPct.toFixed(0)}% overlap`;
          detail = `${overlap}/${topK} documents match, ${positionMatches}/${topK} positions match`;
        }

        metersHTML += `<div class="quant-quality-meter">
          <div class="quant-meter-header">
            <span class="dtype-name">${r.dtype}</span>
            <span class="verdict-badge ${grade}">${gradeLabel}</span>
          </div>
          <div class="quant-meter-track">
            <div class="quant-meter-fill ${grade}" style="width:${exactMatch ? 100 : posMatchPct}%"></div>
          </div>
          <div class="quant-meter-detail">${detail}</div>
        </div>`;
      }
      metersEl.innerHTML = metersHTML;

      // Side-by-side ranking columns
      let rankHTML = `<div class="quant-rank-cols" style="grid-template-columns:repeat(${completed.length},1fr);">`;
      for (const r of completed) {
        rankHTML += `<div><div class="quant-rank-col-header">${r.dtype}${r === baseline ? ' (baseline)' : ''}</div>`;
        r.ranked.slice(0, topK).forEach((item, pos) => {
          const trunc = item.text.length > 55 ? item.text.slice(0, 52) + '…' : item.text;
          let cls = 'baseline';
          if (r !== baseline) {
            cls = (baseline.ranked[pos] && item.index === baseline.ranked[pos].index) ? 'match' : 'differ';
          }
          rankHTML += `<div class="quant-rank-item ${cls}" title="${item.text.replace(/"/g, '&quot;')}">
            <span class="quant-rank-pos">${pos + 1}</span>${trunc}
            <div class="quant-rank-score">${item.similarity.toFixed(4)} · doc ${item.index}</div>
          </div>`;
        });
        rankHTML += '</div>';
      }
      rankHTML += '</div>';
      gridEl.innerHTML = rankHTML;
    } else {
      metersEl.innerHTML = '<span style="color:var(--text-dim)">Select multiple data types (including float) to compare rankings.</span>';
    }

    document.getElementById('quantResult').classList.add('visible');
  } catch (err) {
    showError('quantError', err.message);
  } finally {
    setLoading('quantBtn', false);
  }
};

// ── Benchmark: Cost Calculator ──
// ── Cost Calculator: Shared Helpers ──

function costFormatDollars(n) {
  if (n === 0) return '$0.00';
  if (n < 0.01 && n > 0) return '$' + n.toFixed(4);
  if (n < 1) return '$' + n.toFixed(2);
  return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function costShortNum(n) {
  if (n >= 1e9) return (n / 1e9).toFixed(n % 1e9 === 0 ? 0 : 1) + 'B';
  if (n >= 1e6) return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1) + 'K';
  return String(n);
}

function costGetPricePerM(model) {
  const match = model.price.match(/\$([0-9.]+)\/1M/);
  return match ? parseFloat(match[1]) : null;
}

function costGetV4Models() {
  return allModels.filter(m => !m.legacy && !m.unreleased && costGetPricePerM(m) !== null);
}

// ── Cost Mode Toggle ──

let currentCostMode = 'simple';

function setCostMode(mode) {
  currentCostMode = mode;
  document.querySelectorAll('.cost-mode-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.mode === mode);
  });
  document.getElementById('costSimpleMode').style.display = mode === 'simple' ? '' : 'none';
  document.getElementById('costRagMode').style.display = mode === 'rag' ? '' : 'none';
  if (mode === 'rag') updateRagCalculator();
}

// ── Simple Mode (query-only comparison) ──

function initCostCalculator() {
  // Mode toggle buttons
  document.getElementById('costModeSimple').addEventListener('click', () => setCostMode('simple'));
  document.getElementById('costModeRag').addEventListener('click', () => setCostMode('rag'));

  // Help modal
  const helpModal = document.getElementById('costHelpModal');
  document.getElementById('costHelpBtn').addEventListener('click', () => helpModal.classList.add('open'));
  document.getElementById('costHelpClose').addEventListener('click', () => helpModal.classList.remove('open'));
  helpModal.addEventListener('click', (e) => { if (e.target === helpModal) helpModal.classList.remove('open'); });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      helpModal.classList.remove('open');
      closeExploreModal();
    }
  });

  // Simple mode sliders
  const tokSlider = document.getElementById('costTokens');
  const qSlider = document.getElementById('costQueries');
  const tokValue = document.getElementById('costTokensValue');
  const qValue = document.getElementById('costQueriesValue');

  function updateCost() {
    const tokens = parseInt(tokSlider.value, 10);
    const queries = parseInt(qSlider.value, 10);
    tokValue.textContent = tokens.toLocaleString();
    qValue.textContent = queries.toLocaleString();
    renderCostTable(tokens, queries);
  }

  tokSlider.addEventListener('input', updateCost);
  qSlider.addEventListener('input', updateCost);
  updateCost();

  // RAG mode init
  initRagCalculator();
}

function renderCostTable(tokensPerQuery, queriesPerDay) {
  const tbody = document.getElementById('costTableBody');
  tbody.innerHTML = '';

  const models = allModels.filter(m => !m.legacy && !m.unreleased);
  const rows = [];

  models.forEach(m => {
    const pricePerM = costGetPricePerM(m);
    if (pricePerM === null) return;
    const dailyTokens = tokensPerQuery * queriesPerDay;
    const dailyCost = (dailyTokens / 1_000_000) * pricePerM;
    const monthlyCost = dailyCost * 30;
    rows.push({ name: m.name, type: m.type === 'embedding' ? 'embed' : 'rerank', pricePerM, dailyCost, monthlyCost });
  });

  rows.sort((a, b) => a.monthlyCost - b.monthlyCost);
  const maxMonthly = Math.max(...rows.map(r => r.monthlyCost), 0.01);

  rows.forEach(r => {
    const tr = document.createElement('tr');
    const barPct = Math.max(2, (r.monthlyCost / maxMonthly) * 100);
    const monthlyStr = r.monthlyCost < 0.01 ? '<$0.01' : '$' + (r.monthlyCost < 1 ? r.monthlyCost.toFixed(2) : r.monthlyCost < 100 ? r.monthlyCost.toFixed(1) : r.monthlyCost.toFixed(0));
    const dailyStr = r.dailyCost < 0.01 ? '<$0.01' : '$' + r.dailyCost.toFixed(2);

    tr.innerHTML = `
      <td style="color:var(--text)">${r.name}</td>
      <td style="color:var(--text-dim)">${r.type}</td>
      <td>$${r.pricePerM.toFixed(2)}</td>
      <td>${dailyStr}</td>
      <td class="cost-highlight">${monthlyStr}</td>
      <td class="cost-bar-cell" style="position:relative;padding-left:8px;">
        <div class="cost-bar" style="width:${barPct}%;"></div>
        <span style="position:relative;z-index:1;font-size:12px;color:var(--text-dim);">${monthlyStr}</span>
      </td>
    `;
    tbody.appendChild(tr);
  });
}

// ── RAG Planner Mode (full TCO with strategies) ──

function initRagCalculator() {
  // Populate model dropdowns
  const embeddingModels = allModels.filter(m =>
    m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null
  );

  const docSelect = document.getElementById('ragDocModel');
  const querySelect = document.getElementById('ragQueryModel');

  embeddingModels.forEach(m => {
    const pricePerM = costGetPricePerM(m);
    const opt1 = document.createElement('option');
    opt1.value = m.name;
    opt1.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
    docSelect.appendChild(opt1);

    const opt2 = document.createElement('option');
    opt2.value = m.name;
    opt2.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
    querySelect.appendChild(opt2);
  });

  // Set defaults: voyage-4-large for docs, voyage-4-lite for queries
  docSelect.value = 'voyage-4-large';
  querySelect.value = 'voyage-4-lite';

  // Bind all sliders and selects
  const ids = ['ragDocs', 'ragDocTokens', 'ragQueries', 'ragQueryTokens', 'ragMonths'];
  ids.forEach(id => {
    document.getElementById(id).addEventListener('input', updateRagCalculator);
  });
  docSelect.addEventListener('change', updateRagCalculator);
  querySelect.addEventListener('change', updateRagCalculator);

  updateRagCalculator();
}

function updateRagCalculator() {
  const numDocs = parseInt(document.getElementById('ragDocs').value, 10);
  const docTokens = parseInt(document.getElementById('ragDocTokens').value, 10);
  const numQueries = parseInt(document.getElementById('ragQueries').value, 10);
  const queryTokens = parseInt(document.getElementById('ragQueryTokens').value, 10);
  const months = parseInt(document.getElementById('ragMonths').value, 10);
  const docModelName = document.getElementById('ragDocModel').value;
  const queryModelName = document.getElementById('ragQueryModel').value;

  // Update slider display values
  document.getElementById('ragDocsValue').textContent = costShortNum(numDocs);
  document.getElementById('ragDocTokensValue').textContent = docTokens.toLocaleString();
  document.getElementById('ragQueriesValue').textContent = costShortNum(numQueries);
  document.getElementById('ragQueryTokensValue').textContent = queryTokens.toLocaleString();
  document.getElementById('ragMonthsValue').textContent = months + ' mo';

  const docTotalTokens = numDocs * docTokens;
  const queryTotalTokensPerMonth = numQueries * queryTokens;

  // Get model prices
  const docModel = allModels.find(m => m.name === docModelName);
  const queryModel = allModels.find(m => m.name === queryModelName);
  const docPrice = docModel ? costGetPricePerM(docModel) : 0;
  const queryPrice = queryModel ? costGetPricePerM(queryModel) : 0;

  // ── Build strategies (same logic as CLI) ──
  const strategies = [];
  const v4Embedding = allModels.filter(m =>
    m.type === 'embedding' && !m.legacy && !m.unreleased &&
    (m.sharedSpace === 'voyage-4' || m.name.startsWith('voyage-4')) &&
    costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
  );

  // Strategy group 1: Symmetric with each V4 model
  v4Embedding.forEach(m => {
    const price = costGetPricePerM(m);
    const docCost = (docTotalTokens / 1e6) * price;
    const queryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * price;
    const totalCost = docCost + (queryCostPerMonth * months);
    strategies.push({
      name: `Symmetric: ${m.name}`,
      type: 'symmetric',
      docModel: m.name,
      queryModel: m.name,
      docCost,
      queryCostPerMonth,
      totalCost,
      months,
    });
  });

  // Strategy 2: Asymmetric — user-selected combo
  const asymDocCost = (docTotalTokens / 1e6) * docPrice;
  const asymQueryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * queryPrice;
  const asymTotalCost = asymDocCost + (asymQueryCostPerMonth * months);

  // Only add if it's actually asymmetric (different models)
  if (docModelName !== queryModelName) {
    strategies.push({
      name: `Asymmetric: ${docModelName} + ${queryModelName}`,
      type: 'asymmetric',
      docModel: docModelName,
      queryModel: queryModelName,
      docCost: asymDocCost,
      queryCostPerMonth: asymQueryCostPerMonth,
      totalCost: asymTotalCost,
      months,
      recommended: true,
    });
  }

  // Strategy 3: Asymmetric with nano (local, free queries)
  strategies.push({
    name: `Asymmetric: ${docModelName} + nano (local)`,
    type: 'asymmetric-local',
    docModel: docModelName,
    queryModel: 'voyage-4-nano',
    docCost: asymDocCost,
    queryCostPerMonth: 0,
    totalCost: asymDocCost,
    months,
    localNote: 'Query cost = $0 (runs locally via HuggingFace)',
  });

  strategies.sort((a, b) => a.totalCost - b.totalCost);
  const maxCost = Math.max(...strategies.map(s => s.totalCost), 0.01);

  // ── Render summary cards ──
  const summaryEl = document.getElementById('ragSummary');
  summaryEl.innerHTML = `
    <div class="cost-summary-card">
      <div class="cost-summary-label">Document tokens</div>
      <div class="cost-summary-value">${costShortNum(docTotalTokens)}</div>
      <div class="cost-summary-detail">${costShortNum(numDocs)} docs × ${docTokens.toLocaleString()} tok</div>
    </div>
    <div class="cost-summary-card">
      <div class="cost-summary-label">Query tokens / mo</div>
      <div class="cost-summary-value">${costShortNum(queryTotalTokensPerMonth)}</div>
      <div class="cost-summary-detail">${costShortNum(numQueries)} queries × ${queryTokens} tok</div>
    </div>
    <div class="cost-summary-card">
      <div class="cost-summary-label">Best ${months}-mo total</div>
      <div class="cost-summary-value">${costFormatDollars(strategies[0].totalCost)}</div>
      <div class="cost-summary-detail">${strategies[0].name}</div>
    </div>
    <div class="cost-summary-card">
      <div class="cost-summary-label">Max potential savings</div>
      <div class="cost-summary-value" style="color:var(--success)">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
      <div class="cost-summary-detail">vs ${strategies[strategies.length - 1].name.split(':')[1]?.trim() || 'most expensive'}</div>
    </div>
  `;

  // ── Render strategy cards ──
  const stratEl = document.getElementById('ragStrategies');
  stratEl.innerHTML = strategies.map(s => {
    const savings = maxCost > 0 ? ((1 - s.totalCost / maxCost) * 100) : 0;
    const savingsHtml = savings > 0 ? `<div class="cost-savings">↓ ${savings.toFixed(0)}% savings</div>` : '';
    const badgeHtml = s.recommended ? '<div class="cost-strategy-badge">★ Recommended</div>' : '';
    const localHtml = s.localNote ? `<div style="font-size:11px;color:var(--text-muted);margin-top:6px;">⚡ ${s.localNote}</div>` : '';

    return `
      <div class="cost-strategy${s.recommended ? ' recommended' : ''}">
        ${badgeHtml}
        <div class="cost-strategy-name">${s.name}</div>
        <div class="cost-strategy-row">
          <span class="cost-strategy-row-label">Doc embedding</span>
          <span class="cost-strategy-row-value">${costFormatDollars(s.docCost)} <span style="color:var(--text-muted);font-size:11px">(one-time)</span></span>
        </div>
        <div class="cost-strategy-row">
          <span class="cost-strategy-row-label">Query cost</span>
          <span class="cost-strategy-row-value">${costFormatDollars(s.queryCostPerMonth)}/mo</span>
        </div>
        <div class="cost-strategy-total">
          <span class="cost-strategy-total-label">${s.months}-month total</span>
          <span class="cost-strategy-total-value">${costFormatDollars(s.totalCost)}</span>
        </div>
        ${savingsHtml}
        ${localHtml}
      </div>
    `;
  }).join('');

  // ── Render per-model table ──
  const ragTbody = document.getElementById('ragTableBody');
  ragTbody.innerHTML = '';

  const allEmbedding = allModels.filter(m =>
    m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
  );

  const tableRows = allEmbedding.map(m => {
    const price = costGetPricePerM(m);
    const dCost = (docTotalTokens / 1e6) * price;
    const qCostMo = (queryTotalTokensPerMonth / 1e6) * price;
    const total = dCost + (qCostMo * months);
    return { name: m.name, dCost, qCostMo, total };
  }).sort((a, b) => a.total - b.total);

  const maxTable = Math.max(...tableRows.map(r => r.total), 0.01);

  tableRows.forEach(r => {
    const tr = document.createElement('tr');
    const barPct = Math.max(2, (r.total / maxTable) * 100);
    tr.innerHTML = `
      <td style="color:var(--text)">${r.name}</td>
      <td>${costFormatDollars(r.dCost)}</td>
      <td>${costFormatDollars(r.qCostMo)}</td>
      <td class="cost-highlight">${costFormatDollars(r.total)}</td>
      <td class="cost-bar-cell" style="position:relative;padding-left:8px;">
        <div class="cost-bar" style="width:${barPct}%;"></div>
        <span style="position:relative;z-index:1;font-size:12px;color:var(--text-dim);">${costFormatDollars(r.total)}</span>
      </td>
    `;
    ragTbody.appendChild(tr);
  });

  // ── Tip ──
  const bestSym = strategies.find(s => s.type === 'symmetric' && s.docModel === 'voyage-4-large');
  const bestAsym = strategies.find(s => s.recommended);
  const tipEl = document.getElementById('ragTip');
  if (bestSym && bestAsym && bestSym.totalCost > bestAsym.totalCost) {
    const saved = bestSym.totalCost - bestAsym.totalCost;
    const pct = ((saved / bestSym.totalCost) * 100).toFixed(0);
    tipEl.innerHTML = `💡 <strong>Asymmetric retrieval saves ${costFormatDollars(saved)} (${pct}%)</strong> over symmetric voyage-4-large — same document quality, lower query costs. The shared embedding space makes this possible. <code>vai explain shared-space</code>`;
  } else {
    tipEl.innerHTML = '💡 Try selecting different doc and query models to see asymmetric cost savings. <code>vai explain shared-space</code>';
  }
}

// ── Benchmark: History ──
const HISTORY_KEY = 'vai-bench-history';

function saveBenchHistory(results, textCount, rounds) {
  const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
  history.push({
    timestamp: Date.now(),
    textCount,
    rounds,
    results: results.map(r => ({ model: r.model, avg: r.avg, p50: r.p50, dims: r.dims })),
  });
  // Keep last 20
  if (history.length > 20) history.splice(0, history.length - 20);
  localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
}

function renderHistory() {
  const container = document.getElementById('benchHistoryContent');
  const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');

  if (history.length === 0) {
    container.innerHTML = '<div class="history-empty">No benchmarks recorded yet. Run a latency benchmark to start tracking.</div>';
    return;
  }

  // Collect all models that appear
  const modelSet = new Set();
  history.forEach(h => h.results.forEach(r => modelSet.add(r.model)));
  const models = [...modelSet];
  const colorMap = {};
  models.forEach((m, i) => { colorMap[m] = MODEL_COLORS[i % MODEL_COLORS.length]; });

  // Find global max for scale
  const maxAvg = Math.max(...history.flatMap(h => h.results.map(r => r.avg)));

  let html = '<div class="history-chart">';
  history.forEach((h, hi) => {
    html += '<div class="history-bar-group" title="' + new Date(h.timestamp).toLocaleString() + '">';
    models.forEach(m => {
      const r = h.results.find(r => r.model === m);
      const height = r ? Math.max(4, (r.avg / maxAvg) * 100) : 0;
      const label = r ? `${m}: ${r.avg.toFixed(0)}ms` : '';
      html += `<div class="history-bar" style="height:${height}%;background:${colorMap[m]};" title="${label}"></div>`;
    });
    html += '</div>';
  });
  html += '</div>';

  // Legend
  html += '<div class="history-legend">';
  models.forEach(m => {
    html += `<span><span class="history-legend-dot" style="background:${colorMap[m]}"></span>${m}</span>`;
  });
  html += '</div>';

  // Time labels
  if (history.length > 1) {
    const first = new Date(history[0].timestamp);
    const last = new Date(history[history.length - 1].timestamp);
    html += `<div class="history-labels"><span>${first.toLocaleDateString()} ${first.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</span><span>${last.toLocaleDateString()} ${last.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</span></div>`;
  }

  container.innerHTML = html;
}

window.clearHistory = function() {
  localStorage.removeItem(HISTORY_KEY);
  renderHistory();
};

// ── Settings ──
const SETTINGS_KEY = 'vai-settings';

function getSettings() {
  try {
    return JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
  } catch { return {}; }
}

function saveSetting(key, value) {
  const s = getSettings();
  s[key] = value;
  localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
  flashSaved();
}

function flashSaved() {
  const el = document.getElementById('settingsSavedMsg');
  if (!el) return;
  el.classList.add('show');
  clearTimeout(el._timer);
  el._timer = setTimeout(() => el.classList.remove('show'), 1500);
}

function initSettings() {
  const s = getSettings();

  // Theme sync
  const themeSelect = document.getElementById('settingsTheme');
  const currentTheme = localStorage.getItem('vai-theme') || 'dark';
  if (themeSelect) {
    themeSelect.value = currentTheme;
    themeSelect.addEventListener('change', () => {
      const t = themeSelect.value;
      saveSetting('theme', t);
      localStorage.setItem('vai-theme', t);
      // Reuse existing applyTheme by clicking the toggle if needed
      const toggle = document.getElementById('themeToggle');
      const cur = localStorage.getItem('vai-theme');
      if (cur === 'light') {
        document.documentElement.setAttribute('data-theme', 'light');
        if (toggle) { toggle.textContent = '☀️'; toggle.title = 'Switch to dark mode'; }
      } else {
        document.documentElement.removeAttribute('data-theme');
        if (toggle) { toggle.textContent = '🌙'; toggle.title = 'Switch to light mode'; }
      }
      const logo = document.getElementById('sidebarLogo');
      if (logo) logo.src = '/icons/' + (t === 'light' ? 'light' : 'dark') + '/64.png';
    });
  }

  // Heatmap palette — handled below with preview swatch

  // Default model — populate from global model select
  const settingsModel = document.getElementById('settingsDefaultModel');
  const globalModel = document.getElementById('globalModel');
  if (settingsModel && globalModel) {
    // Clone options from globalModel
    settingsModel.innerHTML = globalModel.innerHTML;
    const savedModel = s.defaultModel || globalModel.value;
    settingsModel.value = savedModel;
    settingsModel.addEventListener('change', () => {
      saveSetting('defaultModel', settingsModel.value);
      globalModel.value = settingsModel.value;
      // Update individual tab selects too
      ['embedModel', 'compareModel', 'searchEmbedModel'].forEach(id => {
        const sel = document.getElementById(id);
        if (sel) sel.value = settingsModel.value;
      });
    });
  }

  // Default input type
  const inputTypeSel = document.getElementById('settingsInputType');
  if (inputTypeSel) {
    inputTypeSel.value = s.inputType || 'document';
    inputTypeSel.addEventListener('change', () => {
      saveSetting('inputType', inputTypeSel.value);
      const embedType = document.getElementById('embedInputType');
      if (embedType) embedType.value = inputTypeSel.value;
    });
  }

  // API base URL
  const apiBaseInput = document.getElementById('settingsApiBase');
  if (apiBaseInput) {
    apiBaseInput.value = s.apiBase || '';
    apiBaseInput.addEventListener('change', () => saveSetting('apiBase', apiBaseInput.value));
  }

  // Timeout
  const timeoutSel = document.getElementById('settingsTimeout');
  if (timeoutSel) {
    timeoutSel.value = s.timeout || '30';
    timeoutSel.addEventListener('change', () => saveSetting('timeout', timeoutSel.value));
  }

  // Default Tab
  const defaultTabSel = document.getElementById('settingsDefaultTab');
  if (defaultTabSel) {
    defaultTabSel.value = s.defaultTab || 'home';
    defaultTabSel.addEventListener('change', () => {
      saveSetting('defaultTab', defaultTabSel.value);
    });
  }

  // Benchmark iterations
  const benchIterSel = document.getElementById('settingsBenchIter');
  if (benchIterSel) {
    benchIterSel.value = s.benchIter || '5';
    benchIterSel.addEventListener('change', () => saveSetting('benchIter', benchIterSel.value));
  }

  // Toggle buttons
  function initToggle(id, settingKey, defaultVal) {
    const btn = document.getElementById(id);
    if (!btn) return;
    const val = s[settingKey] !== undefined ? s[settingKey] : defaultVal;
    btn.classList.toggle('active', val);
    btn.addEventListener('click', () => {
      const isActive = btn.classList.toggle('active');
      saveSetting(settingKey, isActive);
    });
  }
  initToggle('settingsDetailedTimings', 'detailedTimings', false);
  initToggle('settingsCacheEmbeddings', 'cacheEmbeddings', true);

  // API Key (Electron only — uses safeStorage via preload bridge)
  const apiKeyInput = document.getElementById('settingsApiKey');
  const apiKeyToggle = document.getElementById('settingsApiKeyToggle');
  const apiKeySave = document.getElementById('settingsApiKeySave');
  const apiKeyMsg = document.getElementById('settingsApiKeyMsg');
  const apiBanner = document.getElementById('settingsApiBanner');
  const apiBannerIcon = document.getElementById('settingsApiBannerIcon');

  function updateApiBanner(hasKey, key) {
    if (hasKey) {
      apiBanner.className = 'settings-api-banner success';
      apiBannerIcon.textContent = '🔐';
      apiKeyMsg.textContent = 'API key stored securely in OS keychain';
      apiKeyInput.placeholder = window.vai ? window.vai.apiKey.mask(key) : '••••••••';
    } else {
      apiBanner.className = 'settings-api-banner';
      apiBannerIcon.textContent = '⚠️';
      apiKeyMsg.textContent = 'No API key configured — add your key below to get started.';
      apiKeyInput.placeholder = 'pa-...';
    }
  }

  if (window.vai && window.vai.isElectron) {
    // Load existing key status
    window.vai.apiKey.get().then(key => updateApiBanner(!!key, key));

    // Show/hide toggle
    let keyVisible = false;
    apiKeyToggle.addEventListener('click', async () => {
      if (!keyVisible && !apiKeyInput.value) {
        const key = await window.vai.apiKey.get();
        if (key) {
          apiKeyInput.value = key;
          apiKeyInput.type = 'text';
          apiKeyToggle.textContent = '🙈';
          keyVisible = true;
        }
      } else if (keyVisible) {
        apiKeyInput.type = 'password';
        apiKeyToggle.textContent = '👁';
        keyVisible = false;
        if (!apiKeyInput.value) {
          const key = await window.vai.apiKey.get();
          if (key) apiKeyInput.placeholder = window.vai.apiKey.mask(key);
        }
      } else {
        apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
        apiKeyToggle.textContent = apiKeyInput.type === 'password' ? '👁' : '🙈';
        keyVisible = apiKeyInput.type === 'text';
      }
    });

    // Save button
    apiKeySave.addEventListener('click', async () => {
      const key = apiKeyInput.value.trim();
      if (!key) {
        await window.vai.apiKey.delete();
        updateApiBanner(false);
        flashSaved();
        return;
      }
      try {
        await window.vai.apiKey.set(key);
        apiKeyInput.value = '';
        apiKeyInput.type = 'password';
        apiKeyToggle.textContent = '👁';
        keyVisible = false;
        updateApiBanner(true, key);
        flashSaved();
        loadConfig();
      } catch (err) {
        apiBanner.className = 'settings-api-banner';
        apiBannerIcon.textContent = '❌';
        apiKeyMsg.textContent = 'Failed to save: ' + err.message;
      }
    });

    apiKeyInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') apiKeySave.click();
    });
  } else {
    apiBanner.className = 'settings-api-banner';
    apiBannerIcon.textContent = '💡';
    apiKeyMsg.textContent = 'Set VOYAGE_API_KEY environment variable, or use "vai config set key YOUR_KEY"';
    apiKeySave.style.display = 'none';
    apiKeyToggle.style.display = 'none';
    apiKeyInput.disabled = true;
    apiKeyInput.placeholder = 'Secure storage available in desktop app';
  }

  // Heatmap palette preview
  const HEATMAP_GRADIENTS = {
    viridis:  'linear-gradient(90deg, #440154, #31688e, #35b779, #fde725)',
    plasma:   'linear-gradient(90deg, #0d0887, #7e03a8, #cc4778, #f89540, #f0f921)',
    inferno:  'linear-gradient(90deg, #000004, #420a68, #932667, #dd513a, #fca50a, #fcffa4)',
    magma:    'linear-gradient(90deg, #000004, #3b0f70, #8c2981, #de4968, #fea16e, #fcfdbf)',
    cividis:  'linear-gradient(90deg, #00224e, #123570, #3b496c, #575d6d, #707173, #a5a88f, #fee838)',
    turbo:    'linear-gradient(90deg, #30123b, #4662d7, #36aaf9, #1ae4b6, #72fe5e, #c8ef34, #faba39, #f66b19, #d93806, #7a0403)',
  };
  const heatmapSel = document.getElementById('settingsHeatmap');
  const heatmapPreview = document.getElementById('heatmapPreview');
  function updateHeatmapPreview() {
    if (heatmapPreview && heatmapSel) {
      heatmapPreview.style.background = HEATMAP_GRADIENTS[heatmapSel.value] || HEATMAP_GRADIENTS.viridis;
    }
  }
  if (heatmapSel) {
    heatmapSel.value = s.heatmap || 'viridis';
    heatmapSel.addEventListener('change', () => {
      saveSetting('heatmap', heatmapSel.value);
      updateHeatmapPreview();
    });
    updateHeatmapPreview();
  }

  // Clear cache
  const clearBtn = document.getElementById('settingsClearCache');
  if (clearBtn) {
    clearBtn.addEventListener('click', () => {
      if (confirm('Clear all cached data and reset settings to defaults?')) {
        localStorage.removeItem(SETTINGS_KEY);
        localStorage.removeItem('vai-theme');
        localStorage.removeItem('vai-embed-cache');
        location.reload();
      }
    });
  }

  // Apply saved defaults on load
  if (s.defaultModel && globalModel) {
    globalModel.value = s.defaultModel;
  }
  if (s.inputType) {
    const embedType = document.getElementById('embedInputType');
    if (embedType) embedType.value = s.inputType;
  }

  // ── Chat settings event listeners (fields now live in settings page) ──

  // Rerank toggle now handled by inline onclick="toggleRerank()" on the button HTML

  // Immediate save on select/toggle changes
  const chatProvider = document.getElementById('chatProvider');
  if (chatProvider) {
    chatProvider.addEventListener('change', () => {
      // chatProviderChanged() is called via onchange attr — save after a tick
      setTimeout(saveChatSettings, 100);
    });
  }
  const chatModel = document.getElementById('chatModel');
  if (chatModel) chatModel.addEventListener('change', saveChatSettings);
  const chatMode = document.getElementById('chatMode');
  if (chatMode) chatMode.addEventListener('change', () => {
    saveChatSettings();
    if (chatMode.value === 'agent') showAgentInfoModal();
  });
  const chatMaxDocs = document.getElementById('chatMaxDocs');
  if (chatMaxDocs) chatMaxDocs.addEventListener('change', saveChatSettings);

  // Debounced save on text input changes
  const chatDb = document.getElementById('chatDb');
  if (chatDb) chatDb.addEventListener('input', () => { updateChatStatus(); saveChatSettingsDebounced(); });
  const chatCollection = document.getElementById('chatCollection');
  if (chatCollection) chatCollection.addEventListener('input', () => { updateChatStatus(); saveChatSettingsDebounced(); });
  const kbSystemPrompt = document.getElementById('kbSystemPrompt');
  if (kbSystemPrompt) kbSystemPrompt.addEventListener('input', saveChatSettingsDebounced);

  // API key input listeners
  const chatApiKey = document.getElementById('chatApiKey');
  if (chatApiKey) {
    // Mark as modified when user types
    chatApiKey.addEventListener('input', () => {
      delete chatApiKey.dataset.isMasked;
      delete chatApiKey.dataset.actualKey;
    });
    // Save on Enter key
    chatApiKey.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        saveChatApiKey();
      }
    });
  }

  // ── Database settings (read/write ~/.vai/config.json via /api/settings) ──
  const dbSettingsFields = {
    'settingsMongoUri': 'mongodb-uri',
    'settingsDefaultDb': 'default-db',
    'settingsDefaultCollection': 'default-collection',
  };

  // Load current values from ~/.vai/config.json
  fetch('/api/settings').then(r => r.json()).then(settings => {
    for (const [elemId, cliKey] of Object.entries(dbSettingsFields)) {
      const el = document.getElementById(elemId);
      if (el && settings[cliKey] && settings[cliKey].isSet) {
        // Don't populate masked secret values into editable fields
        if (settings[cliKey].isSecret) {
          el.value = settings[cliKey].value || '';
          el.dataset.isMasked = 'true';
        } else {
          el.value = settings[cliKey].value || '';
        }
      }
    }
  }).catch(() => {});

  // Save non-secret fields on change (default-db, default-collection)
  ['settingsDefaultDb', 'settingsDefaultCollection'].forEach(elemId => {
    const el = document.getElementById(elemId);
    if (el) {
      let debounce;
      el.addEventListener('input', () => {
        clearTimeout(debounce);
        debounce = setTimeout(() => {
          const cliKey = dbSettingsFields[elemId];
          fetch('/api/settings', {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ [cliKey]: el.value || null }),
          }).then(() => flashSaved()).catch(() => {});
        }, 600);
      });
    }
  });

  // MongoDB URI — save button (secret field, same pattern as API key)
  const mongoUriInput = document.getElementById('settingsMongoUri');
  const mongoUriToggle = document.getElementById('settingsMongoUriToggle');
  const mongoUriSave = document.getElementById('settingsMongoUriSave');
  if (mongoUriInput && mongoUriSave) {
    let mongoUriVisible = false;
    if (mongoUriToggle) {
      mongoUriToggle.addEventListener('click', async () => {
        if (!mongoUriVisible) {
          // Fetch the real unmasked value
          try {
            const resp = await fetch('/api/settings/reveal/mongodb-uri');
            const data = await resp.json();
            if (data.value) {
              mongoUriInput.value = data.value;
              delete mongoUriInput.dataset.isMasked;
            }
          } catch {}
          mongoUriInput.type = 'text';
          mongoUriVisible = true;
        } else {
          mongoUriInput.type = 'password';
          mongoUriVisible = false;
        }
      });
    }
    mongoUriSave.addEventListener('click', () => {
      // Don't save masked values back
      if (mongoUriInput.dataset.isMasked === 'true') return;
      const val = mongoUriInput.value.trim();
      fetch('/api/settings', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 'mongodb-uri': val || null }),
      }).then(() => {
        flashSaved();
        if (val) {
          mongoUriInput.value = '';
          mongoUriInput.type = 'password';
          mongoUriVisible = false;
          mongoUriInput.placeholder = val.slice(0, 20) + '...';
        }
      }).catch(() => {});
    });
    mongoUriInput.addEventListener('input', () => {
      delete mongoUriInput.dataset.isMasked;
    });
    mongoUriInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') mongoUriSave.click();
    });
  }

  // Set up settings sub-navigation
  setupSettingsNav();

  // Set up doctor health check button
  setupDoctorPanel();
}

// ── Doctor Health Check ──
function setupDoctorPanel() {
  const runBtn = document.getElementById('doctorRunBtn');
  if (!runBtn) return;
  runBtn.addEventListener('click', runDoctorCheck);
}

async function runDoctorCheck() {
  const btn = document.getElementById('doctorRunBtn');
  const loading = document.getElementById('doctorLoading');
  const results = document.getElementById('doctorResults');
  const empty = document.getElementById('doctorEmpty');
  const checkList = document.getElementById('doctorCheckList');
  const summary = document.getElementById('doctorSummary');

  btn.disabled = true;
  btn.textContent = 'Running...';
  empty.style.display = 'none';
  results.style.display = 'none';
  loading.style.display = 'block';

  try {
    const res = await fetch('/api/doctor');
    const data = await res.json();

    checkList.innerHTML = '';
    let hasError = false;
    let hasWarning = false;

    for (const [key, check] of Object.entries(data)) {
      let icon, badgeClass, badgeText;
      if (check.ok === true) {
        icon = '&#x2713;';
        badgeClass = 'pass';
        badgeText = 'pass';
      } else if (check.ok === false) {
        icon = '&#x2717;';
        if (check.required) { hasError = true; badgeClass = 'fail'; badgeText = 'fail'; }
        else { hasWarning = true; badgeClass = 'warn'; badgeText = 'warn'; }
      } else {
        icon = '&#x26A0;';
        hasWarning = true;
        badgeClass = 'optional';
        badgeText = 'optional';
      }

      const el = document.createElement('div');
      el.className = 'doctor-check';
      el.innerHTML = `
        <span class="doctor-check-icon" style="color:var(--${badgeClass === 'pass' ? 'success' : badgeClass === 'fail' ? 'error' : 'warning'})">${icon}</span>
        <div class="doctor-check-body">
          <div class="doctor-check-name">${check.name}</div>
          <div class="doctor-check-msg">${check.message || ''}</div>
          ${check.hint ? `<div class="doctor-check-hint">${check.hint}</div>` : ''}
        </div>
        <span class="doctor-check-badge ${badgeClass}">${badgeText}</span>
      `;
      checkList.appendChild(el);
    }

    if (hasError) {
      summary.style.background = 'rgba(255,105,96,0.08)';
      summary.style.color = 'var(--error)';
      summary.textContent = 'Some required checks failed. Fix the issues above to use vai.';
    } else if (hasWarning) {
      summary.style.background = 'rgba(255,192,16,0.08)';
      summary.style.color = 'var(--warning)';
      summary.textContent = 'All required checks passed. Some optional features are not configured.';
    } else {
      summary.style.background = 'rgba(0,212,170,0.08)';
      summary.style.color = 'var(--success)';
      summary.textContent = 'All checks passed. vai is ready to use!';
    }

    loading.style.display = 'none';
    results.style.display = 'block';
  } catch (err) {
    loading.style.display = 'none';
    empty.style.display = 'block';
    empty.textContent = 'Health check failed: ' + err.message;
  }

  btn.disabled = false;
  btn.textContent = 'Run Health Check';
}

// ── MCP Integrations ──
var MCP_TOOL_ICONS = { claude:'\u{1F4AC}', 'claude-code':'\u{1F4BB}', cursor:'\u{1F5B1}', windsurf:'\u{1F30A}', vscode:'\u{1F4DD}', 'vscode-insiders':'\u{1F9EA}' };

async function loadMcpStatus() {
  var grid = document.getElementById('mcpGrid');
  var loading = document.getElementById('mcpLoading');
  if (!grid) return;
  grid.innerHTML = '';
  loading.style.display = 'block';
  try {
    var res = await fetch('/api/mcp/status');
    var data = await res.json();
    loading.style.display = 'none';
    for (var i = 0; i < data.length; i++) {
      var item = data[i];
      var isInstalled = item.status.indexOf('installed') !== -1;
      var icon = MCP_TOOL_ICONS[item.target] || '\u{1F527}';
      var card = document.createElement('div');
      card.className = 'mcp-card';
      card.id = 'mcp-card-' + item.target;
      var shortPath = item.configPath !== 'N/A' ? item.configPath.replace(/^.*\//, '.../') : '';
      card.innerHTML = '<div class="mcp-card-icon">' + icon + '</div>' +
        '<div class="mcp-card-info"><div class="mcp-card-name">' + item.name + '</div>' +
        '<div class="mcp-card-path" title="' + (item.configPath || '') + '">' + shortPath + '</div></div>' +
        '<div class="mcp-card-actions"><span class="mcp-card-status ' + (isInstalled ? 'installed' : 'not-installed') + '">' + (isInstalled ? 'Installed' : 'Not configured') + '</span>' +
        (isInstalled
          ? '<button class="mcp-install-btn uninstall" onclick="mcpUninstall(\'' + item.target + '\')">Remove</button>'
          : '<button class="mcp-install-btn" onclick="mcpInstall(\'' + item.target + '\')">Install</button>') +
        '</div>';
      grid.appendChild(card);
    }
  } catch (err) {
    loading.style.display = 'none';
    grid.innerHTML = '<div style="text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">Failed to load MCP status: ' + err.message + '</div>';
  }
}

async function mcpInstall(target) {
  var card = document.getElementById('mcp-card-' + target);
  var btn = card && card.querySelector('.mcp-install-btn');
  if (btn) { btn.disabled = true; btn.textContent = 'Installing...'; }
  try {
    var res = await fetch('/api/mcp/install', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({target:target}) });
    var data = await res.json();
    if (data.installed) { flashSaved(); loadMcpStatus(); }
    else if (data.message && data.message.indexOf('already configured') !== -1) {
      if (confirm(data.message + '\n\nOverwrite existing configuration?')) {
        var res2 = await fetch('/api/mcp/install', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({target:target, force:true}) });
        var data2 = await res2.json();
        if (data2.installed) flashSaved();
        loadMcpStatus();
      } else { loadMcpStatus(); }
    } else { alert(data.message || 'Installation failed'); loadMcpStatus(); }
  } catch (err) { alert('Install failed: ' + err.message); loadMcpStatus(); }
}

async function mcpUninstall(target) {
  var card = document.getElementById('mcp-card-' + target);
  var btn = card && card.querySelector('.mcp-install-btn.uninstall');
  if (btn) { btn.disabled = true; btn.textContent = 'Removing...'; }
  try {
    var res = await fetch('/api/mcp/uninstall', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({target:target}) });
    var data = await res.json();
    if (data.removed) flashSaved(); else alert(data.message || 'Uninstall failed');
    loadMcpStatus();
  } catch (err) { alert('Uninstall failed: ' + err.message); loadMcpStatus(); }
}

// ── Update Checker ──
function checkForAppUpdate() {
  if (!window.vai || !window.vai.isElectron) return;

  // Show version in sidebar + settings page
  window.vai.getVersion().then(v => {
    if (!v) return;
    const appVer = typeof v === 'object' ? v.app : v;
    const cliVer = typeof v === 'object' ? v.cli : null;

    const label = document.getElementById('appVersionLabel');
    if (label && appVer) label.textContent = 'v' + appVer;

    const section = document.getElementById('settingsVersionSection');
    const appBadge = document.getElementById('settingsAppVersion');
    const cliBadge = document.getElementById('settingsCliVersion');
    if (section) section.style.display = '';
    if (appBadge && appVer) appBadge.textContent = 'v' + appVer;
    if (cliBadge && cliVer) cliBadge.textContent = 'v' + cliVer;
  });

  // Don't check more than once per hour
  const DISMISS_KEY = 'vai-update-dismissed';
  const dismissed = localStorage.getItem(DISMISS_KEY);
  if (dismissed) {
    const dismissedAt = parseInt(dismissed, 10);
    if (Date.now() - dismissedAt < 3600000) return;
  }

  // Listen for real-time update events from electron-updater
  if (window.vai.updates.onEvent) {
    window.vai.updates.onEvent((data) => {
      const banner = document.getElementById('updateBanner');
      const progressWrap = document.getElementById('updateProgress');
      const progressFill = document.getElementById('updateProgressFill');
      const progressLabel = document.getElementById('updateProgressLabel');
      const bannerText = document.getElementById('updateBannerText');
      const downloadBtn = document.getElementById('updateDownloadBtn');
      const installBtn = document.getElementById('updateInstallBtn');

      if (data.event === 'download-progress') {
        const pct = Math.round(data.percent || 0);
        progressFill.style.width = pct + '%';
        const mbTransferred = (data.transferred / 1048576).toFixed(1);
        const mbTotal = (data.total / 1048576).toFixed(1);
        progressLabel.textContent = `${pct}% (${mbTransferred}/${mbTotal} MB)`;
      }

      if (data.event === 'update-downloaded') {
        progressWrap.style.display = 'none';
        bannerText.innerHTML = `<strong>Vai v${data.latestVersion}</strong> is ready to install.`;
        bannerText.style.display = '';
        downloadBtn.style.display = 'none';
        installBtn.style.display = '';
      }

      if (data.event === 'update-error') {
        progressWrap.style.display = 'none';
        bannerText.innerHTML = `Update failed: ${data.message}. <a href="#" onclick="window.vai.updates.openRelease('https://github.com/mrlynn/voyageai-cli/releases');return false;" style="color:var(--accent);">Download manually</a>`;
        bannerText.style.display = '';
        downloadBtn.style.display = 'none';
      }
    });
  }

  window.vai.updates.check().then(result => {
    if (!result || !result.hasUpdate) return;

    const banner = document.getElementById('updateBanner');
    const versionEl = document.getElementById('updateVersion');
    const currentEl = document.getElementById('currentVersion');
    const downloadBtn = document.getElementById('updateDownloadBtn');
    const installBtn = document.getElementById('updateInstallBtn');
    const dismissBtn = document.getElementById('updateDismissBtn');
    const bannerText = document.getElementById('updateBannerText');
    const progressWrap = document.getElementById('updateProgress');

    if (!banner) return;

    versionEl.textContent = 'v' + result.latestVersion;
    currentEl.textContent = 'v' + result.currentVersion;
    banner.classList.add('show');

    downloadBtn.addEventListener('click', () => {
      // Start auto-download
      if (window.vai.updates.download) {
        downloadBtn.style.display = 'none';
        bannerText.style.display = 'none';
        progressWrap.style.display = 'flex';
        // Timeout: if no progress after 30s, offer manual download
        let gotProgress = false;
        const dlTimeout = setTimeout(() => {
          if (!gotProgress) {
            progressWrap.style.display = 'none';
            bannerText.innerHTML = 'Download stalled. <a href="#" onclick="window.vai.updates.openRelease(\'' + result.releaseUrl + '\');return false;" style="color:var(--accent);">Download manually</a>';
            bannerText.style.display = '';
          }
        }, 30000);
        // Listen for first progress event to cancel timeout
        const unsub = window.vai.updates.onEvent((d) => {
          if (d.event === 'download-progress') { gotProgress = true; clearTimeout(dlTimeout); unsub(); }
          if (d.event === 'update-error') { clearTimeout(dlTimeout); unsub(); }
          if (d.event === 'update-downloaded') { clearTimeout(dlTimeout); unsub(); }
        });
        window.vai.updates.download().catch(() => {
          clearTimeout(dlTimeout);
          // Fallback to manual download
          window.vai.updates.openRelease(result.releaseUrl);
        });
      } else {
        window.vai.updates.openRelease(result.releaseUrl);
      }
    });

    installBtn.addEventListener('click', () => {
      installBtn.textContent = 'Restarting...';
      installBtn.disabled = true;
      window.vai.updates.install();
    });

    dismissBtn.addEventListener('click', () => {
      banner.classList.remove('show');
      localStorage.setItem(DISMISS_KEY, String(Date.now()));
    });
  }).catch(() => {
    // Silent fail — update check is non-critical
  });
}

// ── Onboarding Walkthrough ──
function initOnboarding() {
  const ONBOARDING_KEY = 'vai-onboarding-complete';
  // The legacy welcome tour no longer matches the redesigned interface.
  // Mark it complete so startup skips the broken onboarding flow.
  localStorage.setItem(ONBOARDING_KEY, 'true');
  window.startOnboarding = function() {};
}

// ── Multimodal Tab ──
let mmImageData = null; // base64 data URL of the uploaded image
let mmVideoData = null; // base64 data URL of the uploaded video
let mmGalleryImages = []; // array of { dataUrl, name, size }
let mmSearchMode = 'text';
let mmSearchImageIndex = -1;

function initMultimodal() {
  const dropZone = document.getElementById('mmDropZone');
  const fileInput = document.getElementById('mmFileInput');

  // Click to browse — in Electron, use native dialog (more reliable);
  // in browser, use <input type="file"> with a re-trigger guard.
  let fileDialogOpen = false;
  dropZone.addEventListener('click', async () => {
    if (fileDialogOpen) return;
    fileDialogOpen = true;
    try {
      if (window.vai && window.vai.isElectron && window.vai.openImageDialog) {
        const result = await window.vai.openImageDialog();
        if (!result.canceled && result.dataUrl) {
          handleMultimodalImageFromData(result.dataUrl, result.name, result.size);
        }
      } else {
        fileInput.click();
      }
    } finally {
      setTimeout(() => { fileDialogOpen = false; }, 300);
    }
  });
  fileInput.addEventListener('change', (e) => {
    fileDialogOpen = false;
    if (e.target.files && e.target.files[0]) handleMultimodalImage(e.target.files[0]);
    fileInput.value = '';
  });

  // Drag and drop
  ['dragenter', 'dragover'].forEach(evt => {
    dropZone.addEventListener(evt, (e) => {
      e.preventDefault();
      e.stopPropagation();
      dropZone.classList.add('drag-active');
    });
  });
  ['dragleave', 'drop'].forEach(evt => {
    dropZone.addEventListener(evt, (e) => {
      e.preventDefault();
      e.stopPropagation();
      dropZone.classList.remove('drag-active');
    });
  });
  dropZone.addEventListener('drop', (e) => {
    if (e.dataTransfer.files && e.dataTransfer.files[0]) {
      handleMultimodalImage(e.dataTransfer.files[0]);
    }
  });

  // Paste from clipboard
  document.addEventListener('paste', (e) => {
    // Only handle if multimodal tab is active
    const panel = document.getElementById('tab-multimodal');
    if (!panel || !panel.classList.contains('active')) return;
    const items = e.clipboardData?.items;
    if (!items) return;
    for (const item of items) {
      if (item.type.startsWith('image/')) {
        e.preventDefault();
        handleMultimodalImage(item.getAsFile());
        return;
      }
    }
  });

  // Video drop zone — the hidden <input type="file"> lives inside the same
  // card, so its programmatic .click() bubbles back up through the DOM and
  // re-triggers the dropzone click handler, creating an infinite dialog loop.
  // Fix: stopPropagation on the input so its click never reaches the dropzone,
  // plus a re-trigger guard identical to the image dropzone.
  const videoDropZone = document.getElementById('mmVideoDropZone');
  const videoFileInput = document.getElementById('mmVideoFileInput');

  videoFileInput.addEventListener('click', (e) => e.stopPropagation());

  let videoDialogOpen = false;
  videoDropZone.addEventListener('click', async () => {
    if (videoDialogOpen) return;
    videoDialogOpen = true;
    try {
      if (window.vai && window.vai.isElectron && window.vai.openVideoDialog) {
        const result = await window.vai.openVideoDialog();
        if (!result.canceled && result.dataUrl) {
          handleMultimodalVideoFromData(result.dataUrl, result.name, result.size);
        }
      } else {
        videoFileInput.click();
      }
    } finally {
      setTimeout(() => { videoDialogOpen = false; }, 300);
    }
  });
  videoFileInput.addEventListener('change', (e) => {
    videoDialogOpen = false;
    if (e.target.files && e.target.files[0]) handleMultimodalVideo(e.target.files[0]);
    videoFileInput.value = '';
  });

  ['dragenter', 'dragover'].forEach(evt => {
    videoDropZone.addEventListener(evt, (e) => {
      e.preventDefault();
      e.stopPropagation();
      videoDropZone.classList.add('drag-active');
    });
  });
  ['dragleave', 'drop'].forEach(evt => {
    videoDropZone.addEventListener(evt, (e) => {
      e.preventDefault();
      e.stopPropagation();
      videoDropZone.classList.remove('drag-active');
    });
  });
  videoDropZone.addEventListener('drop', (e) => {
    if (e.dataTransfer.files && e.dataTransfer.files[0]) {
      handleMultimodalVideo(e.dataTransfer.files[0]);
    }
  });

  // Gallery
  renderGalleryGrid();

  const galleryInput = document.getElementById('mmGalleryFileInput');
  galleryInput.addEventListener('change', (e) => {
    if (e.target.files && e.target.files[0]) {
      addGalleryImage(e.target.files[0]);
    }
    galleryInput.value = '';
  });
}

// Handle image from Electron native dialog (already has dataUrl)
function handleMultimodalImageFromData(dataUrl, name, size) {
  mmImageData = dataUrl;
  const img = document.getElementById('mmPreviewImg');
  img.src = mmImageData;
  img.onload = () => {
    const info = document.getElementById('mmFileInfo');
    const sizeStr = size > 1024 * 1024
      ? (size / (1024 * 1024)).toFixed(1) + ' MB'
      : (size / 1024).toFixed(0) + ' KB';
    info.textContent = `${name} · ${img.naturalWidth}×${img.naturalHeight} · ${sizeStr}`;
  };
  document.getElementById('mmDropZone').style.display = 'none';
  document.getElementById('mmPreview').classList.add('visible');
  hideError('mmError');
}

function handleMultimodalImage(file) {
  const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
  if (!VALID_TYPES.includes(file.type)) {
    showError('mmError', 'Unsupported image type. Use PNG, JPEG, WebP, or GIF.');
    return;
  }
  if (file.size > 20 * 1024 * 1024) {
    showError('mmError', 'Image too large. Maximum size is 20 MB.');
    return;
  }
  hideError('mmError');

  const reader = new FileReader();
  reader.onload = (e) => {
    mmImageData = e.target.result;
    const img = document.getElementById('mmPreviewImg');
    img.src = mmImageData;

    img.onload = () => {
      const info = document.getElementById('mmFileInfo');
      const sizeStr = file.size > 1024 * 1024
        ? (file.size / (1024 * 1024)).toFixed(1) + ' MB'
        : (file.size / 1024).toFixed(0) + ' KB';
      info.textContent = `${file.name} · ${img.naturalWidth}×${img.naturalHeight} · ${sizeStr}`;
    };

    document.getElementById('mmDropZone').style.display = 'none';
    document.getElementById('mmPreview').classList.add('visible');
  };
  reader.readAsDataURL(file);
}

window.clearMultimodalImage = function() {
  mmImageData = null;
  document.getElementById('mmPreviewImg').src = '';
  document.getElementById('mmFileInfo').textContent = '';
  document.getElementById('mmPreview').classList.remove('visible');
  document.getElementById('mmDropZone').style.display = '';
  document.getElementById('mmFileInput').value = '';
};

async function handleMultimodalVideoFromData(dataUrl, name, size) {
  mmVideoData = dataUrl;
  const video = document.getElementById('mmPreviewVideo');
  video.src = mmVideoData;

  const info = document.getElementById('mmVideoFileInfo');
  const sizeStr = size > 1024 * 1024
    ? (size / (1024 * 1024)).toFixed(1) + ' MB'
    : (size / 1024).toFixed(0) + ' KB';

  document.getElementById('mmVideoDropZone').style.display = 'none';
  document.getElementById('mmVideoPreview').classList.add('visible');
  hideError('mmError');

  // Estimate tokens from video metadata
  const meta = await estimateVideoTokens(video);
  if (meta) {
    const durStr = meta.duration.toFixed(1) + 's';
    const resStr = meta.width + '×' + meta.height;
    info.textContent = `${name} · ${sizeStr} · ${resStr} · ${durStr} · ~${meta.tokens.toLocaleString()} tokens`;
    if (meta.tokens > 32000) {
      showError('mmError',
        'This video is estimated at ~' + meta.tokens.toLocaleString() + ' tokens, which exceeds the 32,000 token context window. ' +
        'Try a shorter clip, lower resolution, or smaller dimensions. ' +
        '(' + resStr + ', ' + durStr + ')'
      );
    }
  } else {
    info.textContent = `${name} · ${sizeStr}`;
  }
}

// Estimate video tokens: total pixels across all frames / 1120 pixels per token
// The playground server downsamples video to 1fps before sending to Voyage AI,
// so we estimate based on 1fps which matches what will actually be sent.
function estimateVideoTokens(videoEl) {
  return new Promise((resolve) => {
    const checkMeta = () => {
      const w = videoEl.videoWidth;
      const h = videoEl.videoHeight;
      const dur = videoEl.duration;
      if (w && h && dur && isFinite(dur)) {
        // Server downsamples to 1fps before API call
        const assumedFps = 1;
        const frames = Math.ceil(dur * assumedFps);
        const totalPixels = w * h * frames;
        const tokens = Math.ceil(totalPixels / 1120);
        resolve({ width: w, height: h, duration: dur, frames, totalPixels, tokens });
      } else {
        resolve(null);
      }
    };
    if (videoEl.readyState >= 1) {
      checkMeta();
    } else {
      videoEl.addEventListener('loadedmetadata', checkMeta, { once: true });
      // Timeout fallback
      setTimeout(() => resolve(null), 3000);
    }
  });
}

function handleMultimodalVideo(file) {
  const VALID_TYPES = ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska'];
  if (!VALID_TYPES.includes(file.type) && !file.name.match(/\.(mp4|webm|mov|avi|mkv)$/i)) {
    showError('mmError', 'Unsupported video type. Use MP4, WebM, MOV, AVI, or MKV.');
    return;
  }
  if (file.size > 20 * 1024 * 1024) {
    showError('mmError', 'Video too large. Maximum size is 20 MB.');
    return;
  }
  hideError('mmError');

  const reader = new FileReader();
  reader.onload = async (e) => {
    mmVideoData = e.target.result;
    const video = document.getElementById('mmPreviewVideo');
    video.src = mmVideoData;

    const info = document.getElementById('mmVideoFileInfo');
    const sizeStr = file.size > 1024 * 1024
      ? (file.size / (1024 * 1024)).toFixed(1) + ' MB'
      : (file.size / 1024).toFixed(0) + ' KB';

    document.getElementById('mmVideoDropZone').style.display = 'none';
    document.getElementById('mmVideoPreview').classList.add('visible');

    // Estimate tokens from video metadata and warn if likely to exceed limit
    const meta = await estimateVideoTokens(video);
    if (meta) {
      const durStr = meta.duration.toFixed(1) + 's';
      const resStr = meta.width + '×' + meta.height;
      info.textContent = `${file.name} · ${sizeStr} · ${resStr} · ${durStr} · ~${meta.tokens.toLocaleString()} tokens`;
      if (meta.tokens > 32000) {
        showError('mmError',
          'This video is estimated at ~' + meta.tokens.toLocaleString() + ' tokens, which exceeds the 32,000 token context window. ' +
          'Try a shorter clip, lower resolution, or smaller dimensions. ' +
          '(' + resStr + ', ' + durStr + ')'
        );
      }
    } else {
      info.textContent = `${file.name} · ${sizeStr}`;
    }
  };
  reader.readAsDataURL(file);
}

window.clearMultimodalVideo = function() {
  mmVideoData = null;
  const video = document.getElementById('mmPreviewVideo');
  video.pause();
  video.src = '';
  document.getElementById('mmVideoFileInfo').textContent = '';
  document.getElementById('mmVideoPreview').classList.remove('visible');
  document.getElementById('mmVideoDropZone').style.display = '';
  document.getElementById('mmVideoFileInput').value = '';
};

window.doMultimodalCompare = async function() {
  hideError('mmError');
  sendTelemetry('api_call', { endpoint: 'multimodal-compare', model: document.getElementById('mmModel').value });
  const text = document.getElementById('mmText').value.trim();
  const hasMedia = mmImageData || mmVideoData;
  if (!hasMedia && !text) { showError('mmError', 'Upload an image or video, and enter text to compare'); return; }

  // Need at least 2 inputs to compare
  const mediaInputs = [];
  if (mmImageData) {
    mediaInputs.push({ content: [{ type: 'image_base64', image_base64: mmImageData }], label: 'Image' });
  }
  if (mmVideoData) {
    mediaInputs.push({ content: [{ type: 'video_base64', video_base64: mmVideoData }], label: 'Video' });
  }
  if (text) {
    mediaInputs.push({ content: [{ type: 'text', text: text }], label: 'Text' });
  }

  if (mediaInputs.length < 2) {
    showError('mmError', 'Provide at least 2 inputs to compare (e.g., image + text, video + text, or image + video)');
    return;
  }

  setLoading('mmCompareBtn', true);
  try {
    const model = document.getElementById('mmModel').value;
    const dimsVal = document.getElementById('mmDimensions').value;
    const body = {
      inputs: mediaInputs.map(m => m.content ? { content: m.content } : m),
      model: model,
      input_type: 'document'
    };
    if (dimsVal) body.output_dimension = parseInt(dimsVal, 10);

    const data = await apiPost('/api/multimodal-embed', body);

    // Compare first two inputs (primary comparison)
    const vecA = data.data[0].embedding;
    const vecB = data.data[1].embedding;
    const cosine = cosineSim(vecA, vecB);

    // Hero display
    const cosinePct = Math.max(0, cosine * 100);
    let cosineColor;
    if (cosine > 0.7) cosineColor = 'var(--green)';
    else if (cosine > 0.4) cosineColor = 'var(--yellow)';
    else cosineColor = 'var(--red)';

    const scoreEl = document.getElementById('mmSimScore');
    scoreEl.textContent = cosine.toFixed(4);
    scoreEl.style.color = cosineColor;

    const barEl = document.getElementById('mmSimBar');
    barEl.style.width = cosinePct + '%';
    barEl.style.background = cosineColor;

    // Stats
    const usage = data.usage || {};
    const pairLabel = mediaInputs[0].label + ' vs ' + mediaInputs[1].label;
    let statsHtml = `
      <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model || model}</span></span>
      <span class="stat"><span class="stat-label">Comparing</span><span class="stat-value">${pairLabel}</span></span>
      <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
    `;
    if (usage.text_tokens) statsHtml += `<span class="stat"><span class="stat-label">Text Tokens</span><span class="stat-value">${usage.text_tokens}</span></span>`;
    if (usage.image_pixels) statsHtml += `<span class="stat"><span class="stat-label">Image Pixels</span><span class="stat-value">${usage.image_pixels.toLocaleString()}</span></span>`;
    statsHtml += `<span class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value">${usage.total_tokens || '—'}</span></span>`;

    // If 3 inputs, show all pairwise similarities
    if (mediaInputs.length === 3) {
      const vecC = data.data[2].embedding;
      const simAC = cosineSim(vecA, vecC);
      const simBC = cosineSim(vecB, vecC);
      statsHtml += `<br><span class="stat"><span class="stat-label">${mediaInputs[0].label} vs ${mediaInputs[2].label}</span><span class="stat-value">${simAC.toFixed(4)}</span></span>`;
      statsHtml += `<span class="stat"><span class="stat-label">${mediaInputs[1].label} vs ${mediaInputs[2].label}</span><span class="stat-value">${simBC.toFixed(4)}</span></span>`;
    }

    document.getElementById('mmStats').innerHTML = statsHtml;

    // Insight note
    const noteEl = document.getElementById('mmNote');
    if (cosine > 0.7) {
      noteEl.innerHTML = '💡 <strong>High similarity!</strong> The inputs are closely related in Voyage AI\'s multimodal embedding space.';
    } else if (cosine > 0.4) {
      noteEl.innerHTML = '💡 <strong>Moderate similarity.</strong> The inputs share some semantic overlap but are not a direct match.';
    } else {
      noteEl.innerHTML = '💡 <strong>Low similarity.</strong> The inputs are semantically distant. Try content that is more closely related.';
    }

    document.getElementById('mmResult').classList.add('visible');
    CostTracker.addOperation('multimodal-compare', data.model || model, usage.total_tokens || 0);
  } catch (err) {
    let msg = err.message;
    // Provide actionable guidance for context window errors
    if (msg.includes('context window') || msg.includes('exceed')) {
      msg += ' Try reducing video resolution, trimming to a shorter clip, or using a smaller image.';
    }
    showError('mmError', msg);
  } finally {
    setLoading('mmCompareBtn', false);
  }
};

// Gallery functions
function renderGalleryGrid() {
  const grid = document.getElementById('mmGalleryGrid');
  grid.innerHTML = '';

  mmGalleryImages.forEach((img, i) => {
    const slot = document.createElement('div');
    slot.className = 'mm-gallery-slot filled' + (mmSearchMode === 'image' && mmSearchImageIndex === i ? ' query-selected' : '');
    slot.innerHTML = `<img src="${img.dataUrl}" alt="${img.name}"><button class="mm-slot-remove" title="Remove">×</button>`;
    slot.querySelector('.mm-slot-remove').addEventListener('click', (e) => {
      e.stopPropagation();
      removeGalleryImage(i);
    });
    if (mmSearchMode === 'image') {
      slot.style.cursor = 'pointer';
      slot.addEventListener('click', () => selectGalleryImageAsQuery(i));
    }
    grid.appendChild(slot);
  });

  // Add empty slot if under 6
  if (mmGalleryImages.length < 6) {
    const addSlot = document.createElement('div');
    addSlot.className = 'mm-gallery-slot';
    addSlot.innerHTML = '<span class="mm-slot-add">+</span>';
    addSlot.addEventListener('click', async () => {
      if (window._galleryDialogOpen) return;
      window._galleryDialogOpen = true;
      try {
        if (window.vai && window.vai.isElectron && window.vai.openImageDialog) {
          const result = await window.vai.openImageDialog();
          if (!result.canceled && result.dataUrl) {
            addGalleryImageFromData(result.dataUrl, result.name, result.size);
          }
        } else {
          document.getElementById('mmGalleryFileInput').click();
        }
      } finally {
        setTimeout(() => { window._galleryDialogOpen = false; }, 300);
      }
    });
    grid.appendChild(addSlot);
  }
}

// Add gallery image from Electron native dialog (already has dataUrl)
function addGalleryImageFromData(dataUrl, name, size) {
  if (mmGalleryImages.length >= 6) return;
  mmGalleryImages.push({ dataUrl, name, size });
  renderGalleryGrid();
}

function addGalleryImage(file) {
  const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
  if (!VALID_TYPES.includes(file.type)) return;
  if (file.size > 20 * 1024 * 1024) return;
  if (mmGalleryImages.length >= 6) return;

  const reader = new FileReader();
  reader.onload = (e) => {
    mmGalleryImages.push({ dataUrl: e.target.result, name: file.name, size: file.size });
    renderGalleryGrid();
  };
  reader.readAsDataURL(file);
}

function removeGalleryImage(index) {
  mmGalleryImages.splice(index, 1);
  if (mmSearchImageIndex === index) mmSearchImageIndex = -1;
  else if (mmSearchImageIndex > index) mmSearchImageIndex--;
  renderGalleryGrid();
  updateSearchImageLabel();
}

function selectGalleryImageAsQuery(index) {
  mmSearchImageIndex = (mmSearchImageIndex === index) ? -1 : index;
  renderGalleryGrid();
  updateSearchImageLabel();
}

function updateSearchImageLabel() {
  const label = document.getElementById('mmSearchImageLabel');
  if (mmSearchImageIndex >= 0 && mmSearchImageIndex < mmGalleryImages.length) {
    label.textContent = '✓ Image ' + (mmSearchImageIndex + 1) + ' selected as query';
    label.style.color = 'var(--accent)';
  } else {
    label.textContent = 'No image selected';
    label.style.color = 'var(--text-muted)';
  }
}

window.setMmSearchMode = function(mode) {
  mmSearchMode = mode;
  document.querySelectorAll('#mmSearchMode button').forEach(b => {
    b.classList.toggle('active', b.dataset.mode === mode);
  });
  document.getElementById('mmSearchTextWrap').style.display = mode === 'text' ? '' : 'none';
  document.getElementById('mmSearchImageWrap').style.display = mode === 'image' ? '' : 'none';
  renderGalleryGrid();
};

window.doMultimodalSearch = async function() {
  hideError('mmSearchError');
  sendTelemetry('api_call', { endpoint: 'multimodal-search', model: document.getElementById('mmModel').value });

  const corpusText = document.getElementById('mmCorpusText').value.trim();
  const textItems = corpusText ? corpusText.split('\n').map(t => t.trim()).filter(Boolean) : [];

  if (mmGalleryImages.length === 0 && textItems.length === 0) {
    showError('mmSearchError', 'Add at least one image or text to the corpus');
    return;
  }

  // Build query input
  let queryInput;
  if (mmSearchMode === 'text') {
    const q = document.getElementById('mmSearchQuery').value.trim();
    if (!q) { showError('mmSearchError', 'Enter a search query'); return; }
    queryInput = { content: [{ type: 'text', text: q }] };
  } else {
    if (mmSearchImageIndex < 0 || mmSearchImageIndex >= mmGalleryImages.length) {
      showError('mmSearchError', 'Select an image from the corpus to use as query');
      return;
    }
    queryInput = { content: [{ type: 'image_base64', image_base64: mmGalleryImages[mmSearchImageIndex].dataUrl }] };
  }

  const btnId = mmSearchMode === 'text' ? 'mmSearchBtn' : 'mmSearchImgBtn';
  setLoading(btnId, true);

  try {
    const model = document.getElementById('mmModel').value;
    const dimsVal = document.getElementById('mmDimensions').value;

    // Build corpus inputs
    const corpusInputs = [];
    const corpusMeta = []; // track type/content for display

    mmGalleryImages.forEach((img, i) => {
      corpusInputs.push({ content: [{ type: 'image_base64', image_base64: img.dataUrl }] });
      corpusMeta.push({ type: 'image', dataUrl: img.dataUrl, label: img.name || 'Image ' + (i + 1) });
    });

    textItems.forEach(t => {
      corpusInputs.push({ content: [{ type: 'text', text: t }] });
      corpusMeta.push({ type: 'text', label: t });
    });

    // Embed query + all corpus items in one call
    const allInputs = [queryInput, ...corpusInputs];
    const body = {
      inputs: allInputs,
      model: model,
      input_type: 'document'
    };
    if (dimsVal) body.output_dimension = parseInt(dimsVal, 10);

    const data = await apiPost('/api/multimodal-embed', body);

    const queryVec = data.data[0].embedding;
    const results = corpusMeta.map((meta, i) => {
      const vec = data.data[i + 1].embedding;
      const sim = cosineSim(queryVec, vec);
      return { ...meta, similarity: sim };
    }).sort((a, b) => b.similarity - a.similarity);

    // Render results
    const listEl = document.getElementById('mmSearchResultList');
    listEl.innerHTML = '';

    results.forEach((r, i) => {
      let simColor;
      if (r.similarity > 0.7) simColor = 'var(--green)';
      else if (r.similarity > 0.4) simColor = 'var(--yellow)';
      else simColor = 'var(--red)';

      const item = document.createElement('div');
      item.className = 'mm-result-item';

      const thumbHtml = r.type === 'image'
        ? `<img class="mm-result-thumb" src="${r.dataUrl}" alt="${r.label}">`
        : `<div class="mm-result-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-surface);font-size:18px;">📝</div>`;

      item.innerHTML = `
        <div class="mm-result-rank">#${i + 1}</div>
        ${thumbHtml}
        <div class="mm-result-content">
          <div class="mm-result-text" title="${r.label.replace(/"/g, '&quot;')}">${r.label}</div>
          <div class="mm-result-type">${r.type}</div>
        </div>
        <div class="mm-result-score" style="color:${simColor}">${r.similarity.toFixed(4)}</div>
      `;
      listEl.appendChild(item);
    });

    document.getElementById('mmSearchResult').classList.add('visible');
    CostTracker.addOperation('multimodal-search', model, data.usage?.total_tokens || 0);
  } catch (err) {
    let msg = err.message;
    if (msg.includes('context window') || msg.includes('exceed')) {
      msg += ' Try using smaller images or fewer corpus items per batch.';
    }
    showError('mmSearchError', msg);
  } finally {
    setLoading(btnId, false);
  }
};

// ── Patch init to include benchmark setup ──
const _origInit = init;
init = async function() {
  await _origInit();
  initThemeToggle();
  buildModelCheckboxes();
  populateBenchRankSelects();
  populateQuantModelSelect();
  initCostCalculator();
  renderHistory();
  initSettings();
  initMultimodal();
  checkForAppUpdate();
  initOnboarding();
};  

// ── Start ──
init();
 
// ──  Vector Space Invaders (Easter Egg) ──

// VSI Telemetry helper
function sendVSITelemetry(event, extra) {
  try {
    fetch('https://vaicli.com/api/telemetry', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ event, ...extra, sentAt: new Date().toISOString() }),
    }).catch(() => {});
  } catch (_) {}
}

let _vsiGameStartTime = 0;
let _vsiGameTrigger = 'unknown';

(function initEasterEgg() {
  const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
  let konamiIdx = 0;
  let logoClicks = [];

  function normalizeKonamiKey(key) {
    return typeof key === 'string' && key.length === 1 ? key.toLowerCase() : key;
  }

  function isEditableTarget(target) {
    if (!target || !(target instanceof HTMLElement)) return false;
    if (target.isContentEditable) return true;
    const tag = target.tagName;
    return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
  }

  function isVisibleOverlay(node) {
    if (!node) return false;
    const style = window.getComputedStyle(node);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
    return node.classList.contains('open') || style.pointerEvents !== 'none' || node.style.display === 'flex' || node.style.display === 'block';
  }

  function isGameLaunchBlocked() {
    const blockingIds = [
      'commandPaletteOverlay',
      'modelPickerOverlay',
      'exploreModal',
      'costHelpModal',
      'agentInfoModal',
      'wfStoreOverlay',
      'wfOutputModalBackdrop',
      'wfHelpModalBackdrop',
      'wfInputModalBackdrop',
      'wfInfoModalBackdrop',
      'bugOverlay',
      'exportOverlay',
      'vsiOverlay',
    ];

    return blockingIds.some(id => isVisibleOverlay(document.getElementById(id)));
  }

  document.addEventListener('keydown', (e) => {
    if (e.repeat || e.metaKey || e.ctrlKey || e.altKey) return;
    if (isEditableTarget(e.target) || isGameLaunchBlocked()) return;

    const key = normalizeKonamiKey(e.key);
    const expected = KONAMI[konamiIdx];

    if (key === expected) {
      konamiIdx++;
      if (konamiIdx === KONAMI.length) {
        konamiIdx = 0;
        _vsiGameTrigger = 'konami';
        launchVSI();
      }
      return;
    }

    konamiIdx = key === KONAMI[0] ? 1 : 0;
  });

  const logo = document.getElementById('sidebarLogo');
  if (logo) {
    logo.addEventListener('click', () => {
      const now = Date.now();
      logoClicks.push(now);
      logoClicks = logoClicks.filter(t => now - t < 2000);
      if (logoClicks.length >= 7) { logoClicks = []; _vsiGameTrigger = 'logo_click'; launchVSI(); }
    });
  }

  function launchVSI() {
    const overlay = document.getElementById('vsiOverlay');
    const canvas = document.getElementById('vsiCanvas');
    if (!overlay || !canvas) return;
    overlay.style.display = 'flex';
    _vsiGameStartTime = Date.now();

    // Detect version from page
    const versionEl = document.querySelector('.version-label, [data-version]');
    const version = versionEl ? (versionEl.dataset?.version || versionEl.textContent?.trim()) : 'unknown';
    const ua = navigator.userAgent;
    const platform = /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : /Android/.test(ua) ? 'Android' : /iPhone|iPad/.test(ua) ? 'iOS' : 'unknown';

    sendVSITelemetry('vsi_game_start', {
      version,
      context: 'playground',
      platform,
      locale: navigator.language || 'unknown',
      trigger: _vsiGameTrigger,
    });

    startVSI(canvas, () => { overlay.style.display = 'none'; });
  }
})();

function startVSI(canvas, onExit) {
  const ctx = canvas.getContext('2d');
  const W = canvas.width = 600;
  const H = canvas.height = 500;
  const robot = window.VAIRobotAssets || null;
  const robotColors = {
    teal: '#00D4AA',
    cyan: '#40E0FF',
    red: '#F85149',
    white: '#E6EDF3',
  };

  function drawRobotPose(poseName, x, y, pixelSize, options = {}) {
    if (!robot) {
      ctx.save();
      ctx.fillStyle = robotColors.teal;
      ctx.fillRect(x - 10, y - 10, 20, 20);
      ctx.restore();
      return;
    }

    robot.drawPose(ctx, poseName, frame + (options.tickOffset || 0), {
      x,
      y,
      pixelSize,
      center: true,
      alpha: options.alpha == null ? 1 : options.alpha,
      mirror: !!options.mirror,
    });
  }

  function drawGlow(x, y, radius, rgb, alpha) {
    const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
    gradient.addColorStop(0, `rgba(${rgb}, ${alpha})`);
    gradient.addColorStop(1, `rgba(${rgb}, 0)`);
    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPulseBolt(x, y, height, color) {
    ctx.save();
    ctx.fillStyle = color;
    ctx.fillRect(x - 2, y - height, 4, height);
    ctx.fillRect(x - 4, y - 6, 8, 6);
    ctx.restore();
  }

  const ship = { x: W / 2, w: 54, h: 54, speed: 5, damageUntil: 0, boostUntil: 0 };
  const bullets = [];
  const particles = [];
  const explosionTexts = [];
  const ENEMY_LABELS = [
    'embed', 'query', 'atlas',
    'rerank', 'chunk', 'search',
    'agent', 'index', 'cache',
    'drift', 'route', 'sync',
  ];
  const ENEMY_POSES = ['idle', 'blink', 'search', 'thinking'];
  const BOSS_LABEL = 'ROBOT CORE';

  let enemies = [];
  let wave = 1;
  let score = 0;
  let lives = 3;
  let gameOver = false;
  let bossMode = false;
  let boss = null;
  let frame = 0;
  let lastShot = 0;
  let waveQueued = false;
  let hudNotice = { text: 'Robot systems online', pose: 'wave', ttl: 140 };

  const keys = {};
  const keyDown = (e) => {
    keys[e.key] = true;
    if (['ArrowLeft', 'ArrowRight', ' ', 'Escape'].includes(e.key)) e.preventDefault();
    if (e.key === 'Escape') {
      sendVSITelemetry('vsi_game_over', {
        score,
        wave,
        durationMs: Date.now() - _vsiGameStartTime,
        livesRemaining: lives,
        exitReason: 'esc',
      });
      cleanup();
      onExit();
    }
  };
  const keyUp = (e) => { keys[e.key] = false; };
  document.addEventListener('keydown', keyDown);
  document.addEventListener('keyup', keyUp);

  function cleanup() {
    document.removeEventListener('keydown', keyDown);
    document.removeEventListener('keyup', keyUp);
    gameOver = true;
  }

  function setHudNotice(text, pose, ttl) {
    hudNotice = { text, pose, ttl };
  }

  function spawnWave() {
    enemies = [];
    if (wave % 5 === 0) {
      bossMode = true;
      boss = {
        x: W / 2,
        y: 96,
        w: 140,
        h: 140,
        hp: 10 + wave,
        maxHp: 10 + wave,
        dx: 2.2,
        label: BOSS_LABEL,
        flashUntil: 0,
      };
      setHudNotice('Boss wave: robot core breach', 'error', 120);
      return;
    }

    bossMode = false;
    boss = null;
    const rows = Math.min(3, 1 + Math.floor(wave / 2));
    const cols = Math.min(6, 3 + Math.floor(wave / 3));
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const index = (r * cols + c) % ENEMY_LABELS.length;
        enemies.push({
          x: 70 + c * 84,
          y: 68 + r * 70,
          w: 56,
          h: 56,
          hp: 1 + Math.floor(wave / 4),
          label: ENEMY_LABELS[index],
          dx: 1 + wave * 0.3,
          dir: 1,
          pose: ENEMY_POSES[index % ENEMY_POSES.length],
          tickOffset: index * 6,
          flashUntil: 0,
        });
      }
    }
    setHudNotice(`Wave ${wave}: robot sweep`, 'search', 90);
  }

  function shoot() {
    if (frame - lastShot < 10) return;
    lastShot = frame;
    bullets.push({ x: ship.x, y: H - 70, dy: -8, life: 90 });
  }

  function addExplosion(x, y, sim, poseName = 'success') {
    const color = sim > 0.7 ? robotColors.teal : sim > 0.4 ? '#FFC010' : '#FF6960';
    for (let i = 0; i < 12; i++) {
      const angle = (Math.PI * 2 / 12) * i + Math.random() * 0.35;
      const speed = 1.4 + Math.random() * 2.2;
      particles.push({
        x,
        y,
        dx: Math.cos(angle) * speed,
        dy: Math.sin(angle) * speed,
        life: 26 + Math.floor(Math.random() * 10),
        color,
        size: 2 + Math.random() * 2,
        robotPose: i % 4 === 0 ? poseName : null,
        tickOffset: i * 4,
      });
    }
    explosionTexts.push({ x, y, text: sim.toFixed(2), life: 40, color });
  }

  function update() {
    if (keys['ArrowLeft'] || keys.a) {
      ship.x = Math.max(ship.w / 2, ship.x - ship.speed);
      ship.boostUntil = frame + 6;
    }
    if (keys['ArrowRight'] || keys.d) {
      ship.x = Math.min(W - ship.w / 2, ship.x + ship.speed);
      ship.boostUntil = frame + 6;
    }
    if (keys[' ']) shoot();

    for (let i = bullets.length - 1; i >= 0; i--) {
      bullets[i].y += bullets[i].dy;
      bullets[i].life--;
      if (bullets[i].y < -10 || bullets[i].life <= 0) bullets.splice(i, 1);
    }

    if (bossMode && boss) {
      boss.x += boss.dx;
      if (boss.x < boss.w / 2 || boss.x > W - boss.w / 2) boss.dx *= -1;
      if (frame % 60 === 0) {
        enemies.push({
          x: boss.x,
          y: boss.y + 56,
          w: 48,
          h: 48,
          hp: 1,
          label: ENEMY_LABELS[frame % ENEMY_LABELS.length],
          dx: 0,
          dir: 0,
          dropping: true,
          dy: 2,
          pose: 'search',
          tickOffset: frame,
          flashUntil: 0,
        });
      }

      for (let i = bullets.length - 1; i >= 0; i--) {
        const bullet = bullets[i];
        if (bullet.x > boss.x - boss.w / 2 && bullet.x < boss.x + boss.w / 2 && bullet.y > boss.y - boss.h / 2 && bullet.y < boss.y + boss.h / 2) {
          boss.hp--;
          boss.flashUntil = frame + 8;
          bullets.splice(i, 1);
          addExplosion(bullet.x, bullet.y, 0.1 + Math.random() * 0.3, 'error');
          score += 50;
          if (boss.hp <= 0) {
            addExplosion(boss.x, boss.y, 0.99, 'success');
            score += 650;
            boss = null;
            bossMode = false;
            wave++;
            waveQueued = true;
            setHudNotice('Core neutralized', 'success', 120);
            setTimeout(() => { waveQueued = false; spawnWave(); }, 1000);
          }
        }
      }
    }

    let edgeHit = false;
    for (const enemy of enemies) {
      if (enemy.dropping) {
        enemy.y += enemy.dy || 2;
      } else {
        enemy.x += enemy.dx * enemy.dir;
        if (enemy.x < enemy.w / 2 + 20 || enemy.x > W - enemy.w / 2 - 20) edgeHit = true;
      }
    }
    if (edgeHit) {
      for (const enemy of enemies) {
        if (!enemy.dropping) {
          enemy.dir *= -1;
          enemy.y += 15;
        }
      }
    }

    for (let i = bullets.length - 1; i >= 0; i--) {
      for (let j = enemies.length - 1; j >= 0; j--) {
        const bullet = bullets[i];
        const enemy = enemies[j];
        if (!bullet) break;
        if (bullet.x > enemy.x - enemy.w / 2 && bullet.x < enemy.x + enemy.w / 2 && bullet.y > enemy.y - enemy.h / 2 && bullet.y < enemy.y + enemy.h / 2) {
          enemy.hp--;
          enemy.flashUntil = frame + 6;
          bullets.splice(i, 1);
          addExplosion(bullet.x, bullet.y, 0.5 + Math.random() * 0.5, enemy.pose);
          if (enemy.hp <= 0) {
            enemies.splice(j, 1);
            score += 125;
          }
          break;
        }
      }
    }

    for (let i = enemies.length - 1; i >= 0; i--) {
      if (enemies[i].y > H - 72) {
        enemies.splice(i, 1);
        lives--;
        ship.damageUntil = frame + 24;
        setHudNotice('Robot hull breach', 'error', 72);
        if (lives <= 0) {
          gameOver = true;
          setHudNotice('Robot down', 'error', 240);
          sendVSITelemetry('vsi_game_over', {
            score,
            wave,
            durationMs: Date.now() - _vsiGameStartTime,
            livesRemaining: 0,
            exitReason: 'death',
          });
        }
      }
    }

    if (!bossMode && enemies.length === 0 && !gameOver && !waveQueued) {
      wave++;
      waveQueued = true;
      setHudNotice('Wave cleared', 'success', 90);
      setTimeout(() => { waveQueued = false; spawnWave(); }, 800);
    }

    for (let i = particles.length - 1; i >= 0; i--) {
      const particle = particles[i];
      particle.x += particle.dx;
      particle.y += particle.dy;
      particle.life--;
      if (particle.life <= 0) particles.splice(i, 1);
    }
    for (let i = explosionTexts.length - 1; i >= 0; i--) {
      explosionTexts[i].y -= 0.5;
      explosionTexts[i].life--;
      if (explosionTexts[i].life <= 0) explosionTexts.splice(i, 1);
    }
    if (hudNotice.ttl > 0) hudNotice.ttl--;
  }

  function draw() {
    const bgGradient = ctx.createLinearGradient(0, 0, 0, H);
    bgGradient.addColorStop(0, '#031924');
    bgGradient.addColorStop(1, '#071019');
    ctx.fillStyle = bgGradient;
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    for (let i = 0; i < 40; i++) {
      const sx = (i * 137 + frame * 0.1) % W;
      const sy = (i * 97 + frame * 0.05) % H;
      ctx.fillRect(sx, sy, 1, 1);
    }

    for (let x = 0; x < W; x += 40) {
      ctx.fillStyle = 'rgba(64, 224, 255, 0.03)';
      ctx.fillRect(x, 0, 1, H);
    }

    const shipPose = gameOver
      ? 'error'
      : ship.damageUntil > frame
        ? 'error'
        : keys[' ']
          ? 'thinking'
          : ship.boostUntil > frame
            ? 'search'
            : 'idle';

    drawGlow(ship.x, H - 46, 32, '0, 212, 170', 0.18);
    ctx.fillStyle = 'rgba(64, 224, 255, 0.18)';
    ctx.fillRect(ship.x - 22, H - 22, 44, 6);
    drawRobotPose(shipPose, ship.x, H - 48, 3, { mirror: keys['ArrowLeft'] || keys.a });

    for (const bullet of bullets) {
      drawPulseBolt(bullet.x, bullet.y, 14, robotColors.cyan);
    }

    if (boss) {
      const hpPct = boss.hp / boss.maxHp;
      ctx.fillStyle = 'rgba(12, 24, 35, 0.82)';
      ctx.fillRect(boss.x - boss.w / 2, boss.y - boss.h / 2, boss.w, boss.h);
      ctx.strokeStyle = boss.flashUntil > frame ? robotColors.red : robotColors.cyan;
      ctx.lineWidth = 2.5;
      ctx.strokeRect(boss.x - boss.w / 2, boss.y - boss.h / 2, boss.w, boss.h);
      drawGlow(boss.x, boss.y, 54, boss.flashUntil > frame ? '248, 81, 73' : '64, 224, 255', 0.18);
      drawRobotPose(boss.flashUntil > frame ? 'error' : (hpPct > 0.4 ? 'thinking' : 'success'), boss.x, boss.y - 8, 5);
      ctx.fillStyle = robotColors.white;
      ctx.font = 'bold 12px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(boss.label, boss.x, boss.y + boss.h / 2 - 20);
      ctx.fillStyle = '#3D4F58';
      ctx.fillRect(boss.x - boss.w / 2, boss.y - boss.h / 2 - 10, boss.w, 5);
      ctx.fillStyle = hpPct > 0.35 ? robotColors.red : '#FFC010';
      ctx.fillRect(boss.x - boss.w / 2, boss.y - boss.h / 2 - 10, boss.w * hpPct, 5);
    }

    for (const enemy of enemies) {
      ctx.fillStyle = enemy.flashUntil > frame ? 'rgba(248,81,73,0.18)' : 'rgba(17, 39, 51, 0.78)';
      ctx.fillRect(enemy.x - enemy.w / 2, enemy.y - enemy.h / 2, enemy.w, enemy.h);
      ctx.strokeStyle = enemy.flashUntil > frame ? robotColors.red : 'rgba(64,224,255,0.8)';
      ctx.lineWidth = 1;
      ctx.strokeRect(enemy.x - enemy.w / 2, enemy.y - enemy.h / 2, enemy.w, enemy.h);
      drawRobotPose(enemy.flashUntil > frame ? 'error' : enemy.pose, enemy.x, enemy.y - 6, 2, { tickOffset: enemy.tickOffset });
      ctx.fillStyle = '#C1C7C6';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(enemy.label, enemy.x, enemy.y + enemy.h / 2 - 6);
    }

    for (const particle of particles) {
      ctx.globalAlpha = particle.life / 36;
      if (particle.robotPose) {
        drawRobotPose(particle.robotPose, particle.x, particle.y, 1, { alpha: particle.life / 36, tickOffset: particle.tickOffset });
      } else {
        ctx.fillStyle = particle.color;
        ctx.fillRect(particle.x - particle.size / 2, particle.y - particle.size / 2, particle.size, particle.size);
      }
    }
    ctx.globalAlpha = 1;

    for (const explosionText of explosionTexts) {
      ctx.globalAlpha = explosionText.life / 40;
      ctx.fillStyle = explosionText.color;
      ctx.font = 'bold 14px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(explosionText.text, explosionText.x, explosionText.y);
    }
    ctx.globalAlpha = 1;

    ctx.fillStyle = robotColors.white;
    ctx.font = 'bold 11px monospace';
    ctx.textAlign = 'left';
    ctx.fillText('VAI ROBOT DEFENSE', 12, 18);
    ctx.font = '13px monospace';
    ctx.fillText(`SCORE: ${score}`, 12, 36);
    ctx.fillText(`WAVE: ${wave}`, 12, 54);
    ctx.fillText(`MODE: ${bossMode ? 'BOSS' : 'SWEEP'}`, 12, 72);

    for (let i = 0; i < lives; i++) {
      drawRobotPose(ship.damageUntil > frame ? 'error' : 'idle', W - 22 - (i * 24), 20, 1);
    }

    ctx.fillStyle = robotColors.teal;
    ctx.font = 'bold 10px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('VECTOR SPACE INVADERS // ROBOT EDITION', W / 2, H - 8);

    ctx.globalAlpha = 0.3;
    drawRobotPose('wave', W - 42, 26, 1.5);
    ctx.fillStyle = robotColors.teal;
    ctx.font = 'bold 9px monospace';
    ctx.textAlign = 'right';
    ctx.fillText('ROBOT CORE', W - 60, 28);
    ctx.globalAlpha = 1;

    if (hudNotice.ttl > 0) {
      ctx.fillStyle = 'rgba(3, 25, 36, 0.82)';
      ctx.fillRect(W / 2 - 134, 14, 268, 44);
      ctx.strokeStyle = 'rgba(64, 224, 255, 0.35)';
      ctx.strokeRect(W / 2 - 134, 14, 268, 44);
      drawRobotPose(hudNotice.pose, W / 2 - 106, 36, 1.4);
      ctx.fillStyle = robotColors.white;
      ctx.font = 'bold 12px monospace';
      ctx.textAlign = 'left';
      ctx.fillText(hudNotice.text.toUpperCase(), W / 2 - 76, 40);
    }

    if (gameOver) {
      ctx.fillStyle = 'rgba(0,30,43,0.82)';
      ctx.fillRect(0, 0, W, H);
      drawGlow(W / 2, H / 2 - 36, 80, '248, 81, 73', 0.18);
      drawRobotPose('error', W / 2, H / 2 - 60, 5);
      ctx.fillStyle = robotColors.red;
      ctx.font = 'bold 24px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('ROBOT DOWN', W / 2, H / 2 + 26);
      ctx.fillStyle = robotColors.white;
      ctx.font = '16px monospace';
      ctx.fillText(`Final Score: ${score}  |  Wave: ${wave}`, W / 2, H / 2 + 56);
      ctx.fillStyle = '#889397';
      ctx.font = '12px monospace';
      ctx.fillText('Press SPACE to reboot  |  ESC to exit', W / 2, H / 2 + 86);

      if (keys[' ']) {
        score = 0;
        wave = 1;
        lives = 3;
        gameOver = false;
        waveQueued = false;
        enemies = [];
        bullets.length = 0;
        particles.length = 0;
        explosionTexts.length = 0;
        boss = null;
        bossMode = false;
        ship.x = W / 2;
        ship.damageUntil = 0;
        spawnWave();
        setHudNotice('Robot systems online', 'wave', 120);
      }
    }
  }

  function loop() {
    if (!document.getElementById('vsiOverlay') || document.getElementById('vsiOverlay').style.display === 'none') {
      cleanup();
      return;
    }
    frame++;
    update();
    draw();
    requestAnimationFrame(loop);
  }

  spawnWave();
  loop();
}

// ── Settings Sub-Navigation ──

function switchSettingsSection(section) {
  document.querySelectorAll('.settings-nav-item').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.settingsSection === section);
  });
  document.querySelectorAll('.settings-panel').forEach(p => {
    p.classList.toggle('active', p.id === 'settings-' + section);
  });
  // Scroll content area to top
  const content = document.querySelector('.settings-content');
  if (content) content.scrollTop = 0;
  if (section === 'mcp') loadMcpStatus();
}

function openChatSettings() {
  // Open the config section in the KB panel instead of navigating to Settings
  kbOpenConfigSection();
}

// ── Agent Mode Info Modal ──
function showAgentInfoModal() {
  if (localStorage.getItem('__vai_agent_info_dismissed') === '1') return;
  const overlay = document.getElementById('agentInfoModal');
  if (!overlay) return;
  overlay.classList.add('open');

  const okBtn = document.getElementById('agentInfoOkBtn');
  const dismiss = () => {
    overlay.classList.remove('open');
    const check = document.getElementById('agentInfoDismissCheck');
    if (check && check.checked) {
      localStorage.setItem('__vai_agent_info_dismissed', '1');
    }
  };
  okBtn.onclick = dismiss;
  overlay.onclick = (e) => { if (e.target === overlay) dismiss(); };
}
window.showAgentInfoModal = showAgentInfoModal;

function setupSettingsNav() {
  document.querySelectorAll('.settings-nav-item').forEach(btn => {
    btn.addEventListener('click', () => {
      switchSettingsSection(btn.dataset.settingsSection);
    });
  });
}

// ── Settings Origins (provenance badges) ──

async function loadSettingsOrigins() {
  try {
    const res = await fetch('/api/settings/origins');
    if (!res.ok) return;
    const origins = await res.json();
    const labels = { env: 'ENV', config: '~/.vai', project: '.vai.json', default: 'Default' };
    const tooltips = {
      env: 'Set via environment variable',
      config: 'Set in ~/.vai/config.json',
      project: 'Set in project .vai.json',
      default: 'Using built-in default',
    };
    Object.entries(origins).forEach(([key, origin]) => {
      const badge = document.querySelector(`[data-origin-key="${key}"]`);
      if (badge) {
        badge.className = `settings-origin origin-${origin}`;
        badge.textContent = labels[origin] || origin;
        badge.title = tooltips[origin] || '';
      }
    });
  } catch { /* silently ignore */ }
}

// ── Embedding Dropdown Logic ──

function updateEmbedBadge() {
  const sel = document.getElementById('chatEmbeddingModel');
  const badge = document.getElementById('chatEmbedBadge');
  if (!sel || !badge) return;
  const isLocal = sel.value === 'voyage-4-nano';
  badge.textContent = isLocal ? 'LOCAL' : 'API';
  badge.className = 'embed-badge ' + (isLocal ? 'embed-badge-local' : 'embed-badge-api');
  // Handle rerank toggle: disable when nano is selected
  const rerankBtn = document.getElementById('chatRerank');
  if (rerankBtn) {
    if (isLocal) {
      rerankBtn.classList.remove('active');
      rerankBtn.disabled = true;
      rerankBtn.title = 'Reranking disabled with local embeddings';
      rerankBtn.style.opacity = '0.45';
      rerankBtn.style.cursor = 'not-allowed';
    } else {
      rerankBtn.disabled = false;
      rerankBtn.classList.add('active');
      rerankBtn.title = '';
      rerankBtn.style.opacity = '';
      rerankBtn.style.cursor = '';
    }
  }
}

let _embedAvailability = { nanoAvailable: false, hasApiKey: false };

async function initEmbeddingDropdown() {
  const sel = document.getElementById('chatEmbeddingModel');
  if (!sel) return;

  // Fetch availability from config endpoint (includes nanoAvailable and hasApiKey)
  try {
    const res = await fetch('/api/chat/config');
    const data = await res.json();
    _embedAvailability.nanoAvailable = !!data.nanoAvailable;
    _embedAvailability.hasApiKey = !!data.hasApiKey;
  } catch { /* ignore */ }

  // Set disabled states on options
  Array.from(sel.options).forEach(opt => {
    const isNano = opt.value === 'voyage-4-nano';
    if (isNano && !_embedAvailability.nanoAvailable) {
      opt.disabled = true;
      opt.title = "Run 'vai nano setup' to install";
    } else if (!isNano && !_embedAvailability.hasApiKey) {
      opt.disabled = true;
      opt.title = 'API key required';
    } else {
      opt.disabled = false;
      opt.title = '';
    }
  });

  // Auto-default logic (only if no saved value already set)
  const currentVal = sel.value;
  const currentOpt = sel.querySelector(`option[value="${currentVal}"]`);
  if (!currentOpt || currentOpt.disabled) {
    if (_embedAvailability.nanoAvailable) {
      sel.value = 'voyage-4-nano';
    } else if (_embedAvailability.hasApiKey) {
      sel.value = 'voyage-4-large';
    }
    // If neither available, leave first non-disabled or first option
  }

  updateEmbedBadge();

  // Attach change listener (remove existing first to avoid duplication)
  sel.removeEventListener('change', _onEmbedChange);
  sel.addEventListener('change', _onEmbedChange);
}

function _onEmbedChange() {
  updateEmbedBadge();
  saveChatSettingsDebounced();
}

// ── Save Chat Settings ──

let _chatSaveTimer = null;

function saveChatSettings() {
  const settings = {
    provider: document.getElementById('chatProvider').value,
    model: document.getElementById('chatModel').value,
    db: document.getElementById('chatDb').value,
    collection: document.getElementById('chatCollection').value,
    maxDocs: parseInt(document.getElementById('chatMaxDocs').value) || 5,
    rerank: document.getElementById('chatRerank').classList.contains('active'),
    systemPrompt: (document.getElementById('kbSystemPrompt') || document.getElementById('chatSystemPrompt') || {}).value?.trim() || '',
    mode: document.getElementById('chatMode').value,
    embeddingModel: document.getElementById('chatEmbeddingModel').value,
  };
  fetch('/api/chat/config', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(settings),
  }).catch(() => {});
  updateChatStatus();
  // Update not-configured banner
  const notConfigured = document.getElementById('chatNotConfigured');
  if (notConfigured) {
    notConfigured.style.display = (!settings.provider || !settings.db) ? 'flex' : 'none';
  }
  // Refresh origin badges after save
  loadSettingsOrigins();
  flashSaved();
}

function saveChatSettingsDebounced() {
  clearTimeout(_chatSaveTimer);
  _chatSaveTimer = setTimeout(saveChatSettings, 500);
}

function toggleRerank() {
  const btn = document.getElementById('chatRerank');
  if (btn && !btn.disabled) {
    btn.classList.toggle('active');
    saveChatSettings();
  }
}

// flashSaved() is defined earlier (near initSettings) — reused here

// ── Chat Tab ──
let chatSessionId = null;

function updateHealthDots(config) {
  var dots = [
    { id: 'healthDotOllama', available: config.ollamaAvailable,
      tipAvail: 'Ollama: running', tipMissing: 'Ollama: not detected' },
    { id: 'healthDotNano', available: config.nanoAvailable,
      tipAvail: 'voyage-4-nano: ready',
      tipMissing: 'voyage-4-nano: not set up — run "vai nano setup" in your terminal' },
    { id: 'healthDotApiKey', available: config.hasApiKey,
      tipAvail: 'API key: configured', tipMissing: 'API key: not set' },
  ];
  dots.forEach(function(d) {
    var el = document.getElementById(d.id);
    if (el) {
      el.className = 'health-dot ' + (d.available ? 'available' : 'unavailable');
      el.title = d.available ? d.tipAvail : d.tipMissing;
      el.style.cursor = d.available ? '' : 'help';
    }
  });
  // Show nano setup hint below embedding dropdown when nano is unavailable
  var nanoHint = document.getElementById('nanoSetupHint');
  if (!nanoHint) {
    var embedRow = document.getElementById('healthDotNano');
    if (embedRow && embedRow.parentElement) {
      nanoHint = document.createElement('div');
      nanoHint.id = 'nanoSetupHint';
      nanoHint.style.cssText = 'font-size:11px;margin-top:4px;padding:6px 8px;border-radius:6px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.15);';
      nanoHint.innerHTML = '<span style="opacity:0.9">Free local embeddings available!</span> Run <code style="background:rgba(0,212,170,0.15);padding:1px 5px;border-radius:3px;font-size:10px;">vai nano setup</code> in your terminal to enable.';
      embedRow.parentElement.parentElement.appendChild(nanoHint);
    }
  }
  if (nanoHint) {
    nanoHint.style.display = config.nanoAvailable ? 'none' : 'block';
  }
  // Show API key row if no API key (so user sees the red dot and knows to add one)
  var apiKeyRow = document.getElementById('chatApiKeyRow');
  if (apiKeyRow && !config.hasApiKey) {
    apiKeyRow.style.display = '';
  }
}

// ── Onboarding Recommendation Engine ──

function recommendConfig(config) {
  var ollama = !!config.ollamaAvailable;
  var nano = !!config.nanoAvailable;
  var apiKey = !!config.hasApiKey;
  var llmKey = !!config.hasLLMKey;
  var services = { ollama: ollama, nano: nano, apiKey: apiKey, llmKey: llmKey };

  // 1. Best case: Ollama + nano (fully local)
  if (ollama && nano) {
    return { provider: 'ollama', model: null, embeddingModel: 'voyage-4-nano', label: 'Full Local', description: 'Ollama + voyage-4-nano -- free, fully private', services: services };
  }

  // 2. Ollama only (no nano, no API key)
  if (ollama && !nano && !apiKey) {
    return { provider: 'ollama', model: null, embeddingModel: null, label: 'Ollama Only', description: 'Ollama detected -- add nano or API key for embeddings', services: services };
  }

  // 3. Nano + API LLM (has LLM key and nano)
  if (nano && llmKey) {
    return { provider: config.provider || 'anthropic', model: null, embeddingModel: 'voyage-4-nano', label: 'Hybrid', description: 'Cloud LLM + local embeddings -- low cost', services: services };
  }

  // 4. Full API (has API key + LLM key, no local)
  if (apiKey && llmKey) {
    return { provider: config.provider || 'anthropic', model: null, embeddingModel: 'voyage-4-large', label: 'Cloud', description: 'Cloud LLM + Voyage API embeddings', services: services };
  }

  // 5. API key only (Voyage key, no LLM)
  if (apiKey && !llmKey) {
    return { provider: null, model: null, embeddingModel: 'voyage-4-large', label: 'Embeddings Only', description: 'Voyage API key detected -- add LLM provider to chat', services: services };
  }

  // Also handle: Ollama available + API key but no LLM key
  if (ollama && apiKey) {
    return { provider: 'ollama', model: null, embeddingModel: 'voyage-4-large', label: 'Ollama + API', description: 'Ollama + Voyage API embeddings', services: services };
  }

  // 6. Nothing detected
  return { provider: null, model: null, embeddingModel: null, label: 'Setup Required', description: 'Install Ollama or add API keys to get started', services: services };
}

function showOnboardingBanner(config) {
  var banner = document.getElementById('onboardingBanner');
  if (!banner) return;

  // Only show if no provider configured (first-time or unconfigured)
  if (config.provider) {
    banner.style.display = 'none';
    return;
  }

  var rec = recommendConfig(config);

  // Render service detection status
  var servicesEl = document.getElementById('onboardingServices');
  var serviceList = [
    { name: 'Ollama', available: config.ollamaAvailable },
    { name: 'Nano Bridge', available: config.nanoAvailable },
    { name: 'Voyage API', available: config.hasApiKey },
  ];
  servicesEl.innerHTML = serviceList.map(function(s) {
    return '<div class="onboarding-service-item">' +
      '<span class="health-dot ' + (s.available ? 'available' : 'unavailable') + '"></span>' +
      '<span>' + s.name + '</span>' +
      '</div>';
  }).join('');

  // Render recommendation
  var recEl = document.getElementById('onboardingRecommendation');
  recEl.textContent = rec.description;

  // Store recommendation for apply button
  window._onboardingRec = rec;

  // Show/hide apply button based on whether we have a viable config
  var applyBtn = document.getElementById('onboardingApply');
  if (applyBtn) {
    applyBtn.style.display = rec.provider ? '' : 'none';
    applyBtn.textContent = rec.provider ? 'Start with ' + rec.label : 'Start Chatting';
  }

  banner.style.display = '';
}

async function applyRecommendedConfig() {
  var rec = window._onboardingRec;
  if (!rec) return;

  // Set provider
  if (rec.provider) {
    document.getElementById('chatProvider').value = rec.provider;
    await chatProviderChanged(); // populates model dropdown

    // Set model if recommended
    if (rec.model) {
      var modelSelect = document.getElementById('chatModel');
      if (Array.from(modelSelect.options).some(function(o) { return o.value === rec.model; })) {
        modelSelect.value = rec.model;
      }
    }
  }

  // Set embedding model
  if (rec.embeddingModel) {
    var embedSelect = document.getElementById('chatEmbeddingModel');
    if (embedSelect) {
      embedSelect.value = rec.embeddingModel;
      if (typeof updateEmbedBadge === 'function') updateEmbedBadge();
    }
  }

  // Save and dismiss
  saveChatSettings();
  dismissOnboarding();
}

function dismissOnboarding() {
  var banner = document.getElementById('onboardingBanner');
  if (banner) banner.style.display = 'none';
}

async function loadChatConfig() {
  try {
    const res = await fetch('/api/chat/config');
    const data = await res.json();
    if (data.provider) {
      document.getElementById('chatProvider').value = data.provider;
      await chatProviderChanged(); // Populate model dropdown
      // After models load, select the configured model
      if (data.model) {
        const modelSelect = document.getElementById('chatModel');
        const exists = Array.from(modelSelect.options).some(o => o.value === data.model);
        if (exists) {
          modelSelect.value = data.model;
        }
      }
    }
    if (data.db) document.getElementById('chatDb').value = data.db;
    if (data.collection) document.getElementById('chatCollection').value = data.collection;
    if (data.chat?.maxContextDocs) document.getElementById('chatMaxDocs').value = data.chat.maxContextDocs;
    if (data.chat?.rerank === false) document.getElementById('chatRerank').classList.remove('active');
    if (data.chat?.systemPrompt) {
      const spEl = document.getElementById('kbSystemPrompt') || document.getElementById('chatSystemPrompt');
      if (spEl) spEl.value = data.chat.systemPrompt;
    }
    if (data.mode || data.chat?.mode) document.getElementById('chatMode').value = data.mode || data.chat?.mode || 'pipeline';
    // Set embedding model from saved config before running init
    const savedEmbed = data.chat?.embeddingModel || data.embeddingModel;
    if (savedEmbed) {
      const embedSel = document.getElementById('chatEmbeddingModel');
      if (embedSel) embedSel.value = savedEmbed;
    }
    // Initialize embedding dropdown (availability, badges, auto-default)
    await initEmbeddingDropdown();
    updateHealthDots(data);
    updateChatStatus();
    showOnboardingBanner(data);
    // Show not-configured banner if incomplete
    const notConfigured = document.getElementById('chatNotConfigured');
    if (notConfigured) {
      notConfigured.style.display = (!data.provider || !data.db) ? 'flex' : 'none';
    }
  } catch {
    // No config — show not-configured banner and onboarding
    const notConfigured = document.getElementById('chatNotConfigured');
    if (notConfigured) notConfigured.style.display = 'flex';
    showOnboardingBanner({});
  }
}

// ── Chat API Key Management ──

async function loadChatApiKey() {
  if (!window.vai?.llmKey) return;
  const apiKeyInput = document.getElementById('chatApiKey');
  if (!apiKeyInput) return;

  try {
    const key = await window.vai.llmKey.get();
    if (key) {
      // Mask the key by default (show first 4 + last 4)
      const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
      apiKeyInput.value = masked;
      apiKeyInput.dataset.actualKey = key;
      apiKeyInput.dataset.isMasked = 'true';
      apiKeyInput.type = 'password';
    } else {
      apiKeyInput.value = '';
      delete apiKeyInput.dataset.actualKey;
      delete apiKeyInput.dataset.isMasked;
    }
  } catch (err) {
    console.error('Failed to load LLM API key:', err);
  }
}

function toggleChatApiKeyVisibility() {
  const apiKeyInput = document.getElementById('chatApiKey');
  const toggleBtn = document.getElementById('chatApiKeyToggle');
  if (!apiKeyInput || !toggleBtn) return;

  if (apiKeyInput.dataset.isMasked === 'true' && apiKeyInput.dataset.actualKey) {
    // Show actual key
    apiKeyInput.value = apiKeyInput.dataset.actualKey;
    apiKeyInput.type = 'text';
    apiKeyInput.dataset.isMasked = 'false';
    toggleBtn.textContent = '🙈';
    toggleBtn.title = 'Hide API key';
  } else if (apiKeyInput.dataset.actualKey) {
    // Re-mask the key
    const key = apiKeyInput.dataset.actualKey;
    const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
    apiKeyInput.value = masked;
    apiKeyInput.type = 'password';
    apiKeyInput.dataset.isMasked = 'true';
    toggleBtn.textContent = '👁️';
    toggleBtn.title = 'Show API key';
  } else {
    // No stored key, just toggle between text/password for new input
    apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
    toggleBtn.textContent = apiKeyInput.type === 'password' ? '👁️' : '🙈';
    toggleBtn.title = apiKeyInput.type === 'password' ? 'Show API key' : 'Hide API key';
  }
}

async function saveChatApiKey() {
  if (!window.vai?.llmKey) return;
  const apiKeyInput = document.getElementById('chatApiKey');
  const toggleBtn = document.getElementById('chatApiKeyToggle');
  if (!apiKeyInput) return;

  let key = apiKeyInput.value.trim();

  // If showing masked value and it hasn't been modified, no need to save
  if (apiKeyInput.dataset.isMasked === 'true' && apiKeyInput.dataset.actualKey) {
    const currentMasked = apiKeyInput.dataset.actualKey.slice(0, 4) + '•'.repeat(Math.max(0, apiKeyInput.dataset.actualKey.length - 8)) + apiKeyInput.dataset.actualKey.slice(-4);
    if (key === currentMasked) {
      flashSaved();
      return;
    }
  }

  if (!key) {
    // Delete the key if empty
    try {
      await window.vai.llmKey.delete();
      delete apiKeyInput.dataset.actualKey;
      delete apiKeyInput.dataset.isMasked;
      flashSaved();
    } catch (err) {
      console.error('Failed to delete LLM API key:', err);
      alert('Failed to delete API key: ' + err.message);
    }
    return;
  }

  try {
    await window.vai.llmKey.set(key);
    // Mask the newly saved key
    const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
    apiKeyInput.value = masked;
    apiKeyInput.dataset.actualKey = key;
    apiKeyInput.dataset.isMasked = 'true';
    apiKeyInput.type = 'password';
    toggleBtn.textContent = '👁️';
    toggleBtn.title = 'Show API key';
    flashSaved();
  } catch (err) {
    console.error('Failed to save LLM API key:', err);
    alert('Failed to save API key: ' + err.message);
  }
}

function updateChatStatus() {
  const provider = document.getElementById('chatProvider');
  const db = document.getElementById('chatDb').value;
  const collection = document.getElementById('chatCollection').value;
  const mode = document.getElementById('chatMode')?.value || 'pipeline';
  const providerLabel = provider.value
    ? provider.options[provider.selectedIndex].text
    : 'No provider';
  const modeLabel = mode === 'agent' ? 'Agent' : 'Pipeline';
  document.getElementById('chatStatusProvider').textContent = `${providerLabel} (${modeLabel})`;
  document.getElementById('chatStatusDb').textContent =
    (db && collection) ? `${db}.${collection}` : (mode === 'agent' ? 'Agent discovers' : 'No database');
}

async function chatProviderChanged() {
  updateChatStatus();
  const provider = document.getElementById('chatProvider').value;
  const modelSelect = document.getElementById('chatModel');
  const apiKeyRow = document.getElementById('chatApiKeyRow');

  // Show/hide API key row based on provider (not needed for Ollama)
  if (apiKeyRow) {
    apiKeyRow.style.display = (provider === 'anthropic' || provider === 'openai') ? 'flex' : 'none';
    if (provider === 'anthropic' || provider === 'openai') {
      await loadChatApiKey();
    }
  }

  if (!provider) {
    modelSelect.innerHTML = '<option value="">Select provider first</option>';
    return;
  }

  modelSelect.innerHTML = '<option value="">Loading models...</option>';
  modelSelect.disabled = true;

  try {
    const res = await fetch(`/api/chat/models?provider=${provider}`);
    const data = await res.json();
    const models = data.models || [];
    const defaults = { anthropic: 'claude-sonnet-4-5-20250929', openai: 'gpt-4o', ollama: 'llama3.3' };
    const defaultModel = defaults[provider];

    modelSelect.innerHTML = '';

    if (models.length === 0) {
      if (provider === 'ollama') {
        modelSelect.innerHTML = '<option value="">No models found — is Ollama running?</option>';
      } else {
        // Fallback to default
        modelSelect.innerHTML = `<option value="${defaultModel}">${defaultModel}</option>`;
      }
    } else {
      models.forEach(m => {
        const opt = document.createElement('option');
        opt.value = m.id;
        let label = m.name || m.id;
        if (m.context) label += ` (${m.context})`;
        if (m.size) label += ` — ${m.size}`;
        if (m.parameterSize) label += ` [${m.parameterSize}]`;
        if (m.quantization) label += ` ${m.quantization}`;
        opt.textContent = label;
        if (m.id === defaultModel) opt.selected = true;
        modelSelect.appendChild(opt);
      });
    }

    // Always add a custom option
    const customOpt = document.createElement('option');
    customOpt.value = '__custom__';
    customOpt.textContent = '✏️ Custom model...';
    modelSelect.appendChild(customOpt);

  } catch {
    const defaults = { anthropic: 'claude-sonnet-4-5-20250929', openai: 'gpt-4o', ollama: 'llama3.3' };
    modelSelect.innerHTML = `<option value="${defaults[provider] || ''}">${defaults[provider] || 'default'}</option>`;
  }

  modelSelect.disabled = false;
}

// Handle custom model selection
document.getElementById('chatModel')?.addEventListener('change', function() {
  if (this.value === '__custom__') {
    const custom = prompt('Enter model name:');
    if (custom) {
      const opt = document.createElement('option');
      opt.value = custom;
      opt.textContent = custom;
      // Insert before the custom option
      this.insertBefore(opt, this.lastChild);
      this.value = custom;
    } else {
      this.selectedIndex = 0;
    }
  }
});

function chatInputKeydown(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    sendChatMessage();
  }
}

/**
 * Lightweight markdown-to-HTML renderer for assistant chat messages.
 * Handles: fenced code blocks, inline code, headings, bold, italic,
 * links, images, unordered/ordered lists, blockquotes, tables, and <hr>.
 * No external dependencies.
 */
function renderMarkdown(md) {
  // Escape HTML to prevent XSS, then selectively render markdown
  function esc(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

  // Extract fenced code blocks first (preserve content as-is)
  const codeBlocks = [];
  let text = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
    const idx = codeBlocks.length;
    codeBlocks.push(`<pre><code${lang ? ' data-lang="' + esc(lang) + '"' : ''}>${esc(code.replace(/\n$/, ''))}</code></pre>`);
    return '\x00CB' + idx + '\x00';
  });

  // Escape remaining HTML
  text = esc(text);

  // Restore code block placeholders
  text = text.replace(/\x00CB(\d+)\x00/g, (_, i) => codeBlocks[i]);

  // Inline code (must come after escape but before other inline formatting)
  text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>');

  // Headings (must be at line start)
  text = text.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
  text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
  text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
  text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');

  // Horizontal rule
  text = text.replace(/^---+$/gm, '<hr>');

  // Bold and italic
  text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
  text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');

  // Links and images
  text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" style="max-width:100%;border-radius:4px;">');
  text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');

  // Tables (pipe-delimited, GFM-style: header, then |---|---| separator, then body rows)
  text = text.replace(/((?:^\|.+\|?\s*$\n?)+)/gm, (block) => {
    const rows = block.trim().split('\n').filter(r => r.trim());
    if (rows.length < 2) return block;
    // Second row must be separator: only | - : and spaces, and at least 2 pipes
    const sepTrimmed = rows[1].trim();
    const separatorOk = /^[\s|\-:]+$/.test(sepTrimmed) && (sepTrimmed.match(/\|/g) || []).length >= 2;
    if (!separatorOk) return block;
    const headerCells = rows[0].split('|').filter(c => c.trim() !== '').map(c => c.trim());
    let html = '<table><thead><tr>' + headerCells.map(c => '<th>' + c + '</th>').join('') + '</tr></thead><tbody>';
    for (let i = 2; i < rows.length; i++) {
      const cells = rows[i].split('|').filter(c => c.trim() !== '').map(c => c.trim());
      html += '<tr>' + cells.map(c => '<td>' + c + '</td>').join('') + '</tr>';
    }
    html += '</tbody></table>';
    return html;
  });

  // Blockquotes
  text = text.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
  // Merge adjacent blockquotes
  text = text.replace(/<\/blockquote>\n<blockquote>/g, '\n');

  // Unordered lists (lines starting with - or *)
  text = text.replace(/((?:^[\s]*[-*] .+$\n?)+)/gm, (block) => {
    const items = block.trim().split('\n').map(line => {
      const content = line.replace(/^[\s]*[-*] /, '');
      return '<li>' + content + '</li>';
    });
    return '<ul>' + items.join('') + '</ul>';
  });

  // Ordered lists (lines starting with number.)
  text = text.replace(/((?:^[\s]*\d+\. .+$\n?)+)/gm, (block) => {
    const items = block.trim().split('\n').map(line => {
      const content = line.replace(/^[\s]*\d+\. /, '');
      return '<li>' + content + '</li>';
    });
    return '<ol>' + items.join('') + '</ol>';
  });

  // Paragraphs: wrap remaining text lines that aren't already wrapped in block elements
  const blockTags = ['<h1', '<h2', '<h3', '<h4', '<hr', '<pre', '<ul', '<ol', '<table', '<blockquote'];
  const lines = text.split('\n');
  const result = [];
  let paraLines = [];

  function flushPara() {
    if (paraLines.length > 0) {
      const content = paraLines.join('\n').trim();
      if (content) result.push('<p>' + content + '</p>');
      paraLines = [];
    }
  }

  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed === '') {
      flushPara();
    } else if (blockTags.some(tag => trimmed.startsWith(tag))) {
      flushPara();
      result.push(line);
    } else {
      paraLines.push(line);
    }
  }
  flushPara();

  return result.join('\n');
}

/**
 * Tool metadata: icon, label, and a function to summarize the call for the thinking panel.
 */
function lucideIcon(id, size = 16) {
  return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" style="color:var(--accent)"><use href="#${id}"/></svg>`;
}

const TOOL_META = {
  vai_query:       { iconId: 'lg-search',      label: 'RAG Query',      verb: 'Searching',         descFn: a => a.query ? `"${a.query}"` : '' },
  vai_search:      { iconId: 'lg-search-code',  label: 'Vector Search',  verb: 'Searching vectors', descFn: a => a.query ? `"${a.query}"` : '' },
  vai_rerank:      { iconId: 'lg-arrow-up-down', label: 'Rerank',        verb: 'Reranking',         descFn: a => a.query ? `${a.documents?.length || '?'} docs for "${a.query}"` : '' },
  vai_embed:       { iconId: 'lg-lightning',    label: 'Embed',          verb: 'Embedding',         descFn: a => a.text ? `"${a.text.slice(0, 60)}${a.text.length > 60 ? '...' : ''}"` : '' },
  vai_similarity:  { iconId: 'lg-target',       label: 'Similarity',     verb: 'Comparing',         descFn: a => a.text1 ? `two texts` : '' },
  vai_collections: { iconId: 'lg-database',     label: 'Collections',    verb: 'Discovering',       descFn: a => a.db ? `in ${a.db}` : 'available databases' },
  vai_models:      { iconId: 'lg-bot',          label: 'Models',         verb: 'Listing',           descFn: () => 'available models' },
  vai_topics:      { iconId: 'lg-cube',         label: 'Topics',         verb: 'Browsing',          descFn: () => 'educational topics' },
  vai_explain:     { iconId: 'lg-bulb',         label: 'Explain',        verb: 'Explaining',        descFn: a => a.topic || '' },
  vai_estimate:    { iconId: 'lg-coins',        label: 'Cost Estimate',  verb: 'Estimating',        descFn: a => a.docs ? `${a.docs} docs` : '' },
  vai_ingest:      { iconId: 'lg-inbox',        label: 'Ingest',         verb: 'Ingesting',         descFn: a => a.source || 'document' },
};
const DEFAULT_TOOL_META = { iconId: 'lg-config', label: '', verb: 'Running', descFn: () => '' };

/**
 * Create the thinking panel <details> element.
 * Returns { panel, timeline, addStep, finalize }.
 */
function createThinkingPanel() {
  const panel = document.createElement('details');
  panel.className = 'chat-thinking';
  panel.open = true;

  const summary = document.createElement('summary');
  summary.innerHTML =
    '<span class="thinking-icon">' + lucideIcon('lg-brain', 14) + '</span>' +
    '<span class="thinking-label">Thinking</span>' +
    '<span class="thinking-count">0</span>' +
    '<span class="thinking-elapsed"></span>' +
    '<span class="thinking-chevron">\u25B6</span>';
  panel.appendChild(summary);

  const timeline = document.createElement('div');
  timeline.className = 'thinking-timeline';
  panel.appendChild(timeline);

  let stepCount = 0;
  let activeStep = null;
  const startTime = Date.now();
  let elapsedTimer = null;

  // Update the elapsed time in the summary
  function tickElapsed() {
    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
    summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
  }
  elapsedTimer = setInterval(tickElapsed, 200);
  tickElapsed();

  function addStep(data) {
    const meta = TOOL_META[data.name] || DEFAULT_TOOL_META;
    const desc = meta.descFn(data.args || {});

    // Mark previous active step as done
    if (activeStep) {
      activeStep.classList.remove('active');
      activeStep.classList.add('done');
    }

    stepCount++;
    summary.querySelector('.thinking-count').textContent = stepCount;

    const step = document.createElement('div');
    step.className = 'thinking-step active';
    if (data.error) step.className = 'thinking-step error';

    const iconDiv = document.createElement('div');
    iconDiv.className = 'thinking-step-icon';
    iconDiv.innerHTML = lucideIcon(meta.iconId, 14);
    step.appendChild(iconDiv);

    const body = document.createElement('div');
    body.className = 'thinking-step-body';

    const header = document.createElement('div');
    header.className = 'thinking-step-header';
    const nameSpan = document.createElement('span');
    nameSpan.className = 'thinking-step-name';
    nameSpan.textContent = meta.verb + (desc ? ' ' : '') + (meta.label || data.name);
    header.appendChild(nameSpan);

    if (data.timeMs !== undefined) {
      const timeSpan = document.createElement('span');
      timeSpan.className = 'thinking-step-time';
      timeSpan.textContent = data.timeMs + 'ms';
      header.appendChild(timeSpan);
    }
    body.appendChild(header);

    if (desc) {
      const descDiv = document.createElement('div');
      descDiv.className = 'thinking-step-desc';
      descDiv.textContent = desc;
      body.appendChild(descDiv);
    }

    if (data.error) {
      const errDiv = document.createElement('div');
      errDiv.className = 'thinking-step-detail';
      errDiv.style.color = '#e74c3c';
      errDiv.textContent = data.error;
      body.appendChild(errDiv);
    } else if (data.resultSummary) {
      const detailDiv = document.createElement('div');
      detailDiv.className = 'thinking-step-detail';
      detailDiv.innerHTML = data.resultSummary;
      body.appendChild(detailDiv);
    }

    step.appendChild(body);
    timeline.appendChild(step);

    if (!data.error) activeStep = step;

    return step;
  }

  function finalize() {
    clearInterval(elapsedTimer);
    // Final elapsed
    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
    summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
    summary.querySelector('.thinking-label').textContent = 'Thought for ' + elapsed + 's';
    summary.querySelector('.thinking-icon').innerHTML = lucideIcon('lg-sparkles', 14);
    // Mark last step done and collapse
    if (activeStep) {
      activeStep.classList.remove('active');
      activeStep.classList.add('done');
    }
    panel.open = false;
  }

  return { panel, addStep, finalize };
}

function addChatMessage(role, content, sources) {
  const container = document.getElementById('chatMessages');
  const div = document.createElement('div');
  div.className = `chat-message ${role}`;
  const contentSpan = document.createElement('span');
  contentSpan.className = 'chat-message-content';
  contentSpan.textContent = content;
  div.appendChild(contentSpan);

  // Add copy button for assistant messages
  if (role === 'assistant') {
    const copyBtn = document.createElement('button');
    copyBtn.className = 'chat-copy-btn';
    copyBtn.title = 'Copy to clipboard';
    copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>';
    copyBtn.addEventListener('click', () => {
      const text = contentSpan.textContent || contentSpan.innerText;
      navigator.clipboard.writeText(text).then(() => {
        copyBtn.classList.add('copied');
        copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 8.5l3 3 7-7"/></svg>';
        setTimeout(() => {
          copyBtn.classList.remove('copied');
          copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>';
        }, 2000);
      });
    });
    div.appendChild(copyBtn);
  }

  if (sources && sources.length > 0) {
    const details = document.createElement('details');
    details.className = 'chat-sources';
    const summary = document.createElement('summary');
    summary.textContent = `${sources.length} source${sources.length > 1 ? 's' : ''}`;
    details.appendChild(summary);
    const ul = document.createElement('ul');
    sources.forEach(s => {
      const li = document.createElement('li');
      li.textContent = `${s.source} (${s.score?.toFixed(2) || 'N/A'})`;
      ul.appendChild(li);
    });
    details.appendChild(ul);
    div.appendChild(details);
  }

  container.appendChild(div);
  container.scrollTop = container.scrollHeight;
  return div;
}

async function sendChatMessage() {
  const input = document.getElementById('chatInput');
  const query = input.value.trim();
  if (!query) return;

  const provider = document.getElementById('chatProvider').value;
  const modelVal = document.getElementById('chatModel').value;
  const model = (modelVal && modelVal !== '__custom__') ? modelVal : undefined;
  const db = document.getElementById('chatDb').value;
  const collection = document.getElementById('chatCollection').value;
  const maxDocs = parseInt(document.getElementById('chatMaxDocs').value) || 5;
  const rerank = document.getElementById('chatRerank').classList.contains('active');
  const systemPrompt = (document.getElementById('kbSystemPrompt') || document.getElementById('chatSystemPrompt') || {}).value?.trim() || undefined;
  const mode = document.getElementById('chatMode')?.value || 'pipeline';
  const isAgent = mode === 'agent';

  if (!provider) {
    addChatMessage('system-msg', 'Please select an LLM provider in <a href="#" onclick="openChatSettings();return false;">Chat Settings</a>.');
    return;
  }
  if (!isAgent && (!db || !collection)) {
    addChatMessage('system-msg', 'Please configure a database and collection in <a href="#" onclick="openChatSettings();return false;">Chat Settings</a>.');
    return;
  }

  // Show user message
  addChatMessage('user', query);
  input.value = '';
  input.style.height = 'auto';

  // Show typing indicator
  const typing = document.createElement('div');
  typing.className = 'chat-typing';
  typing.textContent = isAgent ? 'Agent working' : 'Thinking';
  document.getElementById('chatMessages').appendChild(typing);

  // Show state indicator
  const stateIndicator = document.getElementById('chatStateIndicator');
  const stateLabel = document.getElementById('chatStateLabel');
  stateIndicator.classList.add('active');
  stateLabel.textContent = 'Validating...';

  // Disable input
  const sendBtn = document.getElementById('chatSendBtn');
  sendBtn.disabled = true;
  input.disabled = true;

  const chatMemoryStrategy = document.getElementById('chatMemoryStrategy')?.value || 'sliding_window';

  try {
    const res = await fetch('/api/chat/message', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, db, collection, provider, model, maxDocs, rerank, systemPrompt, mode, textField: 'content', embeddingModel: document.getElementById('chatEmbeddingModel').value, memoryStrategy: chatMemoryStrategy }),
    });

    if (!res.ok) {
      const err = await res.json();
      typing.remove();
      addChatMessage('system-msg', `Error: ${err.error || 'Request failed'}`);
      return;
    }

    // Parse SSE stream
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let assistantDiv = null;
    let fullText = '';
    let sources = [];
    let thinkingPanel = null;
    let retrievalCostTracked = false;

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      let currentEvent = null;
      for (const line of lines) {
        if (line.startsWith('event: ')) {
          currentEvent = line.slice(7).trim();
          continue;
        }
        if (line.startsWith('data: ')) {
          const data = JSON.parse(line.slice(6));

          if (currentEvent === 'state') {
            try {
              if (stateLabel && data.label) {
                stateLabel.textContent = data.label;
              }
              // Hide on IDLE or STREAMING (streaming has its own visual)
              if (data.to === 'IDLE' || data.to === 'STREAMING') {
                stateIndicator.classList.remove('active');
              }
              // Remove typing indicator once state events arrive
              if (typing && typing.parentNode) {
                typing.remove();
              }
            } catch {}
            currentEvent = null;
            continue;
          }

          if (currentEvent === 'retrieval') {
            typing.textContent = `Retrieved ${data.docs?.length || 0} docs (${data.timeMs}ms)`;
            // Track embedding cost from retrieval (flag to avoid double-counting with done)
            if (data.tokens?.embed) {
              CostTracker.addOperation('chat-embed', 'voyage-4-lite', data.tokens.embed);
              retrievalCostTracked = true;
            }
            if (data.tokens?.rerank) {
              CostTracker.addOperation('chat-rerank', 'rerank-2.5', data.tokens.rerank);
            }
          }

          if (currentEvent === 'tool_call') {
            // Create thinking panel on first tool call
            if (!thinkingPanel) {
              typing.remove();
              thinkingPanel = createThinkingPanel();
              document.getElementById('chatMessages').appendChild(thinkingPanel.panel);
            }
            thinkingPanel.addStep(data);
            document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
          }

          if (currentEvent === 'fallback') {
            // Agent mode fell back to pipeline mode (e.g. model doesn't support tools)
            typing.textContent = data.message || 'Falling back to pipeline mode…';
            // Close any open thinking panel since we're switching to pipeline mode
            if (thinkingPanel) { thinkingPanel.finalize(); thinkingPanel = null; }
            addChatMessage('system-msg', `ℹ️ ${data.message || 'Switched to pipeline mode.'}`);
            // Auto-switch the chat mode dropdown to pipeline so subsequent messages don't re-trigger
            const modeSelect = document.getElementById('chatMode');
            if (modeSelect) { modeSelect.value = 'pipeline'; if (typeof saveChatSettings === 'function') saveChatSettings(); }
          }

          if (currentEvent === 'chunk') {
            if (!assistantDiv) {
              typing.remove();
              assistantDiv = addChatMessage('assistant', '');
            }
            fullText += data.text;
            assistantDiv.querySelector('.chat-message-content').textContent = fullText;
            document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
          }

          if (currentEvent === 'done') {
            // Track costs from done metadata (only if retrieval event didn't already)
            const meta = data.metadata || {};
            if (!retrievalCostTracked && meta.tokens?.embed) {
              CostTracker.addOperation('chat-embed', 'voyage-4-lite', meta.tokens.embed);
            }
            if (!retrievalCostTracked && meta.tokens?.rerank) {
              CostTracker.addOperation('chat-rerank', 'rerank-2.5', meta.tokens.rerank);
            }
            // Track LLM generation cost
            if (meta.tokens?.llmInput || meta.tokens?.llmOutput) {
              CostTracker.addLLMOperation('chat-llm', meta.llmModel, meta.tokens.llmInput, meta.tokens.llmOutput);
            }
            // Agent mode: track tool call tokens from steps/layers
            if (data.steps) {
              for (const step of data.steps) {
                if (step.tokens) CostTracker.addOperation('chat-tool', step.model || 'voyage-4-lite', step.tokens);
              }
            }
            // Finalize the thinking panel (collapse, show elapsed)
            if (thinkingPanel) thinkingPanel.finalize();
            // Render accumulated text as markdown for assistant messages
            if (assistantDiv && fullText) {
              const contentEl = assistantDiv.querySelector('.chat-message-content');
              contentEl.innerHTML = renderMarkdown(fullText);
              contentEl.classList.add('rendered');
            }
            sources = data.sources || [];
            if (sources.length > 0 && assistantDiv) {
              const details = document.createElement('details');
              details.className = 'chat-sources';
              const summary = document.createElement('summary');
              summary.textContent = `${sources.length} source${sources.length > 1 ? 's' : ''}`;
              details.appendChild(summary);
              const ul = document.createElement('ul');
              sources.forEach(s => {
                const li = document.createElement('li');
                li.textContent = `${s.source} (${s.score?.toFixed(2) || 'N/A'})`;
                ul.appendChild(li);
              });
              details.appendChild(ul);
              assistantDiv.appendChild(details);
            }
            // Hide state indicator on done
            stateIndicator.classList.remove('active');
            // Update memory bar
            try {
              const memRes = await fetch('/api/chat/memory');
              if (memRes.ok) {
                const memData = await memRes.json();
                const fill = document.getElementById('chatMemoryFill');
                const pct = document.getElementById('chatMemoryPercent');
                const bar = document.getElementById('chatMemoryBar');
                if (fill && pct && bar) {
                  fill.style.width = memData.utilization.percent + '%';
                  fill.className = 'chat-memory-bar-fill' +
                    (memData.utilization.percent > 80 ? ' danger' : memData.utilization.percent > 60 ? ' warning' : '');
                  pct.textContent = memData.utilization.percent + '% (' + memData.utilization.turnCount + ' turns)';
                  bar.classList.add('active');
                }
              }
            } catch {}
          }

          if (currentEvent === 'error') {
            typing.remove();
            stateIndicator.classList.remove('active');
            addChatMessage('system-msg', `Error: ${data.error}`);
          }

          currentEvent = null;
        }
      }
    }

    // Remove typing and state indicator if still present
    if (typing.parentNode) typing.remove();
    stateIndicator.classList.remove('active');

    // Capture for export
    if (!window._chatExportData) {
      window._chatExportData = {
        sessionId: 'playground-' + Date.now().toString(36),
        startedAt: new Date().toISOString(),
        provider, model: modelVal, collection: collection ? `${db}.${collection}` : undefined,
        turns: [],
      };
    }
    window._chatExportData.turns.push({ role: 'user', content: query, timestamp: new Date().toISOString() });
    if (fullText) {
      window._chatExportData.turns.push({
        role: 'assistant', content: fullText, timestamp: new Date().toISOString(),
        context: sources.map(s => ({ source: s.source, score: s.score })),
      });
    }

  } catch (err) {
    if (typing.parentNode) typing.remove();
    if (thinkingPanel) thinkingPanel.finalize();
    addChatMessage('system-msg', `Error: ${err.message}`);
  } finally {
    sendBtn.disabled = false;
    input.disabled = false;
    input.focus();
  }
}

// Auto-resize chat input
document.getElementById('chatInput')?.addEventListener('input', function() {
  this.style.height = 'auto';
  this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});

// Load chat config and settings origins on init
loadChatConfig();
loadSettingsOrigins();

// ── Expose functions to global scope for onclick handlers ──
window.setGenerateMode = setGenerateMode;
window.copyGeneratedCode = copyGeneratedCode;
window.generateCode = generateCode;
window.updateGenComponents = updateGenComponents;
window.createScaffold = createScaffold;
window.pickScaffoldDirectory = pickScaffoldDirectory;
window.downloadScaffoldZip = downloadScaffoldZip;
window.sendChatMessage = sendChatMessage;
window.openChatSettings = openChatSettings;
window.toggleKBPanel = function() {
  if (window.kbUI) {
    window.kbUI.togglePanelCollapse();
    const btn = document.getElementById('chatKbToggle');
    if (btn) btn.classList.toggle('active', window.kbUI.isPanelOpen);
  }
};
window.kbToggleSection = function(key) {
  if (window.kbUI) window.kbUI.toggleSection(key);
};
window.kbSwitchIngestTab = function(tab) {
  const tabs = ['files', 'paste', 'url'];
  const tabIds = { files: 'kbIngestTabFiles', paste: 'kbIngestTabPaste', url: 'kbIngestTabURL' };
  tabs.forEach(t => {
    const el = document.getElementById(tabIds[t]);
    if (el) el.style.display = t === tab ? 'block' : 'none';
    const tabEl = document.querySelector(`.kb-ingest-tab[data-kb-ingest-tab="${t}"]`);
    if (tabEl) {
      tabEl.classList.toggle('active', t === tab);
      tabEl.style.borderBottom = t === tab ? '2px solid var(--accent)' : '2px solid transparent';
    }
  });
};
window.kbOpenConfigSection = function() {
  // Ensure panel is open
  const panel = document.getElementById('kbPanel');
  if (panel && panel.classList.contains('collapsed')) {
    window.toggleKBPanel();
  }
  // Expand config section if collapsed
  if (window.kbUI && !window.kbUI.sectionStates.config) {
    window.kbUI.toggleSection('config');
  }
  // Scroll to config section
  const configSection = document.querySelector('.kb-section[data-kb-section="config"]');
  if (configSection) configSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
window.switchSettingsSection = switchSettingsSection;
window.updateChatStatus = updateChatStatus;
window.chatInputKeydown = chatInputKeydown;
window.chatProviderChanged = chatProviderChanged;
window.applyRecommendedConfig = applyRecommendedConfig;
window.dismissOnboarding = dismissOnboarding;
window.loadChatApiKey = loadChatApiKey;
window.toggleChatApiKeyVisibility = toggleChatApiKeyVisibility;
window.saveChatApiKey = saveChatApiKey;
window.loadMcpStatus = loadMcpStatus;
window.mcpInstall = mcpInstall;
window.mcpUninstall = mcpUninstall;

// ── Start ──
init();
})();
</script>
<!-- Cost Help Modal -->
<div class="cost-modal-overlay" id="costHelpModal">
  <div class="cost-modal">
    <button class="cost-modal-close" id="costHelpClose">&times;</button>

    <h2>📐 How the Cost Calculator Works</h2>

    <p>Voyage AI charges per <strong>million tokens</strong> processed. A token is roughly ¾ of a word.
    The calculator estimates your total embedding cost based on how many documents you embed
    and how many queries you run over time.</p>

    <h3>💡 Simple Mode</h3>
    <p>Compares the per-model query cost for a given volume. Useful for quick "which model is cheapest?" checks.</p>
    <div class="formula">
      <span class="label">Daily cost =</span><br>
      <span class="accent">tokens_per_query</span> × <span class="accent">queries_per_day</span> ÷ 1,000,000 × <span class="accent">price_per_M_tokens</span><br><br>
      <span class="label">Monthly cost =</span> daily cost × 30
    </div>

    <h3>📊 RAG Planner Mode</h3>
    <p>Models the full cost of a Retrieval-Augmented Generation (RAG) pipeline, separating
    the <strong>one-time</strong> document ingestion cost from the <strong>recurring</strong> query cost.</p>

    <div class="formula">
      <span class="label">Document embedding (one-time):</span><br>
      <span class="accent">doc_cost</span> = num_docs × tokens_per_doc ÷ 1,000,000 × <span class="accent">doc_model_price</span><br><br>
      <span class="label">Query embedding (monthly):</span><br>
      <span class="accent">query_cost/mo</span> = queries_per_month × tokens_per_query ÷ 1,000,000 × <span class="accent">query_model_price</span><br><br>
      <span class="label">Projected total:</span><br>
      <span class="accent">total</span> = doc_cost + (query_cost/mo × <span class="accent">months</span>)
    </div>

    <h3>⚖️ Three Strategies Compared</h3>
    <ul>
      <li><strong>Symmetric</strong> — same model for documents and queries. Simple but expensive at scale,
      because query-heavy workloads pay the full model price on every request.</li>
      <li><strong>Asymmetric (★ Recommended)</strong> — use a high-quality model (e.g. <code>voyage-4-large</code>)
      for documents and a cheaper model (e.g. <code>voyage-4-lite</code>) for queries.
      This works because Voyage 4 models share the same embedding space — vectors from different
      models are directly comparable.</li>
      <li><strong>Asymmetric + Local</strong> — embed documents via the API, but run queries locally using
      <code>voyage-4-nano</code> on HuggingFace (free). Query cost drops to $0.</li>
    </ul>

    <h3>🔗 Shared Embedding Space</h3>
    <p>The Voyage 4 family (<code>voyage-4-large</code>, <code>voyage-4</code>, <code>voyage-4-lite</code>,
    <code>voyage-4-nano</code>) all produce vectors in the <em>same geometric space</em>.
    A document embedded with <code>voyage-4-large</code> can be searched with a query embedded by
    <code>voyage-4-lite</code> — cosine similarity still works correctly. This is what makes
    asymmetric strategies possible.</p>

    <div class="example">
      <strong>Example:</strong> 100K docs × 500 tok = 50M doc tokens<br>
      1M queries/mo × 30 tok = 30M query tokens/mo<br><br>
      <strong>Symmetric</strong> (voyage-4-large @ $0.18/1M):<br>
      &nbsp;&nbsp;Docs: $9.00 + Queries: $5.40/mo × 12 = <strong>$73.80</strong><br><br>
      <strong>Asymmetric</strong> (large docs + lite queries @ $0.05/1M):<br>
      &nbsp;&nbsp;Docs: $9.00 + Queries: $1.50/mo × 12 = <strong>$27.00</strong><br><br>
      &nbsp;&nbsp;Savings: <strong>63%</strong> — same document quality, cheaper queries.
    </div>

    <h3>📋 Per-Model Table</h3>
    <p>The bottom table shows what it would cost to use each model symmetrically (same model for
    docs and queries). The relative bar shows cost compared to the most expensive option.
    Use this to understand the price spread across the full model lineup.</p>

    <h3>🎯 Key Assumptions</h3>
    <ul>
      <li>Token counts are estimates — actual counts depend on your text. Use <code>vai chunk --stats</code> to measure real token counts.</li>
      <li>Document embedding is a one-time cost (you embed once, search many times).</li>
      <li>Re-embedding (e.g. updated docs) is not modeled — add a buffer if your corpus changes frequently.</li>
      <li>Reranking costs are separate and not included here. Reranking is priced per query pair, not per token.</li>
    </ul>

    <p style="margin-top:20px;font-size:12px;color:var(--text-muted);">
      CLI equivalent: <code>vai estimate --docs 100K --queries 1M --doc-model voyage-4-large --query-model voyage-4-lite</code>
    </p>
  </div>
</div>
<!-- 🕹️ Vector Space Invaders -->
<div id="vsiOverlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,30,43,0.92);z-index:99999;align-items:center;justify-content:center;flex-direction:column;gap:12px;">
  <canvas id="vsiCanvas" width="600" height="500" style="border:1px solid #3D4F58;border-radius:8px;image-rendering:pixelated;"></canvas>
  <div style="color:#889397;font-size:11px;font-family:monospace;text-align:center;">← → or A/D steer &nbsp;|&nbsp; SPACE pulse &nbsp;|&nbsp; ESC exit</div>
</div>

<!-- 🐛 Bug Reporter -->
<style>
.sidebar-bug-link{
  display:flex;align-items:center;gap:6px;
  background:none;border:none;cursor:pointer;
  font-size:11px;color:var(--text-muted);
  padding:4px 0;font-family:inherit;
  transition:color .15s;
}
.sidebar-bug-link:hover{color:var(--text);}
.sidebar-bug-link svg{width:12px;height:12px;flex-shrink:0;opacity:0.6;}
.sidebar-bug-link:hover svg{opacity:1;}
.bug-reporter-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999}
.bug-reporter-modal{background:var(--bg-surface);border-radius:12px;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
.bug-reporter-header{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border)}
.bug-reporter-header h2{flex:1;margin:0;font-size:18px;font-weight:600;color:var(--accent-text)}
.bug-reporter-header .close-btn{background:none;border:none;color:var(--text-muted);font-size:24px;cursor:pointer;padding:4px 8px;border-radius:4px}
.bug-reporter-header .close-btn:hover{background:rgba(255,255,255,0.1);color:var(--text)}
.bug-reporter-form{padding:20px}
.bug-reporter-form .form-group{margin-bottom:16px}
.bug-reporter-form label{display:block;font-size:13px;font-weight:500;color:var(--text-muted);margin-bottom:6px}
.bug-reporter-form input,.bug-reporter-form textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input);color:var(--text);font-size:14px;font-family:inherit}
.bug-reporter-form input:focus,.bug-reporter-form textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
.bug-reporter-form textarea{resize:vertical;min-height:80px}
.bug-env-info{padding:12px;background:var(--accent-glow);border-radius:8px;font-size:12px;margin-bottom:16px;color:var(--accent)}
.bug-env-info code{color:var(--text-muted);font-size:11px}
.bug-error{padding:10px 12px;background:rgba(255,107,107,0.15);border:1px solid rgba(255,107,107,0.3);border-radius:8px;color:var(--error);font-size:13px;margin-bottom:16px}
.bug-actions{display:flex;gap:12px}
.bug-actions button{flex:1;padding:12px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;border:none}
.bug-actions .primary{background:linear-gradient(135deg,var(--accent),#5CE8CC);color:#000}
.bug-actions .primary:hover{box-shadow:0 4px 12px rgba(0,212,170,0.4)}
.bug-actions .primary:disabled{opacity:0.6;cursor:not-allowed}
.bug-actions .secondary{background:rgba(255,255,255,0.1);color:var(--text);border:1px solid var(--border)}
.bug-actions .secondary:hover{background:rgba(255,255,255,0.15)}
.bug-success{padding:40px 20px;text-align:center}
.bug-success .icon{width:60px;height:60px;border-radius:50%;background:linear-gradient(135deg,var(--accent),#5CE8CC);color:#000;font-size:32px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
.bug-success h3{margin:0 0 12px;color:var(--accent-text);font-size:20px}
.bug-success p{margin:8px 0;color:var(--text-muted)}
.bug-success code{background:rgba(255,255,255,0.1);padding:4px 8px;border-radius:4px;font-size:12px;color:var(--accent)}

/* ── Cost Dashboard ── */
#costStatusBar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
  background: var(--bg-surface); border-top: 1px solid var(--border);
  padding: 8px 16px; font-size: 13px; color: var(--text-dim);
  display: flex; align-items: center; gap: 12px; backdrop-filter: blur(8px);
  opacity: 0.95; font-family: 'SF Mono', 'Fira Code', monospace;
  overflow-x: auto; overflow-y: hidden; white-space: nowrap;
  scrollbar-width: none;
}
#costStatusBar::-webkit-scrollbar { display: none; }
#costStatusBar > span, #costStatusBar > button { flex-shrink: 0; }
#costStatusBar .cost-sep { color: var(--border); }
#costStatusBar .cost-savings { color: var(--green, #00D4AA); }
#costStatusBar .cost-more { color: var(--red, #FF6960); }
#costStatusBar .cost-val { color: var(--accent); font-weight: 600; }
#costDetailsToggle {
  margin-left: auto; cursor: pointer; color: var(--accent);
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 3px 10px; font-size: 12px; font-family: inherit;
}
#costDetailsToggle:hover { background: var(--bg-card); }
#costDetailPanel {
  position: fixed; bottom: 40px; left: 0; right: 0; z-index: 300;
  background: var(--bg-surface); border-top: 1px solid var(--border);
  max-height: 0; overflow: hidden; transition: max-height 0.3s ease;
}
#costDetailPanel.open { max-height: 50vh; overflow-y: auto; }
#costDetailPanel .cost-panel-inner { padding: 16px 20px; }
#costDetailPanel table {
  width: 100%; border-collapse: collapse; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace;
}
#costDetailPanel th { text-align: left; color: var(--text-muted); font-weight: 500; padding: 4px 8px; border-bottom: 1px solid var(--border); }
#costDetailPanel td { padding: 4px 8px; color: var(--text-dim); border-bottom: 1px solid var(--border); }
#costDetailPanel .cost-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
#costDetailPanel .cost-panel-header h3 { margin: 0; font-size: 14px; color: var(--text); }
#costResetBtn {
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 3px 10px; font-size: 12px; color: var(--text-dim); cursor: pointer; font-family: inherit;
}
#costResetBtn:hover { background: var(--bg-card); color: var(--text); }
.cost-projection { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.cost-projection span { color: var(--accent); font-weight: 600; }
.cost-toast {
  position: fixed; bottom: 52px; left: 50%; transform: translateX(-50%);
  background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 16px; font-size: 12px; color: var(--text-dim); z-index: 1000;
  font-family: 'SF Mono', 'Fira Code', monospace; white-space: nowrap;
  animation: costToastIn 0.2s ease, costToastOut 0.3s ease 2.7s forwards;
  pointer-events: none;
}
@keyframes costToastIn { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
@keyframes costToastOut { from { opacity: 1; } to { opacity: 0; transform: translateX(-50%) translateY(-10px); } }
body { padding-bottom: 44px; }
</style>

<!-- Bug button moved to sidebar footer -->

<div class="bug-reporter-overlay" id="bugOverlay" style="display:none">
  <div class="bug-reporter-modal">
    <div class="bug-reporter-header">
      <span style="font-size:28px">🐛</span>
      <h2>Report a Bug</h2>
      <button class="close-btn" id="bugClose">×</button>
    </div>
    <div class="bug-reporter-form" id="bugForm">
      <div class="form-group">
        <label>Title *</label>
        <input type="text" id="bugTitle" placeholder="Brief description of the bug" maxlength="200">
      </div>
      <div class="form-group">
        <label>Description *</label>
        <textarea id="bugDescription" placeholder="What happened? What did you expect?" rows="4" maxlength="5000"></textarea>
      </div>
      <div class="form-group">
        <label>Steps to Reproduce</label>
        <textarea id="bugSteps" placeholder="1. Go to...&#10;2. Click on...&#10;3. See error" rows="3" maxlength="2000"></textarea>
      </div>
      <div class="form-group">
        <label>Email (optional, for follow-up)</label>
        <input type="email" id="bugEmail" placeholder="your@email.com">
      </div>
      <div class="bug-env-info">
        <span>📋 Environment will be included:</span><br>
        <code id="bugEnvInfo">Loading...</code>
      </div>
      <div class="bug-error" id="bugError" style="display:none"></div>
      <div class="bug-actions">
        <button class="primary" id="bugSubmit">Submit Bug Report</button>
        <button class="secondary" id="bugGithub">Open GitHub Issue</button>
      </div>
    </div>
    <div class="bug-success" id="bugSuccess" style="display:none">
      <div class="icon">✓</div>
      <h3>Bug Reported!</h3>
      <p>Bug ID: <code id="bugResultId"></code></p>
      <p>Thank you for helping improve Vai!</p>
      <div class="bug-actions" style="justify-content:center;margin-top:24px">
        <button class="primary" id="bugSuccessClose">Close</button>
      </div>
    </div>
  </div>
</div>

<!-- ========== WORKFLOW VISUALIZER JS ========== -->
<script>
// ── Workflow Visualizer ──

// escapeHtml is defined in the main IIFE scope and not accessible here — redeclare it
function escapeHtml(str) {
  if (str == null) return '';
  const s = typeof str === 'string' ? str : String(str);
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

const WF_NODE_META = {
  query:       { icon: '\u{1F50D}', label: 'RAG Query',       color: '#40E0FF', category: 'retrieval' },
  search:      { icon: '\u{1F50E}', label: 'Vector Search',   color: '#40E0FF', category: 'retrieval' },
  rerank:      { icon: '\u{1F3C6}', label: 'Rerank',          color: '#40E0FF', category: 'retrieval' },
  ingest:      { icon: '\u{1F4E5}', label: 'Ingest',          color: '#40E0FF', category: 'retrieval' },
  embed:       { icon: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', label: 'Embed',           color: '#B388FF', category: 'embedding' },
  similarity:  { icon: 'M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4', label: 'Similarity',      color: '#B388FF', category: 'embedding' },
  collections: { icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z', label: 'Collections',     color: '#00D4AA', category: 'management' },
  models:      { icon: 'M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6M9 14l2 2 4-4', label: 'Models',          color: '#00D4AA', category: 'management' },
  estimate:    { icon: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6', label: 'Cost Estimate',   color: '#FFB74D', category: 'utility' },
  explain:     { icon: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z', label: 'Explain',         color: '#FFB74D', category: 'utility' },
  topics:      { icon: 'M12 6V2M8 18H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-4M12 18v4M8 22h8', label: 'Topics',          color: '#FFB74D', category: 'utility' },
  merge:       { icon: 'M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9v6M18 15l-6-6-6 6', label: 'Merge',           color: '#90A4AE', category: 'control' },
  filter:      { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Filter',          color: '#90A4AE', category: 'control' },
  transform:   { icon: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16', label: 'Transform',       color: '#90A4AE', category: 'control' },
  generate:    { icon: 'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z', label: 'Generate', color: '#69F0AE', category: 'generation' },
  query:       { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Query',           color: '#64B5F6', category: 'retrieval' },
  rerank:      { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank',          color: '#CE93D8', category: 'retrieval' },
  search:      { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Search',          color: '#64B5F6', category: 'retrieval' },
  ingest:      { icon: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3', label: 'Ingest',          color: '#4DB6AC', category: 'management' },
  // Phase 1-3: New workflow nodes
  conditional: { icon: 'M12 3l9 9-9 9-9-9z', label: 'Conditional', color: '#90A4AE', category: 'control', shape: 'diamond' },
  loop:        { icon: 'M17 1l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3', label: 'Loop', color: '#90A4AE', category: 'control' },
  template:    { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8', label: 'Template', color: '#90A4AE', category: 'control' },
  chunk:       { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 15h6M9 11h6M9 19h4', label: 'Chunk', color: '#00D4AA', category: 'processing' },
  aggregate:   { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Aggregate', color: '#00D4AA', category: 'processing' },
  http:        { icon: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z', label: 'HTTP Request', color: '#F5A623', category: 'integration' },
  // Code search tools (voyage-code-3)
  code_index:        { icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2zM12 11v6M9 14h6', label: 'Code Index', color: '#06B6D4', category: 'code' },
  code_search:       { icon: 'M10 20l4-16M18 8l4 4-4 4M6 16l-4-4 4-4', label: 'Code Search', color: '#06B6D4', category: 'code' },
  code_query:        { icon: 'M10 20l4-16M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Code Query', color: '#06B6D4', category: 'code' },
  code_find_similar: { icon: 'M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M12 8v8M8 12h8', label: 'Find Similar', color: '#06B6D4', category: 'code' },
  code_status:       { icon: 'M22 12h-4l-3 9L9 3l-3 9H2', label: 'Code Status', color: '#06B6D4', category: 'code' },
};

// Fallback icon (gear) for unknown workflow node types
const WF_FALLBACK_ICON = 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2zM12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0';

const WF_NODE_W = 180;
const WF_NODE_H = 64;
const WF_LAYER_GAP = 260;
const WF_NODE_GAP = 100;
const WF_PAD = 80;
const WF_PORT_R = 5; // Port circle radius

let wfState = {
  workflows: [],
  examples: [],
  activeWorkflow: null,
  selectedNodeId: null,
  executionState: {},
  executionResults: {},
  nodePositions: {},
  layers: [],
  graph: {},
  zoom: 1,
  panX: 0,
  panY: 0,
  isPanning: false,
  panStart: { x: 0, y: 0 },
  executing: false,
  // Builder state
  builderMode: false,
  draggingEdge: null,
  dragNode: null,
  dirtyFlag: false,
  outputFormat: 'auto',
};

// ── Library ──
async function wfLoadLibrary() {
  try {
    const [wfRes, exRes] = await Promise.all([
      fetch('/api/workflows'),
      fetch('/api/workflows/examples'),
    ]);
    const wfData = await wfRes.json();
    const exData = await exRes.json();
    wfState.workflows = wfData.workflows || [];
    wfState.official = wfData.official || [];
    wfState.community = wfData.community || [];
    wfState.examples = exData.examples || [];
    wfRenderLibrary();
  } catch (err) {
    const list = document.getElementById('wfLibraryList');
    if (list) list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">Failed to load workflows</div>';
  }
}

// ── Library Search ──
let _wfLibSearchTimer = null;
function wfOnLibrarySearch() {
  clearTimeout(_wfLibSearchTimer);
  _wfLibSearchTimer = setTimeout(() => wfRenderLibrary(), 150);
  const input = document.getElementById('wfLibrarySearch');
  const clear = document.getElementById('wfLibrarySearchClear');
  if (input && clear) clear.style.display = input.value ? 'block' : 'none';
}
function wfClearLibrarySearch() {
  const input = document.getElementById('wfLibrarySearch');
  if (input) { input.value = ''; input.focus(); }
  const clear = document.getElementById('wfLibrarySearchClear');
  if (clear) clear.style.display = 'none';
  wfRenderLibrary();
}

function wfGetLibSectionStates() {
  const state = wfGetLayoutState();
  return (state.library && state.library.sections) || { builtin: true, examples: true, catalog: false, community: false };
}
function wfSaveLibSectionState(key, expanded) {
  const state = wfGetLayoutState();
  if (!state.library) state.library = { open: true };
  if (!state.library.sections) state.library.sections = { builtin: true, examples: true, catalog: false, community: false };
  state.library.sections[key] = expanded;
  try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
}
function wfToggleLibSection(key) {
  const states = wfGetLibSectionStates();
  const newVal = !states[key];
  wfSaveLibSectionState(key, newVal);
  // Toggle UI directly
  const header = document.querySelector(`.wf-lib-section-header[data-section="${key}"]`);
  const body = document.querySelector(`.wf-lib-section-body[data-section="${key}"]`);
  if (header && body) {
    header.classList.toggle('expanded', newVal);
    if (newVal) {
      body.style.maxHeight = body.scrollHeight + 'px';
      setTimeout(() => { body.style.maxHeight = 'none'; }, 200);
    } else {
      body.style.maxHeight = body.scrollHeight + 'px';
      requestAnimationFrame(() => { body.style.maxHeight = '0px'; });
    }
  }
}

function wfRenderLibrary() {
  const list = document.getElementById('wfLibraryList');
  if (!list) return;

  const searchInput = document.getElementById('wfLibrarySearch');
  const query = (searchInput ? searchInput.value : '').toLowerCase().trim();
  const sectionStates = wfGetLibSectionStates();

  function matchesSearch(w) {
    if (!query) return true;
    const name = (w.name || '').toLowerCase();
    const desc = (w.description || '').toLowerCase();
    return name.includes(query) || desc.includes(query);
  }

  function renderItem(w, prefix) {
    const displayName = prefix + w.name.replace(/^@vaicli\/vai-workflow-/, '').replace(/^vai-workflow-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
    const authorLine = w.author ? `<span style="color:var(--text-muted);font-size:10px;">by ${w.author}${w.version ? ' · v' + w.version : ''}</span>` : '';
    const tagLine = (w.tags || []).length ? `<div style="margin-top:2px;font-size:10px;color:var(--text-muted);">${w.tags.join(' · ')}</div>` : '';
    const infoBtn = (prefix === '✓ ' || prefix === '🌐 ')
      ? `<div class="wf-library-item-info" onclick="event.stopPropagation();wfShowWorkflowInfo('${w.name}')" title="Workflow info">i</div>`
      : '';
    return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
      ${infoBtn}
      <div class="wf-library-item-name">${displayName}</div>
      <div class="wf-library-item-desc">${w.description || ''}</div>
      ${authorLine}${tagLine}
    </div>`;
  }

  function renderSection(key, label, items, prefix) {
    const filtered = items.filter(matchesSearch);
    if (filtered.length === 0) return '';
    const expanded = sectionStates[key] !== false;
    const chevron = '&#9654;';
    const itemsHtml = filtered.map(w => renderItem(w, prefix)).join('');
    return `<div class="wf-library-section" data-section="${key}">
      <div class="wf-lib-section-header ${expanded ? 'expanded' : ''}" data-section="${key}" onclick="wfToggleLibSection('${key}')">
        <span class="wf-chevron">${chevron}</span>
        <span>${label}</span>
        <span class="wf-section-count">(${filtered.length})</span>
      </div>
      <div class="wf-lib-section-body" data-section="${key}" style="max-height:${expanded ? 'none' : '0px'};">
        ${itemsHtml}
      </div>
    </div>`;
  }

  let html = '';

  // Built-in
  html += renderSection('builtin', 'Built-in', wfState.workflows, '');

  // Examples
  const examples = wfState.examples || [];
  if (examples.length > 0) {
    html += renderSection('examples', 'Examples', examples, '');
  }

  // Official catalog
  const official = wfState.official || [];
  html += renderSection('catalog', 'Official Catalog (@vaicli)', official, '✓ ');

  // Community
  const community = wfState.community || [];
  html += renderSection('community', 'Community', community, '🌐 ');

  if (!html) {
    html = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
  }

  // Install from npm button
  html += `<div style="padding:8px 12px;">
    <button onclick="wfShowInstallDialog()" style="background:none;border:1px dashed var(--border);color:var(--accent);cursor:pointer;padding:6px 12px;border-radius:6px;font-size:11px;width:100%;text-align:center;">+ Install from npm</button>
  </div>`;

  list.innerHTML = html;
}

// ── Install Dialog ──
function wfShowInstallDialog() {
  let modal = document.getElementById('wfInstallModal');
  if (!modal) {
    modal = document.createElement('div');
    modal.id = 'wfInstallModal';
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;';
    modal.innerHTML = `
      <div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;padding:24px;width:480px;max-height:70vh;display:flex;flex-direction:column;">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
          <h3 style="margin:0;font-size:16px;">Install Workflow Package</h3>
          <button onclick="wfCloseInstallDialog()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;">✕</button>
        </div>
        <div style="display:flex;gap:8px;margin-bottom:12px;">
          <input id="wfInstallSearch" type="text" placeholder="Search vai workflows on npm..." style="flex:1;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;" onkeydown="if(event.key==='Enter')wfSearchNpm()">
          <button onclick="wfSearchNpm()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">Search</button>
        </div>
        <div id="wfInstallResults" style="overflow-y:auto;flex:1;min-height:100px;"></div>
      </div>`;
    document.body.appendChild(modal);
    modal.addEventListener('click', (e) => { if (e.target === modal) wfCloseInstallDialog(); });
  }
  modal.style.display = 'flex';
  document.getElementById('wfInstallSearch').value = '';
  document.getElementById('wfInstallResults').innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Search for vai-workflow packages on npm</div>';
  document.getElementById('wfInstallSearch').focus();
}

function wfCloseInstallDialog() {
  const modal = document.getElementById('wfInstallModal');
  if (modal) modal.style.display = 'none';
}

function wfCompareVersions(a, b) {
  const pa = (a || '0.0.0').split('.').map(Number);
  const pb = (b || '0.0.0').split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pa[i] || 0) > (pb[i] || 0)) return 1;
    if ((pa[i] || 0) < (pb[i] || 0)) return -1;
  }
  return 0;
}

async function wfSearchNpm() {
  const query = document.getElementById('wfInstallSearch').value.trim();
  const results = document.getElementById('wfInstallResults');
  if (!query) return;
  results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Searching...</div>';
  try {
    const res = await fetch('/api/workflows/community/search?q=' + encodeURIComponent(query) + '&limit=10');
    const data = await res.json();
    if (!data.results || data.results.length === 0) {
      results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">No packages found</div>';
      return;
    }
    results.innerHTML = data.results.map(r => {
      const isOfficial = r.name.startsWith('@vaicli/');
      const badge = isOfficial ? '<span style="background:var(--accent);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:6px;">OFFICIAL</span>' : '';
      const installedPkg = [...(wfState.official||[]), ...(wfState.community||[])].find(w => w.name === r.name);
      const installed = !!installedPkg;
      const installedVersion = installedPkg?.version;
      const npmVersion = r.version;
      const hasUpdate = installed && npmVersion && installedVersion && npmVersion !== installedVersion && wfCompareVersions(npmVersion, installedVersion) > 0;
      let btn;
      if (hasUpdate) {
        btn = `<button onclick="wfInstallPkg('${r.name.replace(/'/g,"\\'")}')" style="padding:4px 10px;background:#F59E0B;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;" title="Installed: v${installedVersion}">Update</button>`;
      } else if (installed) {
        btn = '<button disabled style="padding:4px 10px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text-muted);font-size:11px;cursor:default;">Installed</button>';
      } else {
        btn = `<button onclick="wfInstallPkg('${r.name.replace(/'/g,"\\'")}')" style="padding:4px 10px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">Install</button>`;
      }
      const versionInfo = hasUpdate
        ? `v${installedVersion} → <span style="color:#F59E0B;font-weight:500;">v${npmVersion}</span>`
        : `v${npmVersion || '?'}`;
      return `<div style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;">
        <div style="flex:1;min-width:0;">
          <div style="font-size:13px;font-weight:500;">${r.name}${badge}</div>
          <div style="font-size:11px;color:var(--text-muted);margin-top:2px;">${r.description || ''}</div>
          <div style="font-size:10px;color:var(--text-muted);margin-top:2px;">${versionInfo}</div>
        </div>
        <div style="margin-left:12px;flex-shrink:0;">${btn}</div>
      </div>`;
    }).join('');
  } catch (err) {
    results.innerHTML = `<div style="padding:16px;color:#f44;font-size:12px;text-align:center;">Search failed: ${err.message}</div>`;
  }
}

async function wfInstallPkg(name) {
  const results = document.getElementById('wfInstallResults');
  // Find the clicked button and show installing state
  const btns = results.querySelectorAll('button');
  let clickedBtn = null;
  btns.forEach(b => {
    if ((b.textContent === 'Install' || b.textContent === 'Update') && !clickedBtn) {
      // Find the button in the same row as the package name
      const row = b.closest('div[style*="border-bottom"]');
      if (row && row.textContent.includes(name)) {
        clickedBtn = b;
        b.textContent = 'Installing...';
        b.disabled = true;
        b.style.opacity = '0.7';
      }
    }
  });

  try {
    const res = await fetch('/api/workflows/community/install', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name })
    });
    const text = await res.text();
    let data;
    try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response' }; }
    if (data.success) {
      if (clickedBtn) { clickedBtn.textContent = '✓ Installed'; clickedBtn.style.opacity = '1'; }
      await wfLoadLibrary();
      await wfSearchNpm();
    } else {
      const errMsg = data.error || 'Unknown error';
      if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
      console.error('Install failed:', errMsg);
      alert('Install failed: ' + errMsg);
    }
  } catch (err) {
    if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
    console.error('Install error:', err);
    alert('Install failed: ' + err.message);
  }
}

function wfToggleExamples(btn) {
  const content = btn.nextElementSibling;
  const isOpen = content.style.display !== 'none';
  content.style.display = isOpen ? 'none' : '';
  btn.classList.toggle('open', !isOpen);
}

async function wfSelectWorkflow(name) {
  // Highlight in library
  document.querySelectorAll('.wf-library-item').forEach(el => {
    el.classList.toggle('active', el.dataset.wfName === name);
  });

  try {
    const res = await fetch('/api/workflows/' + encodeURIComponent(name));
    const data = await res.json();
    wfState.activeWorkflow = data.definition;
    wfState.activeWorkflowName = name;
    wfState.builderMode = false;
    wfState.selectedNodeId = null;
    wfState.executionState = {};
    wfState.executionResults = {};
    wfSetToolbarEnabled(true);
    document.getElementById('wfCanvasEmpty').style.display = 'none';
    await wfRenderWorkflow(data.definition);
    wfSwitchLibTab('library');
    wfOpenInspector();
    wfUpdateInspector();
  } catch (err) {
    console.error('Failed to load workflow:', err);
  }
}

// ── Load workflow from file ──
function wfLoadFromFile() {
  document.getElementById('wfFileInput').click();
}

function wfHandleFileLoad(event) {
  const file = event.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = async (e) => {
    try {
      const definition = JSON.parse(e.target.result);
      if (!definition.steps || !Array.isArray(definition.steps)) {
        alert('Invalid workflow: missing "steps" array');
        return;
      }
      // Validate via server
      const valRes = await fetch('/api/workflows/validate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ definition }),
      });
      const valData = await valRes.json();
      if (!valData.valid) {
        alert('Workflow validation failed:\n' + (valData.errors || []).join('\n'));
        return;
      }
      // Deselect any library item
      document.querySelectorAll('.wf-library-item.active').forEach(el => el.classList.remove('active'));
      // Load the workflow
      wfState.activeWorkflow = definition;
      wfState.selectedNodeId = null;
      wfState.executionState = {};
      wfState.executionResults = {};
      wfSetToolbarEnabled(true);
      document.getElementById('wfCanvasEmpty').style.display = 'none';
      wfHideExecStatus();
      await wfRenderWorkflow(definition);
      wfOpenInspector();
      wfUpdateInspector();
    } catch (err) {
      alert('Failed to parse workflow file: ' + err.message);
    }
  };
  reader.readAsText(file);
  // Reset file input so the same file can be re-loaded
  event.target.value = '';
}

// ── DAG Layout + SVG Rendering ──
/**
 * Sugiyama-style DAG auto-layout: barycenter crossing minimization + neighbor-aware placement.
 * Takes layers (from Kahn's topological sort) and graph (dependency map),
 * reorders nodes within each layer to minimize edge crossings,
 * then assigns Y coordinates so nodes sit near their connected neighbors.
 *
 * @param {string[][]} layers - layers[i] = [stepIds that can run in parallel]
 * @param {Object} graph - { stepId: [depStepIds...] } (incoming edges)
 * @returns {Object} positions - { stepId: { x, y } }
 */
function wfAutoLayout(layers, graph) {
  if (!layers || layers.length === 0) return {};

  // Build adjacency: forward (outgoing) and backward (incoming) neighbor lists
  const incoming = {}; // stepId -> [ids in previous layers that connect to it]
  const outgoing = {}; // stepId -> [ids in next layers it connects to]
  const allIds = new Set(layers.flat());
  for (const id of allIds) { incoming[id] = []; outgoing[id] = []; }

  for (const [stepId, deps] of Object.entries(graph)) {
    if (!deps || !Array.isArray(deps)) continue;
    for (const rawDep of deps) {
      const dep = rawDep.replace(/^!/, '');
      if (allIds.has(dep) && allIds.has(stepId)) {
        incoming[stepId].push(dep);
        outgoing[dep].push(stepId);
      }
    }
  }

  // Index: which layer is each node in?
  const layerOf = {};
  layers.forEach((layer, li) => layer.forEach(id => { layerOf[id] = li; }));

  // Work on mutable copies of each layer's ordering
  const ordered = layers.map(l => [...l]);

  // Barycenter crossing minimization (multi-pass, alternating direction)
  // Barycenter = average position of a node's neighbors in the adjacent layer.
  // Sorting by barycenter within each layer reduces crossings.
  const NUM_PASSES = 4;

  for (let pass = 0; pass < NUM_PASSES; pass++) {
    if (pass % 2 === 0) {
      // Forward pass: for each layer (left to right), sort by barycenter of incoming neighbors
      for (let li = 1; li < ordered.length; li++) {
        const prevOrder = {};
        ordered[li - 1].forEach((id, idx) => { prevOrder[id] = idx; });

        ordered[li].sort((a, b) => {
          const baryA = wfBarycenter(a, incoming, prevOrder);
          const baryB = wfBarycenter(b, incoming, prevOrder);
          return baryA - baryB;
        });
      }
    } else {
      // Backward pass: for each layer (right to left), sort by barycenter of outgoing neighbors
      for (let li = ordered.length - 2; li >= 0; li--) {
        const nextOrder = {};
        ordered[li + 1].forEach((id, idx) => { nextOrder[id] = idx; });

        ordered[li].sort((a, b) => {
          const baryA = wfBarycenter(a, outgoing, nextOrder);
          const baryB = wfBarycenter(b, outgoing, nextOrder);
          return baryA - baryB;
        });
      }
    }
  }

  // Assign X coordinates (fixed per layer)
  // Assign Y coordinates using neighbor-aware placement:
  // First pass: assign evenly spaced Y (centered).
  // Second pass: nudge each node toward the average Y of its neighbors.
  const positions = {};
  const maxLayerSize = Math.max(...ordered.map(l => l.length));
  const totalH = maxLayerSize * (WF_NODE_H + WF_NODE_GAP);

  // Initial even spacing (centered vertically)
  ordered.forEach((layer, li) => {
    const x = WF_PAD + li * WF_LAYER_GAP;
    const layerH = layer.length * WF_NODE_H + (layer.length - 1) * WF_NODE_GAP;
    const startY = WF_PAD + (totalH - layerH) / 2;
    layer.forEach((stepId, ni) => {
      positions[stepId] = { x, y: startY + ni * (WF_NODE_H + WF_NODE_GAP) };
    });
  });

  // Neighbor-aware Y nudging (iterative relaxation)
  // Pulls nodes toward the average Y of their connected neighbors while maintaining order and minimum gap.
  const RELAX_PASSES = 3;
  const RELAX_STRENGTH = 0.4; // How much to move toward neighbor average (0=none, 1=full)

  for (let rp = 0; rp < RELAX_PASSES; rp++) {
    for (let li = 0; li < ordered.length; li++) {
      const layer = ordered[li];
      if (layer.length <= 1) continue;

      // Compute ideal Y for each node (average Y of all neighbors)
      const idealY = {};
      for (const id of layer) {
        const neighbors = [...(incoming[id] || []), ...(outgoing[id] || [])];
        if (neighbors.length === 0) { idealY[id] = positions[id].y; continue; }
        const avgY = neighbors.reduce((sum, nid) => sum + (positions[nid]?.y || 0), 0) / neighbors.length;
        idealY[id] = avgY;
      }

      // Nudge toward ideal, then enforce minimum gap and original order
      for (const id of layer) {
        const current = positions[id].y;
        const target = idealY[id];
        positions[id].y = current + (target - current) * RELAX_STRENGTH;
      }

      // Re-sort by Y to maintain the barycenter ordering, then enforce minimum gaps
      const sortedLayer = [...layer].sort((a, b) => positions[a].y - positions[b].y);
      const minGap = WF_NODE_H + WF_NODE_GAP;

      // Push apart any overlaps (top-down)
      for (let i = 1; i < sortedLayer.length; i++) {
        const prev = positions[sortedLayer[i - 1]].y;
        const curr = positions[sortedLayer[i]].y;
        if (curr - prev < minGap) {
          positions[sortedLayer[i]].y = prev + minGap;
        }
      }

      // Re-center the layer vertically to keep it balanced
      const layerTop = positions[sortedLayer[0]].y;
      const layerBottom = positions[sortedLayer[sortedLayer.length - 1]].y + WF_NODE_H;
      const layerMid = (layerTop + layerBottom) / 2;
      const canvasMid = WF_PAD + totalH / 2;
      const shift = canvasMid - layerMid;
      for (const id of sortedLayer) {
        positions[id].y += shift;
      }
    }
  }

  return positions;
}

/**
 * Compute barycenter (average position) of a node's neighbors in an adjacent layer.
 * If a node has no neighbors in the given orderMap, returns Infinity to push it to the end.
 */
function wfBarycenter(nodeId, neighborMap, orderMap) {
  const neighbors = (neighborMap[nodeId] || []).filter(n => orderMap[n] !== undefined);
  if (neighbors.length === 0) return Infinity;
  return neighbors.reduce((sum, n) => sum + orderMap[n], 0) / neighbors.length;
}

async function wfRenderWorkflow(definition) {
  const svg = document.getElementById('wf-canvas');
  // Clear previous nodes and edges (keep defs)
  svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());

  // Get execution plan from server
  let layers, graph;
  try {
    const res = await fetch('/api/workflows/plan', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ definition }),
    });
    const data = await res.json();
    layers = data.layers;
    graph = data.graph;
  } catch (err) {
    console.error('Failed to get execution plan:', err);
    return;
  }

  wfState.layers = layers;
  wfState.graph = graph;

  // Build step lookup
  const stepMap = {};
  definition.steps.forEach(s => { stepMap[s.id] = s; });

  // Auto-layout: crossing minimization + neighbor-aware placement
  const positions = wfAutoLayout(layers, graph);
  wfState.nodePositions = positions;

  // Build port-visibility maps: which nodes have input deps, which have dependents
  const nodeHasDeps = {};       // node has incoming edges (show input port)
  const nodeHasDependents = {}; // node has outgoing edges (show output port)
  for (const [stepId, deps] of Object.entries(graph)) {
    if (!deps || !Array.isArray(deps)) continue;
    deps.forEach(rawDepId => {
      const depId = rawDepId.replace(/^!/, '');
      if (positions[depId] && positions[stepId]) {
        nodeHasDeps[stepId] = true;
        nodeHasDependents[depId] = true;
      }
    });
  }

  // Build conditional branch maps for edge styling
  const elseBranchEdges = new Set(); // "fromId->toId" for else branches
  if (definition.steps) {
    for (const step of definition.steps) {
      if (step.tool === 'conditional' && step.inputs) {
        const elseSteps = step.inputs.else || [];
        for (const eId of elseSteps) {
          elseBranchEdges.add(`${step.id}->${eId}`);
        }
      }
    }
  }

  // Draw edges first (behind nodes)
  const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  edgeGroup.classList.add('wf-edge-group');
  for (const [stepId, deps] of Object.entries(graph)) {
    if (!deps || !Array.isArray(deps)) continue;
    deps.forEach(rawDepId => {
      // Strip negation prefix (e.g., "!similarity_check" -> "similarity_check")
      const depId = rawDepId.replace(/^!/, '');
      if (positions[depId] && positions[stepId]) {
        const edge = wfDrawEdge(depId, stepId, positions);
        // Mark else-branch edges with dashed style
        const edgeKey = `${depId}->${stepId}`;
        if (elseBranchEdges.has(edgeKey)) {
          edge.querySelector('.wf-edge')?.classList.add('wf-edge--else');
        }
        // Mark edges to skipped nodes
        if (wfState.executionState[stepId] === 'skipped') {
          edge.querySelector('.wf-edge')?.classList.add('wf-edge--skipped');
        }
        edgeGroup.appendChild(edge);
      }
    });
  }
  svg.appendChild(edgeGroup);

  // Draw nodes
  for (const step of definition.steps) {
    const pos = positions[step.id];
    if (!pos) continue;
    const state = wfState.executionState[step.id] || 'idle';
    const hasDeps = !!nodeHasDeps[step.id];
    const hasDependents = !!nodeHasDependents[step.id];
    const nodeGroup = wfDrawNode(step, pos.x, pos.y, state, hasDeps, hasDependents);
    svg.appendChild(nodeGroup);
  }

  // Set viewBox to fit
  wfFitToView();
}

function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
  const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666', category: 'unknown' };
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  g.classList.add('wf-node');
  if (state !== 'idle') g.classList.add('wf-node--' + state);
  if (wfState.selectedNodeId === step.id) g.classList.add('selected');
  g.dataset.stepId = step.id;
  g.setAttribute('transform', `translate(${x}, ${y})`);
  g.addEventListener('click', (e) => {
    e.stopPropagation();
    wfSelectNode(step.id);
  });

  // Builder: node drag
  if (wfState.builderMode) {
    g.style.cursor = 'move';
    g.addEventListener('mousedown', (e) => {
      // Don't drag if clicking a port
      if (e.target.classList.contains('wf-port')) return;
      e.stopPropagation();
      const svg = document.getElementById('wf-canvas');
      const rect = svg.getBoundingClientRect();
      const startSvgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
      const startSvgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
      const pos = wfState.nodePositions[step.id];
      if (!pos) return;
      const offX = startSvgX - pos.x;
      const offY = startSvgY - pos.y;
      wfState.dragNode = step.id;

      function onMove(ev) {
        const mx = (ev.clientX - rect.left) / wfState.zoom + wfState.panX;
        const my = (ev.clientY - rect.top) / wfState.zoom + wfState.panY;
        wfState.nodePositions[step.id] = { x: mx - offX, y: my - offY };
        wfRefreshNodes();
      }
      function onUp() {
        wfState.dragNode = null;
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
      }
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  }

  // Background shape: diamond for conditional, rounded rect for others
  if (meta.shape === 'diamond') {
    const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
    const cx = WF_NODE_W / 2, cy = WF_NODE_H / 2;
    const rx = WF_NODE_W / 2 + 8, ry = WF_NODE_H / 2 + 4;
    diamond.setAttribute('points', `${cx},${cy - ry} ${cx + rx},${cy} ${cx},${cy + ry} ${cx - rx},${cy}`);
    diamond.setAttribute('fill', meta.color);
    diamond.setAttribute('stroke', meta.color);
    diamond.setAttribute('opacity', '0.85');
    g.appendChild(diamond);
  } else {
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('width', WF_NODE_W);
    rect.setAttribute('height', WF_NODE_H);
    rect.setAttribute('rx', '6');
    rect.setAttribute('ry', '6');
    rect.setAttribute('fill', meta.color);
    rect.setAttribute('stroke', meta.color);
    rect.setAttribute('opacity', '0.85');
    g.appendChild(rect);
  }

  // Input port (left side): show in builder mode always, otherwise only if has deps
  const showInPort = wfState.builderMode || hasDeps;
  if (showInPort) {
    const inPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    inPort.classList.add('wf-port', 'wf-port-in');
    if (wfState.builderMode) inPort.classList.add('wf-port-builder');
    inPort.setAttribute('cx', 0);
    inPort.setAttribute('cy', WF_NODE_H / 2);
    inPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
    inPort.setAttribute('stroke', meta.color);
    inPort.setAttribute('stroke-width', '2');
    inPort.setAttribute('pointer-events', 'all');
    if (wfState.builderMode) {
      inPort.addEventListener('mouseup', () => {
        if (wfState.draggingEdge) wfEdgeDropOnInput(step.id);
      });
    }
    g.appendChild(inPort);
  }

  // Output port (right side): show in builder mode always, otherwise only if has dependents
  const showOutPort = wfState.builderMode || hasDependents;
  if (showOutPort) {
    const outPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    outPort.classList.add('wf-port', 'wf-port-out');
    if (wfState.builderMode) outPort.classList.add('wf-port-builder');
    outPort.setAttribute('cx', WF_NODE_W);
    outPort.setAttribute('cy', WF_NODE_H / 2);
    outPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
    outPort.setAttribute('fill', meta.color);
    outPort.setAttribute('stroke-width', '2');
    outPort.setAttribute('pointer-events', 'all');
    if (wfState.builderMode) {
      outPort.addEventListener('mousedown', (e) => {
        e.stopPropagation();
        const pos = wfState.nodePositions[step.id];
        if (!pos) return;
        wfEdgeDragStart(step.id, pos.x + WF_NODE_W, pos.y + WF_NODE_H / 2);
      });
    }
    g.appendChild(outPort);
  }

  // Icon (Lucide SVG)
  const iconG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  iconG.classList.add('wf-node-icon');
  const iconSize = 18;
  const ix = 22 - iconSize / 2;
  const iy = WF_NODE_H / 2 - iconSize / 2;
  iconG.setAttribute('transform', `translate(${ix},${iy}) scale(${iconSize / 24})`);
  const fullD = meta.icon || WF_FALLBACK_ICON;
  const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  iconPath.setAttribute('d', fullD);
  iconPath.setAttribute('fill', 'none');
  iconPath.setAttribute('stroke', 'currentColor');
  iconPath.setAttribute('stroke-width', '1.75');
  iconPath.setAttribute('stroke-linecap', 'round');
  iconPath.setAttribute('stroke-linejoin', 'round');
  iconG.appendChild(iconPath);
  g.appendChild(iconG);

  // Label (step name, truncated)
  const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  label.classList.add('wf-node-label');
  label.setAttribute('x', WF_NODE_W / 2 + 10);
  label.setAttribute('y', WF_NODE_H / 2 - 7);
  const maxChars = 20;
  const displayName = (step.name || step.id).length > maxChars
    ? (step.name || step.id).slice(0, maxChars - 2) + '..'
    : (step.name || step.id);
  label.textContent = displayName;
  g.appendChild(label);

  // Tool badge
  const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  badge.classList.add('wf-node-badge');
  badge.setAttribute('x', WF_NODE_W / 2 + 10);
  badge.setAttribute('y', WF_NODE_H / 2 + 10);
  badge.textContent = meta.label;
  g.appendChild(badge);

  // Condition indicator
  if (step.condition) {
    const cond = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    cond.classList.add('wf-node-condition');
    cond.setAttribute('x', WF_NODE_W - 8);
    cond.setAttribute('y', 14);
    cond.textContent = '\u26A1';
    g.appendChild(cond);
  }

  // Status overlay (for execution)
  if (state === 'completed') {
    const check = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    check.classList.add('wf-node-status');
    check.setAttribute('x', WF_NODE_W - 18);
    check.setAttribute('y', WF_NODE_H / 2);
    check.textContent = '\u2713';
    g.appendChild(check);
  } else if (state === 'error') {
    const errIcon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    errIcon.classList.add('wf-node-status');
    errIcon.setAttribute('x', WF_NODE_W - 18);
    errIcon.setAttribute('y', WF_NODE_H / 2);
    errIcon.textContent = '\u2717';
    g.appendChild(errIcon);
  } else if (state === 'skipped') {
    const skip = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    skip.classList.add('wf-node-status');
    skip.setAttribute('x', WF_NODE_W - 18);
    skip.setAttribute('y', WF_NODE_H / 2);
    skip.textContent = '\u2500';
    g.appendChild(skip);
  }

  // Time badge (if completed)
  const result = wfState.executionResults[step.id];
  if (result && result.timeMs !== undefined) {
    const time = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    time.classList.add('wf-node-time');
    time.setAttribute('x', WF_NODE_W / 2);
    time.setAttribute('y', WF_NODE_H + 16);
    time.textContent = result.timeMs + 'ms';
    g.appendChild(time);
  }

  return g;
}

function wfDrawEdge(fromId, toId, positions) {
  const from = positions[fromId];
  const to = positions[toId];
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  g.classList.add('wf-edge-group-item');
  g.dataset.from = fromId;
  g.dataset.to = toId;

  // Port positions: output port on right side of source, input port on left side of target
  const x1 = from.x + WF_NODE_W;
  const y1 = from.y + WF_NODE_H / 2;
  const x2 = to.x;
  const y2 = to.y + WF_NODE_H / 2;

  // Detect backward edges (target is same column or to the left)
  const isBackward = x2 <= x1;

  let d;
  if (isBackward) {
    // Route backward edges: go down from output, loop under/over, come in from left of target
    const midY = Math.max(from.y + WF_NODE_H, to.y + WF_NODE_H) + 40;
    d = `M ${x1} ${y1} C ${x1 + 50} ${y1}, ${x1 + 50} ${midY}, ${(x1 + x2) / 2} ${midY} C ${x2 - 50} ${midY}, ${x2 - 50} ${y2}, ${x2} ${y2}`;
  } else {
    // Normal left-to-right bezier
    const dx = Math.abs(x2 - x1);
    const tension = Math.max(dx * 0.4, 40);
    d = `M ${x1} ${y1} C ${x1 + tension} ${y1}, ${x2 - tension} ${y2}, ${x2} ${y2}`;
  }

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.classList.add('wf-edge');
  if (isBackward) path.classList.add('wf-edge--backward');
  path.setAttribute('d', d);

  // Set state-based classes
  const fromState = wfState.executionState[fromId];
  const toState = wfState.executionState[toId];
  if (toState === 'running') path.classList.add('wf-edge--active');
  else if (fromState === 'completed' && (toState === 'completed' || toState === 'skipped')) path.classList.add('wf-edge--complete');

  g.appendChild(path);
  return g;
}

// ── Panel Layout State ──
let _wfLayoutSaveTimer = null;
function wfGetLayoutState() {
  try {
    const s = localStorage.getItem('vai-workflow-layout');
    if (s) return JSON.parse(s);
  } catch(e) {}
  return { library: { open: true }, properties: { open: true } };
}
function wfSaveLayoutState() {
  clearTimeout(_wfLayoutSaveTimer);
  _wfLayoutSaveTimer = setTimeout(() => {
    const lib = document.getElementById('wfLibrary');
    const insp = document.getElementById('wfInspector');
    const existing = wfGetLayoutState();
    const state = {
      library: { open: lib ? !lib.classList.contains('collapsed') : true, sections: (existing.library && existing.library.sections) || { builtin: true, examples: true, catalog: false, community: false } },
      properties: { open: insp ? !insp.classList.contains('collapsed') : true, sections: (existing.properties && existing.properties.sections) || { inputs: true, steps: false, output: false } }
    };
    try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
  }, 300);
}
// ── Properties Accordion State ──
function wfGetAccordionStates() {
  const state = wfGetLayoutState();
  return (state.properties && state.properties.sections) || { inputs: true, steps: false, output: false };
}
function wfSaveAccordionState(key, expanded) {
  const state = wfGetLayoutState();
  if (!state.properties) state.properties = { open: true };
  if (!state.properties.sections) state.properties.sections = { inputs: true, steps: false, output: false };
  state.properties.sections[key] = expanded;
  try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
}
function wfToggleAccordion(key, headerEl) {
  const body = document.querySelector(`.wf-accordion-body[data-acc="${key}"]`);
  if (!body) return;
  const expanded = headerEl.classList.contains('expanded');
  const newVal = !expanded;
  headerEl.classList.toggle('expanded', newVal);
  if (newVal) {
    body.style.maxHeight = body.scrollHeight + 'px';
    setTimeout(() => { body.style.maxHeight = 'none'; }, 200);
  } else {
    body.style.maxHeight = body.scrollHeight + 'px';
    requestAnimationFrame(() => { body.style.maxHeight = '0px'; });
  }
  wfSaveAccordionState(key, newVal);
}

function wfSyncPanelUI() {
  const lib = document.getElementById('wfLibrary');
  const insp = document.getElementById('wfInspector');
  const libBtn = document.getElementById('wfToggleLibBtn');
  const propsBtn = document.getElementById('wfTogglePropsBtn');
  const leftHandle = document.getElementById('wfEdgeHandleLeft');
  const rightHandle = document.getElementById('wfEdgeHandleRight');
  const libCollapsed = lib && lib.classList.contains('collapsed');
  const inspCollapsed = insp && insp.classList.contains('collapsed');
  if (libBtn) libBtn.classList.toggle('active', !libCollapsed);
  if (propsBtn) propsBtn.classList.toggle('active', !inspCollapsed);
  if (leftHandle) leftHandle.classList.toggle('visible', !!libCollapsed);
  if (rightHandle) rightHandle.classList.toggle('visible', !!inspCollapsed);
}
function wfRestoreLayoutState() {
  const state = wfGetLayoutState();
  const lib = document.getElementById('wfLibrary');
  const insp = document.getElementById('wfInspector');
  if (lib) lib.classList.toggle('collapsed', !state.library.open);
  if (insp) insp.classList.toggle('collapsed', !state.properties.open);
  wfSyncPanelUI();
}

// ── Library Toggle ──
function wfToggleLibrary() {
  const panel = document.getElementById('wfLibrary');
  if (!panel) return;
  panel.classList.toggle('collapsed');
  wfSyncPanelUI();
  wfSaveLayoutState();
}

// ── Inspector Toggle ──
function wfToggleInspector() {
  const panel = document.getElementById('wfInspector');
  if (!panel) return;
  panel.classList.toggle('collapsed');
  wfSyncPanelUI();
  wfSaveLayoutState();
}

function wfOpenInspector() {
  const panel = document.getElementById('wfInspector');
  if (!panel || !panel.classList.contains('collapsed')) return;
  panel.classList.remove('collapsed');
  wfSyncPanelUI();
  wfSaveLayoutState();
}

// ── Sidebar Collapse ──
function toggleSidebarCollapse() {
  const sidebar = document.querySelector('.sidebar');
  if (!sidebar) return;
  sidebar.classList.toggle('collapsed');
  // Persist
  const state = wfGetLayoutState();
  if (!state.sidebar) state.sidebar = {};
  state.sidebar.collapsed = sidebar.classList.contains('collapsed');
  try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
}
function restoreSidebarState() {
  const state = wfGetLayoutState();
  if (state.sidebar && state.sidebar.collapsed) {
    const sidebar = document.querySelector('.sidebar');
    if (sidebar) sidebar.classList.add('collapsed');
  }
}
restoreSidebarState();

// ── Keyboard Shortcuts (Workflows tab only) ──
let _wfLastOpenedPanel = null; // 'library' or 'properties'
(function() {
  const origToggleLib = window.wfToggleLibrary;
  window.wfToggleLibrary = function() {
    origToggleLib();
    const lib = document.getElementById('wfLibrary');
    if (lib && !lib.classList.contains('collapsed')) _wfLastOpenedPanel = 'library';
  };
  const origToggleInsp = window.wfToggleInspector;
  window.wfToggleInspector = function() {
    origToggleInsp();
    const insp = document.getElementById('wfInspector');
    if (insp && !insp.classList.contains('collapsed')) _wfLastOpenedPanel = 'properties';
  };
})();

document.addEventListener('keydown', function(e) {
  // Only active when Workflows tab is visible
  const wfTab = document.getElementById('tab-workflows');
  if (!wfTab || wfTab.style.display === 'none' || !wfTab.classList.contains('active')) {
    // Check if tab-btn-workflows is active instead
    const wfBtn = document.getElementById('tab-btn-workflows');
    if (!wfBtn || !wfBtn.classList.contains('active')) return;
  }

  const mod = e.metaKey || e.ctrlKey;
  if (!mod && e.key !== 'Escape') return;

  // Cmd/Ctrl + Shift + B → Toggle sidebar
  if (mod && e.shiftKey && (e.key === 'B' || e.key === 'b')) {
    e.preventDefault();
    toggleSidebarCollapse();
    return;
  }
  // Cmd/Ctrl + B → Toggle Library
  if (mod && !e.shiftKey && (e.key === 'B' || e.key === 'b')) {
    e.preventDefault();
    wfToggleLibrary();
    return;
  }
  // Cmd/Ctrl + I → Toggle Properties
  if (mod && (e.key === 'I' || e.key === 'i')) {
    e.preventDefault();
    wfToggleInspector();
    return;
  }
  // Cmd/Ctrl + \ → Collapse both panels
  if (mod && e.key === '\\') {
    e.preventDefault();
    const lib = document.getElementById('wfLibrary');
    const insp = document.getElementById('wfInspector');
    if (lib && !lib.classList.contains('collapsed')) { lib.classList.add('collapsed'); }
    if (insp && !insp.classList.contains('collapsed')) { insp.classList.add('collapsed'); }
    wfSyncPanelUI();
    wfSaveLayoutState();
    return;
  }
  // Escape → Close most recently opened panel
  if (e.key === 'Escape' && !mod) {
    if (_wfLastOpenedPanel === 'library') {
      const lib = document.getElementById('wfLibrary');
      if (lib && !lib.classList.contains('collapsed')) { wfToggleLibrary(); _wfLastOpenedPanel = null; return; }
    }
    if (_wfLastOpenedPanel === 'properties') {
      const insp = document.getElementById('wfInspector');
      if (insp && !insp.classList.contains('collapsed')) { wfToggleInspector(); _wfLastOpenedPanel = null; return; }
    }
    // Fallback: close properties then library
    const insp = document.getElementById('wfInspector');
    if (insp && !insp.classList.contains('collapsed')) { wfToggleInspector(); return; }
    const lib = document.getElementById('wfLibrary');
    if (lib && !lib.classList.contains('collapsed')) { wfToggleLibrary(); return; }
  }
});

// ── Node Selection ──
function wfSelectNode(stepId) {
  wfState.selectedNodeId = stepId;
  // Update visual selection
  document.querySelectorAll('.wf-node').forEach(n => {
    n.classList.toggle('selected', n.dataset.stepId === stepId);
  });
  wfOpenInspector();
  wfUpdateInspector();
}

function wfDeselectNode() {
  wfState.selectedNodeId = null;
  document.querySelectorAll('.wf-node.selected').forEach(n => n.classList.remove('selected'));
  wfUpdateInspector();
}

// ── Inspector ──
function wfUpdateInspector() {
  const body = document.getElementById('wfInspectorBody');
  const header = document.getElementById('wfInspectorHeader');
  if (!body || !header) return;

  const def = wfState.activeWorkflow;
  if (!def) {
    header.textContent = 'Inspector';
    body.innerHTML = '<div class="wf-inspector-empty">Select a workflow to view details</div>';
    return;
  }

  try {
  if (!wfState.selectedNodeId) {
    // Show workflow-level info
    header.textContent = def.name || 'Workflow';
    let html = '';

    if (wfState.builderMode) {
      // Builder: editable workflow fields
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">Name</div>
        <input class="wf-inspector-input" value="${escapeHtml(def.name || '')}" onchange="wfEditWorkflowField('name', this.value); document.getElementById('wfInspectorHeader').textContent = this.value || 'Workflow';">
      </div>`;
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">Description</div>
        <textarea class="wf-inspector-input" rows="2" style="resize:vertical;" onchange="wfEditWorkflowField('description', this.value)">${escapeHtml(def.description || '')}</textarea>
      </div>`;
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">Steps</div>
        <div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}</div>
      </div>`;
      if (def.steps.length > 0) {
        html += `<div class="wf-inspector-section">
          <div class="wf-inspector-section-title">Output Mapping</div>
          <textarea class="wf-inspector-input" rows="3" style="resize:vertical;font-family:monospace;font-size:11px;" onchange="try { wfEditWorkflowField('output', JSON.parse(this.value)); } catch(e) {}">${escapeHtml(JSON.stringify(def.output || {}, null, 2))}</textarea>
        </div>`;
      }
      html += `<div class="wf-inspector-section" style="margin-top:8px;">
        <div style="font-size:10px;color:var(--text-muted);">Add steps from the Palette tab, then drag between ports to connect them.</div>
      </div>`;
    } else {
      // Read-only: Description (above accordion)
      if (def.description) {
        html += `<div class="wf-inspector-section">
          <div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
        </div>`;
      }

      // Get accordion states from localStorage
      const accStates = wfGetAccordionStates();
      const hasDone = !!wfState.executionResults._done;

      // ── INPUTS Accordion ──
      const inputKeys = def.inputs ? Object.keys(def.inputs) : [];
      const inputsExpanded = accStates.inputs !== false;
      html += `<div class="wf-accordion-header ${inputsExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('inputs', this)">
        <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> INPUTS</span>
        <span class="wf-acc-summary">${inputKeys.length} field${inputKeys.length !== 1 ? 's' : ''}</span>
      </div>
      <div class="wf-accordion-body" data-acc="inputs" style="max-height:${inputsExpanded ? 'none' : '0px'};">
        <div class="wf-accordion-body-inner">`;
      if (inputKeys.length > 0) {
        for (const [key, spec] of Object.entries(def.inputs)) {
          const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
          html += `<div style="margin-bottom:12px;">
            <div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
            <div style="font-size:10px;color:var(--text-muted);margin-bottom:4px;">${escapeHtml(spec.description || spec.type || '')}</div>
            <input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? escapeHtml(String(spec.default)) : ''}">
          </div>`;
        }
      } else {
        html += '<div style="font-size:12px;color:var(--text-muted);">No inputs defined</div>';
      }
      html += '</div></div>';

      // ── STEPS Accordion ──
      const stepCount = def.steps.length;
      const layerCount = wfState.layers ? wfState.layers.length : 0;
      const stepsSummary = `${stepCount} step${stepCount !== 1 ? 's' : ''}${layerCount ? ' · ' + layerCount + ' layer' + (layerCount !== 1 ? 's' : '') : ''}`;
      const stepsExpanded = accStates.steps === true;
      html += `<div class="wf-accordion-header ${stepsExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('steps', this)">
        <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> STEPS</span>
        <span class="wf-acc-summary">${stepsSummary}</span>
      </div>
      <div class="wf-accordion-body" data-acc="steps" style="max-height:${stepsExpanded ? 'none' : '0px'};">
        <div class="wf-accordion-body-inner">`;
      for (const step of def.steps) {
        const meta = WF_NODE_META[step.tool] || { color: '#666' };
        const result = wfState.executionResults[step.id];
        const timeStr = result && result.timeMs != null ? result.timeMs + 'ms' : '—';
        html += `<div class="wf-step-row">
          <span class="wf-step-dot" style="background:${meta.color}"></span>
          <span class="wf-step-name">${escapeHtml(step.name || step.id)}</span>
          <span class="wf-step-time">${timeStr}</span>
        </div>`;
      }
      html += '</div></div>';

      // ── OUTPUT Accordion ──
      const outputExpanded = hasDone || accStates.output === true;
      const fmtSelectorHtml = hasDone ? `<select class="wf-output-format-select" onchange="wfChangeOutputFormat(this.value)" onclick="event.stopPropagation()">
          <option value="auto"${wfState.outputFormat === 'auto' ? ' selected' : ''}>Auto</option>
          <option value="json"${wfState.outputFormat === 'json' ? ' selected' : ''}>JSON</option>
          <option value="table"${wfState.outputFormat === 'table' ? ' selected' : ''}>Table</option>
          <option value="markdown"${wfState.outputFormat === 'markdown' ? ' selected' : ''}>Markdown</option>
          <option value="text"${wfState.outputFormat === 'text' ? ' selected' : ''}>Text</option>
        </select>` : '';
      html += `<div class="wf-accordion-header ${outputExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('output', this)">
        <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> OUTPUT</span>
        <span class="wf-acc-summary">${fmtSelectorHtml}</span>
      </div>
      <div class="wf-accordion-body" data-acc="output" style="max-height:${outputExpanded ? 'none' : '0px'};">
        <div class="wf-accordion-body-inner" id="wfOutputContent">`;
      if (hasDone) {
        const r = wfState.executionResults._done;
        const hints = r.formatters || {};
        const fmt = wfState.outputFormat || 'auto';
        const effectiveFmt = fmt === 'auto' ? wfAutoFormat(r.output) : fmt;
        html += `<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ${r.totalTimeMs}ms</div>`;
        html += wfRenderOutputAs(r.output, effectiveFmt, hints);
        html += `<button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>`;
      } else if (def.output) {
        html += `<div class="wf-inspector-code" style="color:#40E0FF;">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>`;
      } else {
        html += '<div style="font-size:12px;color:var(--text-muted);">Run the workflow to see output</div>';
      }
      html += '</div></div>';
    }

    body.innerHTML = html;
    wfBindExpandButtons(body);
    return;
  }

  // Show step details
  const step = def.steps.find(s => s.id === wfState.selectedNodeId);
  if (!step) {
    body.innerHTML = '<div class="wf-inspector-empty">Step not found: ' + escapeHtml(wfState.selectedNodeId) + '</div>';
    return;
  }

  const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
  header.textContent = step.name || step.id;

  let html = '';

  // Tool badge (always shown) with help button
  html += `<div class="wf-inspector-section">
    <div class="wf-inspector-section-title">Tool</div>
    <div style="display:flex;align-items:center;">
      <span class="wf-tool-badge" style="background:${meta.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>${meta.label}</span>
      <button class="wf-inspector-help-btn" onclick="wfOpenHelpModal('${step.tool}')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Help</button>
    </div>
  </div>`;

  if (wfState.builderMode) {
    // Builder: editable step fields
    const sid = escapeHtml(step.id);
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Step ID</div>
      <input class="wf-inspector-input" value="${sid}" style="font-family:monospace;" onchange="wfEditStepId('${sid}', this.value)">
    </div>`;
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Name</div>
      <input class="wf-inspector-input" value="${escapeHtml(step.name || '')}" onchange="wfEditStepField('${sid}', 'name', this.value)">
    </div>`;

    // Inputs from WF_INPUT_DEFS
    const inputDefs = WF_INPUT_DEFS[step.tool] || [];
    if (inputDefs.length > 0) {
      html += `<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>`;
      for (const d of inputDefs) {
        const val = step.inputs?.[d.key] ?? '';
        const display = typeof val === 'string' ? val : JSON.stringify(val);
        const req = d.required ? ' <span style="color:#e74c3c;font-size:10px;">required</span>' : '';
        html += `<div style="margin-bottom:8px;">
          <div style="font-size:11px;font-weight:600;color:var(--text);">${escapeHtml(d.key)}${req}</div>`;

        if (d.type === 'textarea' || d.type === 'json') {
          html += `<textarea class="wf-inspector-input" rows="2" style="resize:vertical;font-family:monospace;font-size:11px;" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">${escapeHtml(display)}</textarea>`;
        } else if (d.type === 'select' && d.options) {
          html += `<select class="wf-inspector-input" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">
            <option value="">--</option>`;
          for (const opt of d.options) {
            html += `<option value="${escapeHtml(opt)}" ${val === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`;
          }
          html += `</select>`;
        } else if (d.type === 'number') {
          html += `<input class="wf-inspector-input" type="number" value="${escapeHtml(String(val))}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
        } else {
          html += `<input class="wf-inspector-input" value="${escapeHtml(display)}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
        }
        html += `</div>`;
      }
      html += '</div>';
    }

    // Condition
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Condition <span style="font-size:10px;color:var(--text-muted)">(optional)</span></div>
      <input class="wf-inspector-input" value="${escapeHtml(step.condition || '')}" placeholder="e.g. results.length > 0" onchange="wfEditStepField('${sid}', 'condition', this.value || undefined)">
    </div>`;

    // continueOnError toggle
    html += `<div class="wf-inspector-section" style="display:flex;align-items:center;gap:8px;">
      <input type="checkbox" id="wf-coe-${sid}" ${step.continueOnError ? 'checked' : ''} onchange="wfEditStepField('${sid}', 'continueOnError', this.checked)">
      <label for="wf-coe-${sid}" style="font-size:11px;color:var(--text);cursor:pointer;">Continue on error</label>
    </div>`;

    // Delete button
    html += `<button class="wf-inspector-delete-btn" onclick="if(confirm('Delete step ${sid}?')) wfDeleteStep('${sid}')">Delete Step</button>`;
  } else {
    // Read-only step view
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">ID</div>
      <div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
    </div>`;

    if (step.inputs) {
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">Inputs</div>`;
      for (const [key, val] of Object.entries(step.inputs)) {
        const display = typeof val === 'string' ? val : JSON.stringify(val);
        html += `<div class="wf-inspector-field">
          <span class="wf-inspector-field-label">${escapeHtml(key)}</span>
          <span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
        </div>`;
      }
      html += '</div>';
    }

    if (step.condition) {
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">Condition \u26A1</div>
        <div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
      </div>`;
    }

    if (step.forEach) {
      html += `<div class="wf-inspector-section">
        <div class="wf-inspector-section-title">ForEach</div>
        <div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
      </div>`;
    }
  }

  // Execution result for this step (shown in both modes)
  const result = wfState.executionResults[step.id];
  const state = wfState.executionState[step.id];
  if (state === 'completed' && result) {
    const outputJson = result.output ? JSON.stringify(result.output, null, 2) : '';
    const stepTitle = step.name || step.id;
    const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
    const showStepFmtToggle = stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison');
    const stepFmtToggle = showStepFmtToggle ?
      `<div style="margin-bottom:4px;"><select class="wf-output-format-select" style="width:auto;margin:0;" onchange="wfChangeStepFormat('${escapeHtml(step.id)}', this.value)">
        <option value="json">JSON</option>
        <option value="table" selected>Table</option>
        <option value="text">Text</option>
      </select></div>` : '';
    const stepResultContent = showStepFmtToggle
      ? wfRenderOutputAs(result.output, 'table', {})
      : (outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : '');
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Result</div>
      <div class="wf-inspector-result success">
        <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${result.timeMs}ms${result.summary ? ', ' + result.summary : ''}</div>
        ${stepFmtToggle}
        <div id="wf-step-result-${escapeHtml(step.id)}">${stepResultContent}</div>
      </div>
      ${outputJson ? '<button class="wf-output-expand-btn" data-expand-step="' + escapeHtml(step.id) + '">&#x2922; Expand</button>' : ''}
    </div>`;
  } else if (state === 'error' && result) {
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Error</div>
      <div class="wf-inspector-result error">
        <div style="font-size:12px;margin-bottom:4px;">${escapeHtml(result.error || 'Unknown error')}</div>
      </div>
    </div>`;
  } else if (state === 'skipped' && result) {
    html += `<div class="wf-inspector-section">
      <div class="wf-inspector-section-title">Skipped</div>
      <div style="font-size:12px;color:var(--text-muted);">${escapeHtml(result.reason || 'Condition not met')}</div>
    </div>`;
  }

  body.innerHTML = html;
  wfBindExpandButtons(body);
  } catch (err) {
    console.error('Inspector render error:', err);
    body.innerHTML = '<div class="wf-inspector-empty" style="color:#e74c3c;">Error rendering inspector: ' + escapeHtml(err.message) + '</div>';
  }
}

function wfBindExpandButtons(container) {
  container.querySelectorAll('.wf-output-expand-btn[data-expand-step]').forEach(btn => {
    btn.addEventListener('click', () => {
      const stepId = btn.dataset.expandStep;
      const result = wfState.executionResults[stepId];
      if (!result) return;
      let title, content;
      if (stepId === '_done') {
        title = 'Workflow Output';
        content = JSON.stringify(result.output, null, 2);
        // Rich modal rendering for workflow output
        const hints = result.formatters || {};
        const fmt = wfState.outputFormat || hints.default || 'auto';
        const effectiveFmt = fmt === 'auto' ? wfAutoFormat(result.output) : fmt;
        wfOpenOutputModal(title, content, effectiveFmt, result.output, hints);
        return;
      } else {
        const def = wfState.activeWorkflow;
        const step = def ? def.steps.find(s => s.id === stepId) : null;
        title = (step ? step.name || step.id : stepId) + ' Output';
        content = JSON.stringify(result.output, null, 2);
        // Rich modal for step outputs with array/comparison shapes
        const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
        if (stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison')) {
          wfOpenOutputModal(title, content, 'table', result.output, {});
          return;
        }
      }
      wfOpenOutputModal(title, content);
    });
  });
}

// ── Output Format Rendering ──

function wfStringify(val) {
  if (val == null) return '';
  if (typeof val === 'number') return val % 1 === 0 ? String(val) : val.toFixed(4);
  if (typeof val === 'boolean') return val ? 'true' : 'false';
  if (typeof val === 'object') {
    const s = JSON.stringify(val);
    return s.length > 80 ? s.slice(0, 77) + '...' : s;
  }
  return String(val);
}

function wfDetectOutputShape(output) {
  if (output == null || typeof output !== 'object') return { type: 'scalar', value: output };
  const keys = Object.keys(output);
  for (const key of keys) {
    const val = output[key];
    if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0] !== null) {
      return { type: 'array', arrayKey: key, columns: Object.keys(val[0]), totalRows: val.length };
    }
  }
  const objKeys = keys.filter(k => output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]));
  if (objKeys.length >= 2) {
    return { type: 'comparison', objectKeys: objKeys, metricKeys: keys.filter(k => !objKeys.includes(k)) };
  }
  const textKeys = keys.filter(k => typeof output[k] === 'string' && output[k].length > 100);
  if (textKeys.length > 0) {
    return { type: 'text', textKeys, metricKeys: keys.filter(k => !textKeys.includes(k)) };
  }
  return { type: 'metrics', keys };
}

function wfAutoFormat(output) {
  const shape = wfDetectOutputShape(output);
  if (shape.type === 'array' || shape.type === 'comparison') return 'table';
  return 'text';
}

function wfRenderOutputAs(output, format, hints) {
  hints = hints || {};
  if (!output) return '<div class="wf-inspector-code" style="color:#40E0FF;">(no output)</div>';
  switch (format) {
    case 'json':
      return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
    case 'table':
      return wfRenderTable(output, hints);
    case 'markdown':
      return '<div class="wf-output-markdown">' + renderMarkdown(wfOutputToMarkdown(output, hints)) + '</div>';
    case 'text':
      return wfRenderText(output, hints);
    default:
      return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
  }
}

function wfRenderTable(output, hints) {
  const shape = wfDetectOutputShape(output);
  let html = '';

  if (shape.type === 'array') {
    const key = hints.arrayField || shape.arrayKey;
    const data = output[key] || [];
    if (data.length === 0) return '<div style="color:var(--text-muted);font-size:12px;">(empty results)</div>';
    const columns = hints.columns || shape.columns;

    // Show non-array metrics above
    const metricKeys = Object.keys(output).filter(k => k !== key && !Array.isArray(output[k]));
    if (metricKeys.length > 0) {
      html += '<div style="margin-bottom:8px;">';
      metricKeys.forEach(k => {
        html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
      });
      html += '</div>';
    }

    html += '<table class="wf-output-table"><thead><tr>';
    columns.forEach(c => { html += '<th>' + escapeHtml(c) + '</th>'; });
    html += '</tr></thead><tbody>';
    data.forEach(row => {
      html += '<tr>';
      columns.forEach(c => { html += '<td>' + escapeHtml(wfStringify(row[c])) + '</td>'; });
      html += '</tr>';
    });
    html += '</tbody></table>';
    return html;
  }

  if (shape.type === 'comparison') {
    const keys = shape.objectKeys;
    const allFields = new Set();
    keys.forEach(k => { if (output[k]) Object.keys(output[k]).forEach(f => allFields.add(f)); });

    // Show non-object metrics above
    const metricKeys = (shape.metricKeys || []).filter(k => output[k] != null);
    if (metricKeys.length > 0) {
      html += '<div style="margin-bottom:8px;">';
      metricKeys.forEach(k => {
        html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
      });
      html += '</div>';
    }

    html += '<table class="wf-output-table"><thead><tr><th></th>';
    keys.forEach(k => { html += '<th>' + escapeHtml(String(k)) + '</th>'; });
    html += '</tr></thead><tbody>';
    allFields.forEach(f => {
      html += '<tr><td style="font-weight:600;">' + escapeHtml(f) + '</td>';
      keys.forEach(k => { html += '<td>' + escapeHtml(wfStringify(output[k] && output[k][f])) + '</td>'; });
      html += '</tr>';
    });
    html += '</tbody></table>';
    return html;
  }

  // Fallback: key-value table
  html += '<table class="wf-output-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>';
  Object.entries(output).forEach(function(entry) {
    html += '<tr><td>' + escapeHtml(entry[0]) + '</td><td>' + escapeHtml(wfStringify(entry[1])) + '</td></tr>';
  });
  html += '</tbody></table>';
  return html;
}

function wfRenderText(output, hints) {
  const shape = wfDetectOutputShape(output);
  if (shape.type === 'scalar') return '<div style="font-size:12px;color:var(--text);">' + escapeHtml(String(output || '')) + '</div>';
  let html = '';
  if (hints.title) {
    html += '<div style="font-weight:600;font-size:13px;color:var(--accent);margin-bottom:8px;">' + escapeHtml(hints.title) + '</div>';
  }

  // Metrics
  const metricKeys = Object.keys(output).filter(k =>
    output[k] != null && (typeof output[k] !== 'object' || typeof output[k] === 'boolean') &&
    (typeof output[k] !== 'string' || output[k].length <= 100)
  );
  if (metricKeys.length > 0) {
    html += '<div style="margin-bottom:10px;">';
    metricKeys.forEach(k => {
      html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
    });
    html += '</div>';
  }

  // Object fields (comparison-like)
  Object.keys(output).forEach(k => {
    const v = output[k];
    if (v != null && typeof v === 'object' && !Array.isArray(v)) {
      html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + '</div>';
      Object.entries(v).forEach(function(entry) {
        html += '<div style="font-size:12px;margin-left:8px;"><span style="color:var(--text-muted);">' + escapeHtml(entry[0]) + ':</span> <span style="color:#40E0FF;">' + escapeHtml(wfStringify(entry[1])) + '</span></div>';
      });
      html += '</div>';
    }
  });

  // Text fields
  Object.keys(output).forEach(k => {
    if (typeof output[k] === 'string' && output[k].length > 100) {
      html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-text-value">' + escapeHtml(output[k]) + '</div></div>';
    }
  });

  // Arrays
  Object.keys(output).forEach(k => {
    if (Array.isArray(output[k])) {
      html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + ' (' + output[k].length + ' items)</div>';
      html += '<div class="wf-inspector-code" style="max-height:100px;overflow:auto;">' + escapeHtml(JSON.stringify(output[k], null, 2)) + '</div></div>';
    }
  });

  return html || '<div class="wf-inspector-code" style="color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
}

function wfOutputToMarkdown(output, hints) {
  let md = '';
  if (hints.title) md += '## ' + hints.title + '\n\n';
  else md += '## Workflow Output\n\n';
  Object.entries(output).forEach(function(entry) {
    const k = entry[0], v = entry[1];
    if (v == null) return;
    if (typeof v !== 'object' || typeof v === 'boolean') {
      if (typeof v !== 'string' || v.length <= 100) {
        md += '- **' + k + ':** ' + wfStringify(v) + '\n';
      }
    }
  });
  md += '\n';
  // Arrays as tables
  Object.entries(output).forEach(function(entry) {
    const k = entry[0], v = entry[1];
    if (!Array.isArray(v) || v.length === 0 || typeof v[0] !== 'object') return;
    const cols = hints.columns || Object.keys(v[0]);
    md += '### ' + k + '\n\n| ' + cols.join(' | ') + ' |\n| ' + cols.map(function() { return '---'; }).join(' | ') + ' |\n';
    v.forEach(function(row) { md += '| ' + cols.map(function(c) { return wfStringify(row[c]); }).join(' | ') + ' |\n'; });
    md += '\n';
  });
  // Comparison objects
  const objKeys = Object.keys(output).filter(function(k) { return output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]); });
  if (objKeys.length >= 2) {
    const allFields = new Set();
    objKeys.forEach(function(k) { Object.keys(output[k]).forEach(function(f) { allFields.add(f); }); });
    md += '| | ' + objKeys.join(' | ') + ' |\n| --- | ' + objKeys.map(function() { return '---'; }).join(' | ') + ' |\n';
    allFields.forEach(function(f) {
      md += '| **' + f + '** | ' + objKeys.map(function(k) { return wfStringify(output[k][f]); }).join(' | ') + ' |\n';
    });
    md += '\n';
  }
  // Long text
  Object.entries(output).forEach(function(entry) {
    if (typeof entry[1] === 'string' && entry[1].length > 100) {
      md += '### ' + entry[0] + '\n\n' + entry[1] + '\n\n';
    }
  });
  return md;
}

function wfChangeOutputFormat(format) {
  wfState.outputFormat = format;
  const r = wfState.executionResults._done;
  if (!r) return;
  const hints = r.formatters || {};
  const effectiveFormat = format === 'auto' ? wfAutoFormat(r.output) : format;
  const container = document.getElementById('wfOutputContent');
  if (!container) return;
  let html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ' + r.totalTimeMs + 'ms</div>';
  html += wfRenderOutputAs(r.output, effectiveFormat, hints);
  html += '<button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>';
  container.innerHTML = html;
  wfBindExpandButtons(container);
}

function wfChangeStepFormat(stepId, format) {
  const result = wfState.executionResults[stepId];
  if (!result || !result.output) return;
  const container = document.getElementById('wf-step-result-' + stepId);
  if (!container) return;
  if (format === 'json') {
    container.innerHTML = '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(JSON.stringify(result.output, null, 2)) + '</div>';
  } else {
    container.innerHTML = wfRenderOutputAs(result.output, format, {});
  }
}

// escapeHtml is already defined globally — reuse it

// ── Toolbar helpers ──
function wfSetToolbarEnabled(enabled) {
  document.getElementById('wfRunBtn').disabled = !enabled;
  document.getElementById('wfDryRunBtn').disabled = !enabled;
  document.getElementById('wfExportBtn').disabled = !enabled;
  document.getElementById('wfEditBtn').disabled = !enabled;
  document.getElementById('wfInfoBtn').disabled = !enabled;
}

// ── Export workflow JSON ──
function wfExportJson() {
  const def = wfState.activeWorkflow;
  if (!def) return;
  const json = JSON.stringify(def, null, 2);
  const name = (def.name || 'workflow').replace(/\s+/g, '-').toLowerCase();
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = name + '.vai-workflow.json';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

// ── Dry Run ──
async function wfDryRun() {
  const def = wfState.activeWorkflow;
  if (!def || !wfState.layers) return;
  
  // Strict validation before dry run
  const validation = await wfValidateBuilder();
  if (validation && !validation.valid) {
    wfShowValidationErrors(validation.errors || [], 'Dry Run');
    return;
  }

  const layers = wfState.layers;
  const stepMap = {};
  def.steps.forEach(s => { stepMap[s.id] = s; });

  // Build layer HTML
  let layersHtml = '';
  let totalSteps = 0;
  let conditionalSteps = 0;

  layers.forEach((layer, i) => {
    const parallel = layer.length > 1 ? ' (parallel)' : '';
    layersHtml += '<div class="wf-dryrun-layer">';
    layersHtml += '<div class="wf-dryrun-layer-title">';
    layersHtml += '<span class="wf-dryrun-layer-badge">' + (i + 1) + '</span>';
    layersHtml += ' Layer ' + (i + 1) + parallel;
    layersHtml += '</div>';

    layer.forEach(stepId => {
      const step = stepMap[stepId];
      if (!step) return;
      totalSteps++;
      const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
      layersHtml += '<div class="wf-dryrun-step">';
      layersHtml += '<span class="wf-dryrun-step-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="' + (meta.icon || WF_FALLBACK_ICON) + '"/></svg></span>';
      layersHtml += '<div class="wf-dryrun-step-info">';
      layersHtml += '<div class="wf-dryrun-step-name">' + escapeHtml(step.name || step.id) + '</div>';
      layersHtml += '<div class="wf-dryrun-step-tool">' + escapeHtml(meta.label) + ' (' + escapeHtml(step.id) + ')</div>';
      if (step.condition) {
        conditionalSteps++;
        layersHtml += '<div class="wf-dryrun-step-cond">\u26A1 ' + escapeHtml(step.condition) + '</div>';
      }
      layersHtml += '</div></div>';
    });

    layersHtml += '</div>';
  });

  // Summary stats
  const parallelLayers = layers.filter(l => l.length > 1).length;
  let summaryHtml = '<div class="wf-dryrun-summary">';
  summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + totalSteps + '</div><div class="wf-dryrun-stat-label">Steps</div></div>';
  summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + layers.length + '</div><div class="wf-dryrun-stat-label">Layers</div></div>';
  summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + parallelLayers + '</div><div class="wf-dryrun-stat-label">Parallel</div></div>';
  if (conditionalSteps > 0) {
    summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + conditionalSteps + '</div><div class="wf-dryrun-stat-label">Conditional</div></div>';
  }
  summaryHtml += '</div>';

  // Inputs summary
  let inputsHtml = '';
  if (def.inputs && Object.keys(def.inputs).length > 0) {
    inputsHtml += '<div style="margin-bottom:12px;font-size:12px;">';
    inputsHtml += '<div style="font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:6px;">Required Inputs</div>';
    for (const [key, spec] of Object.entries(def.inputs)) {
      const req = spec.required ? ' *' : '';
      inputsHtml += '<div style="margin-bottom:2px;"><span style="font-weight:600;color:var(--text);">' + escapeHtml(key) + req + '</span>';
      inputsHtml += ' <span style="color:var(--text-muted);">(' + escapeHtml(spec.type || 'string') + ')</span></div>';
    }
    inputsHtml += '</div>';
  }

  // Create overlay
  const canvasArea = document.querySelector('.wf-canvas-area');
  // Remove any existing overlay
  canvasArea.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());

  const overlay = document.createElement('div');
  overlay.className = 'wf-dryrun-overlay';
  overlay.innerHTML = '<div class="wf-dryrun-panel">' +
    '<div class="wf-dryrun-header">' +
      '<span class="wf-dryrun-title">Execution Plan: ' + escapeHtml(def.name || 'Workflow') + '</span>' +
      '<button class="wf-dryrun-close" onclick="wfCloseDryRun()" title="Close">&times;</button>' +
    '</div>' +
    '<div class="wf-dryrun-body">' +
      inputsHtml +
      summaryHtml +
      '<div style="margin-top:16px;">' + layersHtml + '</div>' +
    '</div>' +
  '</div>';

  // Click backdrop to close
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) wfCloseDryRun();
  });

  canvasArea.appendChild(overlay);
}

function wfCloseDryRun() {
  document.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
}

// ── Execution ──
let wfAbortController = null;
let wfExecTimer = null;
let wfExecStartTime = 0;
const WF_EXEC_TIMEOUT_MS = 120000; // 2 minute total timeout

function wfShowExecStatus(text, state) {
  const bar = document.getElementById('wfExecStatus');
  const textEl = document.getElementById('wfExecStatusText');
  if (!bar || !textEl) return;
  bar.style.display = 'flex';
  bar.className = 'wf-exec-status' + (state ? ' ' + state : '');
  textEl.textContent = text;
}

function wfHideExecStatus() {
  const bar = document.getElementById('wfExecStatus');
  if (bar) bar.style.display = 'none';
}

function wfUpdateExecTimer() {
  const timeEl = document.getElementById('wfExecStatusTime');
  if (!timeEl || !wfState.executing) return;
  const elapsed = Date.now() - wfExecStartTime;
  const secs = (elapsed / 1000).toFixed(1);
  timeEl.textContent = secs + 's';

  // Check timeout
  if (elapsed > WF_EXEC_TIMEOUT_MS) {
    wfStopExecution('Execution timed out after ' + (WF_EXEC_TIMEOUT_MS / 1000) + 's');
    return;
  }
  wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);
}

function wfStopExecution(reason) {
  if (!wfState.executing) return;
  if (wfAbortController) {
    wfAbortController.abort();
    wfAbortController = null;
  }
  if (wfExecTimer) {
    cancelAnimationFrame(wfExecTimer);
    wfExecTimer = null;
  }

  // Mark any still-running nodes as error
  const def = wfState.activeWorkflow;
  if (def) {
    def.steps.forEach(s => {
      if (wfState.executionState[s.id] === 'running') {
        wfState.executionState[s.id] = 'error';
        wfState.executionResults[s.id] = { error: reason || 'Stopped by user' };
      } else if (wfState.executionState[s.id] === 'pending') {
        wfState.executionState[s.id] = 'skipped';
        wfState.executionResults[s.id] = { reason: reason || 'Stopped by user' };
      }
    });
    wfRefreshNodes();
  }

  const stopReason = reason || 'Stopped by user';
  wfShowExecStatus(stopReason, reason && reason.includes('timed out') ? 'error' : 'stopped');
  wfState.executing = false;
  wfSetToolbarEnabled(true);
  document.getElementById('wfStopBtn').style.display = 'none';
  wfUpdateInspector();
}

// ── Workflow Input Cache (localStorage) ──

function wfSlugify(name) {
  return String(name).toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
}

function wfGetInputCache(workflowName) {
  try {
    const slug = wfSlugify(workflowName);
    if (!slug) return {};
    const raw = localStorage.getItem('vai-workflow-inputs-' + slug);
    if (!raw) return {};
    const parsed = JSON.parse(raw);
    return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
  } catch { return {}; }
}

function wfSaveInputCache(workflowName, inputs) {
  try {
    const slug = wfSlugify(workflowName);
    if (!slug || !inputs) return;
    localStorage.setItem('vai-workflow-inputs-' + slug, JSON.stringify(inputs));
  } catch { /* localStorage may be full or disabled */ }
}

// ── Input Modal (pre-execution) ──

function wfShowInputModal() {
  const def = wfState.activeWorkflow;
  if (!def || !def.inputs) return;
  const entries = Object.entries(def.inputs);
  if (entries.length === 0) { wfExecuteWithInputs({}); return; }

  document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
  const cached = wfGetInputCache(def.name || '');
  let html = '';
  for (const [key, spec] of entries) {
    const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
    const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
    // Pre-fill priority: 1) inspector field, 2) cached from last run, 3) spec default
    const inspectorEl = document.getElementById('wf-input-' + key);
    let prefill = inspectorEl ? inspectorEl.value : '';
    if (!prefill && key in cached) prefill = String(cached[key]);
    if (!prefill && spec.default !== undefined) prefill = String(spec.default);
    const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
    html += `<div class="wf-input-modal-field">
      <div class="wf-input-modal-label">${escapeHtml(key)}${req}</div>
      ${desc}
      <input class="wf-input-modal-input" id="wf-modal-input-${key}" placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(prefill)}" data-key="${escapeHtml(key)}" data-type="${spec.type || 'string'}" data-required="${!!spec.required}">
      <div class="wf-input-modal-error" id="wf-modal-err-${key}">This field is required</div>
    </div>`;
  }
  document.getElementById('wfInputModalBody').innerHTML = html;
  document.getElementById('wfInputModalBackdrop').style.display = '';

  // Focus first empty required field, or first field
  const firstEmpty = entries.find(([k, s]) => {
    const el = document.getElementById('wf-modal-input-' + k);
    return s.required && el && !el.value;
  });
  const focusKey = firstEmpty ? firstEmpty[0] : entries[0][0];
  const focusEl = document.getElementById('wf-modal-input-' + focusKey);
  if (focusEl) setTimeout(() => focusEl.focus(), 50);
}

function wfCloseInputModal() {
  document.getElementById('wfInputModalBackdrop').style.display = 'none';
}

function wfInputModalSubmit() {
  const def = wfState.activeWorkflow;
  if (!def || !def.inputs) return;

  const inputs = {};
  let hasError = false;

  for (const [key, spec] of Object.entries(def.inputs)) {
    const el = document.getElementById('wf-modal-input-' + key);
    const errEl = document.getElementById('wf-modal-err-' + key);
    if (!el) continue;

    let val = el.value.trim();

    // Clear previous error
    el.classList.remove('error');
    if (errEl) errEl.style.display = 'none';

    // Required check
    if (!val && spec.required && spec.default === undefined) {
      el.classList.add('error');
      if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
      hasError = true;
      continue;
    }

    // Use default if empty
    if (!val && spec.default !== undefined) val = String(spec.default);

    // Type validation
    if (spec.type === 'number' && val && isNaN(Number(val))) {
      el.classList.add('error');
      if (errEl) { errEl.textContent = 'Must be a number'; errEl.style.display = ''; }
      hasError = true;
      continue;
    }

    // Coerce
    if (spec.type === 'number' && val) val = Number(val);
    if (val !== undefined && val !== '') inputs[key] = val;
  }

  if (hasError) return;

  // Cache inputs for next run
  wfSaveInputCache(def.name || '', inputs);

  // Also update inspector fields to keep them in sync
  for (const [key, val] of Object.entries(inputs)) {
    const inspEl = document.getElementById('wf-input-' + key);
    if (inspEl) inspEl.value = typeof val === 'number' ? String(val) : val;
  }

  wfCloseInputModal();
  wfExecuteWithInputs(inputs);
}

// Keyboard handler for input modal
document.addEventListener('keydown', (e) => {
  const backdrop = document.getElementById('wfInputModalBackdrop');
  if (!backdrop || backdrop.style.display === 'none') return;
  if (e.key === 'Escape') { wfCloseInputModal(); e.preventDefault(); }
  if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { wfInputModalSubmit(); e.preventDefault(); }
});

async function wfExecute() {
  const def = wfState.activeWorkflow;
  if (!def || wfState.executing) return;

  // If workflow has inputs, show the input modal instead of executing directly
  if (def.inputs && Object.keys(def.inputs).length > 0) {
    wfShowInputModal();
    return;
  }

  // Check for empty required step inputs and prompt the user
  const missing = wfFindMissingStepInputs(def);
  if (missing.length > 0) {
    wfShowStepInputModal(missing);
    return;
  }

  // No inputs needed, execute directly
  wfExecuteWithInputs({});
}

/**
 * Scan all steps for required inputs that are empty (and not filled by template refs).
 * Returns array of { stepId, stepName, tool, key, placeholder, type } for each missing input.
 */
function wfFindMissingStepInputs(def) {
  const missing = [];
  for (const step of (def.steps || [])) {
    const inputDefs = WF_INPUT_DEFS[step.tool] || [];
    for (const d of inputDefs) {
      if (!d.required) continue;
      const val = step.inputs?.[d.key];
      // Skip if already filled (non-empty string, or any non-string value)
      if (val !== undefined && val !== null && val !== '') continue;
      missing.push({
        stepId: step.id,
        stepName: step.name || step.id,
        tool: step.tool,
        key: d.key,
        placeholder: d.placeholder || '',
        type: d.type || 'text',
      });
    }
  }
  return missing;
}

/**
 * Show a modal prompting the user for missing required step inputs before execution.
 * Reuses the existing input modal UI.
 */
function wfShowStepInputModal(missing) {
  document.getElementById('wfInputModalTitle').textContent = 'Required Inputs';
  let html = '';
  let lastStepId = '';

  for (const m of missing) {
    // Group header per step
    if (m.stepId !== lastStepId) {
      const meta = WF_NODE_META[m.tool] || {};
      html += `<div style="margin-top:${lastStepId ? '16' : '0'}px;margin-bottom:8px;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);display:flex;align-items:center;gap:6px;">
        <span style="color:${meta.color || 'var(--text-muted)'}">
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>
        </span>
        ${escapeHtml(m.stepName)}
      </div>`;
      lastStepId = m.stepId;
    }

    const inputId = 'wf-stepin-' + m.stepId + '-' + m.key;
    html += `<div class="wf-input-modal-field">
      <div class="wf-input-modal-label">${escapeHtml(m.key)} <span style="color:#e74c3c">*</span></div>
      <input class="wf-input-modal-input" id="${inputId}" placeholder="${escapeHtml(m.placeholder)}" data-step-id="${escapeHtml(m.stepId)}" data-key="${escapeHtml(m.key)}" data-type="${m.type}">
      <div class="wf-input-modal-error" id="${inputId}-err">This field is required</div>
    </div>`;
  }

  document.getElementById('wfInputModalBody').innerHTML = html;

  // Swap the footer button handler temporarily
  const runBtn = document.querySelector('.wf-input-modal-run');
  runBtn.textContent = 'Run Workflow';
  runBtn.onclick = () => wfStepInputModalSubmit(missing);

  document.getElementById('wfInputModalBackdrop').style.display = '';

  // Focus first input
  const firstId = 'wf-stepin-' + missing[0].stepId + '-' + missing[0].key;
  const firstEl = document.getElementById(firstId);
  if (firstEl) setTimeout(() => firstEl.focus(), 50);
}

function wfStepInputModalSubmit(missing) {
  let hasError = false;

  for (const m of missing) {
    const inputId = 'wf-stepin-' + m.stepId + '-' + m.key;
    const el = document.getElementById(inputId);
    const errEl = document.getElementById(inputId + '-err');
    if (!el) continue;

    el.classList.remove('error');
    if (errEl) errEl.style.display = 'none';

    const val = el.value.trim();
    if (!val) {
      el.classList.add('error');
      if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
      hasError = true;
      continue;
    }

    // Write the value into the step definition
    const def = wfState.activeWorkflow;
    const step = def.steps.find(s => s.id === m.stepId);
    if (step) {
      if (!step.inputs) step.inputs = {};
      step.inputs[m.key] = val;
    }
  }

  if (hasError) return;

  // Restore the default modal handler
  const runBtn = document.querySelector('.wf-input-modal-run');
  runBtn.onclick = () => wfInputModalSubmit();

  wfCloseInputModal();

  // Refresh inspector if a step is selected
  if (wfState.selectedNodeId) wfUpdateInspector();

  wfExecuteWithInputs({});
}

async function wfExecuteWithInputs(inputs) {
  const def = wfState.activeWorkflow;
  if (!def || wfState.executing) return;
  
  // Strict validation before execution
  const validation = await wfValidateBuilder();
  if (validation && !validation.valid) {
    wfShowValidationErrors(validation.errors || [], 'Run Workflow');
    return;
  }

  wfState.executing = true;
  wfState.executionResults = {};
  wfAbortController = new AbortController();
  wfExecStartTime = Date.now();
  wfSetToolbarEnabled(false);
  document.getElementById('wfStopBtn').style.display = '';
  wfShowExecStatus('Running...', '');
  wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);

  // Reset all nodes to pending
  def.steps.forEach(s => { wfState.executionState[s.id] = 'pending'; });
  wfRefreshNodes();

  let hasError = false;
  let errorMessage = '';

  try {
    const res = await fetch('/api/workflows/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ definition: def, inputs }),
      signal: wfAbortController.signal,
    });

    if (!res.ok) {
      throw new Error('Server returned ' + res.status + ': ' + (await res.text()));
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let currentEvent = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });

      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (line.startsWith('event: ')) {
          currentEvent = line.slice(7).trim();
        } else if (line.startsWith('data: ') && currentEvent) {
          let data;
          try { data = JSON.parse(line.slice(6)); } catch { continue; }

          if (currentEvent === 'step_start') {
            wfState.executionState[data.stepId] = 'running';
            const stepName = (def.steps.find(s => s.id === data.stepId) || {}).name || data.stepId;
            wfShowExecStatus('Running: ' + stepName, '');
            wfRefreshNodes();
          } else if (currentEvent === 'step_complete') {
            wfState.executionState[data.stepId] = 'completed';
            wfState.executionResults[data.stepId] = {
              output: data.output,
              timeMs: data.timeMs,
              summary: data.summary || '',
            };
            // Update cost tracker with usage data from this step
            if (data._usage && Array.isArray(data._usage)) {
              data._usage.forEach(u => {
                if (u.op === 'llm') {
                  CostTracker.addLLMOperation('wf-' + u.op, u.model, u.inputTokens || 0, u.outputTokens || 0);
                } else {
                  CostTracker.addOperation('wf-' + u.op, u.model, u.tokens || 0);
                }
              });
            }
            wfRefreshNodes();
            if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
          } else if (currentEvent === 'step_skip') {
            wfState.executionState[data.stepId] = 'skipped';
            wfState.executionResults[data.stepId] = { reason: data.reason };
            wfRefreshNodes();
            if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
          } else if (currentEvent === 'step_error') {
            wfState.executionState[data.stepId] = 'error';
            wfState.executionResults[data.stepId] = { error: data.error };
            hasError = true;
            errorMessage = data.error || 'Step failed';
            wfRefreshNodes();
            if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
          } else if (currentEvent === 'done') {
            wfState.executionResults._done = data;
            wfState.outputFormat = 'auto';
            if (!wfState.selectedNodeId) wfUpdateInspector();
          } else if (currentEvent === 'error') {
            hasError = true;
            errorMessage = data.error || 'Workflow error';
            console.error('Workflow execution error:', data.error);
          }
          currentEvent = '';
        }
      }
    }
  } catch (err) {
    if (err.name === 'AbortError') {
      // Handled by wfStopExecution
      return;
    }
    hasError = true;
    errorMessage = err.message || 'Execution failed';
    console.error('Workflow execution failed:', err);
    // Mark running nodes as error
    def.steps.forEach(s => {
      if (wfState.executionState[s.id] === 'running') {
        wfState.executionState[s.id] = 'error';
        wfState.executionResults[s.id] = { error: errorMessage };
      }
    });
    wfRefreshNodes();
  }

  if (wfExecTimer) { cancelAnimationFrame(wfExecTimer); wfExecTimer = null; }
  wfAbortController = null;
  wfState.executing = false;
  wfSetToolbarEnabled(true);
  document.getElementById('wfStopBtn').style.display = 'none';

  const elapsed = ((Date.now() - wfExecStartTime) / 1000).toFixed(1);
  if (hasError) {
    wfShowExecStatus('Failed: ' + errorMessage, 'error');
  } else {
    wfShowExecStatus('Completed in ' + elapsed + 's', 'done');
  }
  wfUpdateInspector();
}

function wfRefreshNodes() {
  // Re-render the full SVG (simple approach: redraw)
  if (wfState.activeWorkflow) {
    const svg = document.getElementById('wf-canvas');
    const def = wfState.activeWorkflow;
    const positions = wfState.nodePositions;

    // Remove existing nodes and edges
    svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());

    // Redraw edges
    const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    edgeGroup.classList.add('wf-edge-group');
    for (const [stepId, deps] of Object.entries(wfState.graph)) {
      if (!deps || !Array.isArray(deps)) continue;
      deps.forEach(rawDepId => {
        const depId = rawDepId.replace(/^!/, '');
        if (positions[depId] && positions[stepId]) {
          edgeGroup.appendChild(wfDrawEdge(depId, stepId, positions));
        }
      });
    }
    svg.appendChild(edgeGroup);

    // Redraw nodes
    for (const step of def.steps) {
      const pos = positions[step.id];
      if (!pos) continue;
      const state = wfState.executionState[step.id] || 'idle';
      svg.appendChild(wfDrawNode(step, pos.x, pos.y, state));
    }
  }
}

function wfResetExecution() {
  if (wfState.executing) return;
  wfState.executionState = {};
  wfState.executionResults = {};
  wfHideExecStatus();
  wfRefreshNodes();
  wfUpdateInspector();
}

async function wfRelayout() {
  const def = wfState.activeWorkflow;
  if (!def) return;

  // If layers/graph aren't populated yet (e.g. builder mode), fetch them
  if (!wfState.layers || !wfState.graph || wfState.layers.length === 0) {
    try {
      const res = await fetch('/api/workflows/plan', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ definition: def }),
      });
      const data = await res.json();
      if (data.layers) wfState.layers = data.layers;
      if (data.graph) wfState.graph = data.graph;
    } catch (err) {
      console.warn('Relayout: failed to fetch plan:', err.message);
      return;
    }
  }

  if (!wfState.layers || wfState.layers.length === 0) return;

  const positions = wfAutoLayout(wfState.layers, wfState.graph || {});

  // Preserve positions for orphan nodes not in any layer (builder mode)
  if (def.steps) {
    for (const step of def.steps) {
      if (!positions[step.id] && wfState.nodePositions[step.id]) {
        positions[step.id] = wfState.nodePositions[step.id];
      }
    }
  }

  wfState.nodePositions = positions;
  wfRefreshNodes();
  wfFitToView();
}

// ── Output Modal ──
let wfOutputModalData = '';

function wfOpenOutputModal(title, content, format, outputObj, hints) {
  wfOutputModalData = content;
  const backdrop = document.getElementById('wfOutputModalBackdrop');
  const titleEl = document.getElementById('wfOutputModalTitle');
  const bodyEl = document.getElementById('wfOutputModalBody');
  const copyLabel = document.getElementById('wfOutputCopyLabel');
  if (!backdrop || !bodyEl) return;
  titleEl.textContent = title || 'Output';
  copyLabel.textContent = 'Copy';

  // Rich rendering if format + output object provided
  if (format && outputObj && format !== 'json') {
    bodyEl.innerHTML = wfRenderOutputAs(outputObj, format, hints || {});
  } else {
    bodyEl.textContent = content;
  }
  backdrop.style.display = 'flex';
}

function wfCloseOutputModal() {
  const backdrop = document.getElementById('wfOutputModalBackdrop');
  if (backdrop) backdrop.style.display = 'none';
}

function wfCopyOutput() {
  const label = document.getElementById('wfOutputCopyLabel');
  navigator.clipboard.writeText(wfOutputModalData).then(() => {
    if (label) { label.textContent = 'Copied!'; setTimeout(() => { label.textContent = 'Copy'; }, 2000); }
  }).catch(() => {
    if (label) label.textContent = 'Failed';
  });
}

// ── Node Help Modal ──
let _nodeHelpCache = null;
async function getNodeHelp() {
  if (_nodeHelpCache) return _nodeHelpCache;
  try {
    const res = await fetch('/api/workflows/node-help');
    const data = await res.json();
    _nodeHelpCache = data.nodeHelp || {};
  } catch { _nodeHelpCache = {}; }
  return _nodeHelpCache;
}

async function wfOpenHelpModal(tool) {
  const helpData = await getNodeHelp();
  const help = helpData[tool];
  const meta = WF_NODE_META[tool] || { icon: WF_FALLBACK_ICON, label: tool, color: '#666', category: 'unknown' };
  const categoryLabel = WF_CATEGORY_LABELS[meta.category] || meta.category || 'Unknown';

  const headerEl = document.getElementById('wfHelpModalHeader');
  const bodyEl = document.getElementById('wfHelpModalBody');
  const backdrop = document.getElementById('wfHelpModalBackdrop');
  if (!headerEl || !bodyEl || !backdrop) return;

  // Build header
  const isEmoji = !meta.icon.includes(' ');
  const iconHtml = isEmoji
    ? `<div class="wf-help-icon" style="background:${meta.color}22;font-size:18px;">${meta.icon}</div>`
    : `<div class="wf-help-icon" style="background:${meta.color}22;color:${meta.color};"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${meta.icon}"/></svg></div>`;

  headerEl.innerHTML = iconHtml
    + `<span class="wf-help-title">${escapeHtml(meta.label)}</span>`
    + `<span class="wf-help-category-badge">${escapeHtml(categoryLabel)}</span>`
    + `<button class="wf-help-close-btn" onclick="wfCloseHelpModal()" title="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;

  // Build body
  if (!help) {
    bodyEl.innerHTML = `<div class="wf-help-section"><p>No help content available for this node yet.</p></div>`;
    backdrop.style.display = 'flex';
    return;
  }

  let html = '';

  // Overview
  if (help.description) {
    html += `<div class="wf-help-section"><div class="wf-help-section-title">Overview</div><p>${escapeHtml(help.description)}</p></div>`;
  }

  // How it Works
  if (help.howItWorks) {
    html += `<div class="wf-help-section"><div class="wf-help-section-title">How it Works</div><p>${escapeHtml(help.howItWorks)}</p></div>`;
  }

  // Inputs
  if (help.inputs && help.inputs.length > 0) {
    html += `<div class="wf-help-section"><div class="wf-help-section-title">Inputs</div><table class="wf-help-io-table"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody>`;
    for (const inp of help.inputs) {
      const reqBadge = inp.required ? '<span class="wf-help-required">required</span>' : '';
      html += `<tr><td><span class="wf-help-key">${escapeHtml(inp.key)}</span>${reqBadge}</td><td><span class="wf-help-type">${escapeHtml(inp.type)}</span></td><td>${escapeHtml(inp.desc)}</td></tr>`;
    }
    html += '</tbody></table></div>';
  }

  // Outputs
  if (help.outputs && help.outputs.length > 0) {
    html += `<div class="wf-help-section"><div class="wf-help-section-title">Output</div><table class="wf-help-io-table"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody>`;
    for (const out of help.outputs) {
      html += `<tr><td><span class="wf-help-key">${escapeHtml(out.key)}</span></td><td><span class="wf-help-type">${escapeHtml(out.type)}</span></td><td>${escapeHtml(out.desc)}</td></tr>`;
    }
    html += '</tbody></table></div>';
  }

  // Tips
  if (help.tips && help.tips.length > 0) {
    html += `<div class="wf-help-section"><div class="wf-help-section-title">Tips</div>`;
    for (const tip of help.tips) {
      html += `<div class="wf-help-tip"><span class="wf-help-tip-dot"></span><span>${escapeHtml(tip)}</span></div>`;
    }
    html += '</div>';
  }

  bodyEl.innerHTML = html;
  backdrop.style.display = 'flex';
}

function wfCloseHelpModal() {
  const backdrop = document.getElementById('wfHelpModalBackdrop');
  if (backdrop) backdrop.style.display = 'none';
}

// Close modals on Escape
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    const helpBackdrop = document.getElementById('wfHelpModalBackdrop');
    if (helpBackdrop && helpBackdrop.style.display !== 'none') {
      wfCloseHelpModal();
      e.preventDefault();
      return;
    }
    const backdrop = document.getElementById('wfOutputModalBackdrop');
    if (backdrop && backdrop.style.display !== 'none') {
      wfCloseOutputModal();
      e.preventDefault();
    }
  }
});

// ── Zoom & Pan ──
function wfZoom(direction) {
  // Multiplicative zoom: feels consistent at any zoom level
  const factor = direction > 0 ? 1.1 : 1 / 1.1;
  wfState.zoom = Math.max(0.2, Math.min(5, wfState.zoom * factor));
  wfApplyViewBox();
}

function wfFitToView() {
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;
  const positions = wfState.nodePositions;
  const ids = Object.keys(positions);
  if (ids.length === 0) return;

  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  ids.forEach(id => {
    const p = positions[id];
    minX = Math.min(minX, p.x);
    minY = Math.min(minY, p.y);
    maxX = Math.max(maxX, p.x + WF_NODE_W);
    maxY = Math.max(maxY, p.y + WF_NODE_H + 50); // extra for time label + backward edge loops
  });

  const pad = 40;
  const vbW = maxX - minX + pad * 2;
  const vbH = maxY - minY + pad * 2;
  // Calculate zoom to fit
  const rect = svg.parentElement.getBoundingClientRect();
  const scaleX = rect.width / vbW;
  const scaleY = rect.height / vbH;
  wfState.zoom = Math.min(scaleX, scaleY, 1.5);
  // Center content within the viewport
  const viewW = rect.width / wfState.zoom;
  const viewH = rect.height / wfState.zoom;
  const contentCX = (minX + maxX) / 2;
  const contentCY = (minY + maxY) / 2;
  wfState.panX = contentCX - viewW / 2;
  wfState.panY = contentCY - viewH / 2;
  wfApplyViewBox();
}

function wfApplyViewBox() {
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;
  const rect = svg.parentElement.getBoundingClientRect();
  const w = rect.width / wfState.zoom;
  const h = rect.height / wfState.zoom;
  svg.setAttribute('viewBox', `${wfState.panX} ${wfState.panY} ${w} ${h}`);
}

// Pan via mouse drag
function wfInitPan() {
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;

  svg.addEventListener('mousedown', (e) => {
    if (e.target === svg || e.target.tagName === 'svg') {
      wfState.isPanning = true;
      wfState.panStart = { x: e.clientX, y: e.clientY };
      svg.style.cursor = 'grabbing';
    }
  });

  svg.addEventListener('mousemove', (e) => {
    if (!wfState.isPanning) return;
    const dx = (e.clientX - wfState.panStart.x) / wfState.zoom;
    const dy = (e.clientY - wfState.panStart.y) / wfState.zoom;
    wfState.panX -= dx;
    wfState.panY -= dy;
    wfState.panStart = { x: e.clientX, y: e.clientY };
    wfApplyViewBox();
  });

  svg.addEventListener('mouseup', () => {
    wfState.isPanning = false;
    svg.style.cursor = '';
  });

  svg.addEventListener('mouseleave', () => {
    wfState.isPanning = false;
    svg.style.cursor = '';
  });

  // Scroll wheel zoom, centered on cursor position
  svg.addEventListener('wheel', (e) => {
    e.preventDefault();
    const factor = e.deltaY < 0 ? 1.03 : 1 / 1.03;
    const oldZoom = wfState.zoom;
    const newZoom = Math.max(0.2, Math.min(5, oldZoom * factor));
    // Zoom toward cursor: keep the SVG point under the mouse fixed
    const rect = svg.getBoundingClientRect();
    const mx = (e.clientX - rect.left) / oldZoom + wfState.panX;
    const my = (e.clientY - rect.top) / oldZoom + wfState.panY;
    wfState.zoom = newZoom;
    wfState.panX = mx - (e.clientX - rect.left) / newZoom;
    wfState.panY = my - (e.clientY - rect.top) / newZoom;
    wfApplyViewBox();
  }, { passive: false });

  // Click on canvas background to deselect
  svg.addEventListener('click', (e) => {
    if (e.target === svg || e.target.tagName === 'svg') {
      wfDeselectNode();
    }
  });
}

// ── Builder: Input Definitions ──
const WF_INPUT_DEFS = {
  query:       [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false, placeholder: 'Collection name' }, { key: 'db', type: 'text', required: false, placeholder: 'Database name' }, { key: 'limit', type: 'number', required: false, placeholder: '5' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
  search:      [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'limit', type: 'number', required: false, placeholder: '10' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
  rerank:      [{ key: 'query', type: 'text', required: true }, { key: 'documents', type: 'json', required: true, placeholder: '["doc1","doc2"]' }, { key: 'model', type: 'text', required: false, placeholder: 'rerank-2.5' }],
  ingest:      [{ key: 'text', type: 'textarea', required: true }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'source', type: 'text', required: false }, { key: 'chunkSize', type: 'number', required: false, placeholder: '512' }, { key: 'chunkStrategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }],
  embed:       [{ key: 'text', type: 'text', required: true, placeholder: 'Text to embed' }, { key: 'model', type: 'text', required: false, placeholder: 'voyage-4-large' }, { key: 'inputType', type: 'select', required: false, options: ['document','query'] }],
  similarity:  [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
  collections: [{ key: 'db', type: 'text', required: false }],
  models:      [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
  estimate:    [{ key: 'docs', type: 'number', required: true, placeholder: '1000' }, { key: 'queries', type: 'number', required: false, placeholder: '0' }, { key: 'months', type: 'number', required: false, placeholder: '12' }],
  explain:     [{ key: 'topic', type: 'text', required: true }],
  topics:      [{ key: 'search', type: 'text', required: false }],
  merge:       [{ key: 'sources', type: 'json', required: true, placeholder: '["step1.output","step2.output"]' }, { key: 'strategy', type: 'select', required: false, options: ['concat','interleave','unique'] }],
  filter:      [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
  transform:   [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
  generate:    [{ key: 'prompt', type: 'textarea', required: true, placeholder: 'Generate a summary of...' }, { key: 'context', type: 'text', required: false, placeholder: '{{ step.output }}' }],
  conditional: [{ key: 'condition', type: 'text', required: true, placeholder: '{{ step.output.results.length > 0 }}' }, { key: 'then', type: 'json', required: true, placeholder: '["step_a"]' }, { key: 'else', type: 'json', required: false, placeholder: '["step_b"]' }],
  loop:        [{ key: 'items', type: 'text', required: true, placeholder: '{{ step.output.results }}' }, { key: 'as', type: 'text', required: true, placeholder: 'item' }, { key: 'step', type: 'json', required: true, placeholder: '{"tool":"template","inputs":{"text":"{{ item }}"}}' }, { key: 'maxIterations', type: 'number', required: false, placeholder: '100' }],
  template:    [{ key: 'text', type: 'textarea', required: true, placeholder: 'Compose text with {{ step.output }} references' }],
  chunk:       [{ key: 'text', type: 'textarea', required: true, placeholder: '{{ step.output.text }}' }, { key: 'strategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }, { key: 'size', type: 'number', required: false, placeholder: '512' }, { key: 'overlap', type: 'number', required: false, placeholder: '50' }, { key: 'source', type: 'text', required: false, placeholder: 'document.md' }],
  aggregate:   [{ key: 'pipeline', type: 'json', required: true, placeholder: '[{"$group":{"_id":"$field","count":{"$sum":1}}}]' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }],
  http:        [{ key: 'url', type: 'text', required: true, placeholder: 'https://api.example.com/data' }, { key: 'method', type: 'select', required: false, options: ['GET','POST','PUT','PATCH','DELETE'] }, { key: 'headers', type: 'json', required: false, placeholder: '{"Authorization":"Bearer ..."}' }, { key: 'body', type: 'json', required: false, placeholder: '{}' }, { key: 'timeout', type: 'number', required: false, placeholder: '30000' }],
};

const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'processing', 'control', 'generation', 'integration', 'management', 'utility'];
const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation', processing: 'Processing', integration: 'Integration' };

// ── Builder: Library/Palette tab toggle ──
function wfSwitchLibTab(tab) {
  const libraryList = document.getElementById('wfLibraryList');
  const paletteList = document.getElementById('wfPaletteList');
  document.querySelectorAll('.wf-lib-tab').forEach(b => b.classList.toggle('active', b.dataset.libTab === tab));
  if (libraryList) libraryList.style.display = tab === 'library' ? '' : 'none';
  if (paletteList) paletteList.style.display = tab === 'palette' ? '' : 'none';
  if (tab === 'palette') wfRenderPalette();
}

// ── Builder: Palette rendering ──
function wfRenderPalette() {
  const container = document.getElementById('wfPaletteList');
  if (!container) return;
  const grouped = {};
  for (const [tool, meta] of Object.entries(WF_NODE_META)) {
    const cat = meta.category || 'unknown';
    if (!grouped[cat]) grouped[cat] = [];
    grouped[cat].push({ tool, ...meta });
  }
  let html = '';
  for (const cat of WF_CATEGORY_ORDER) {
    const items = grouped[cat];
    if (!items) continue;
    html += `<div class="wf-palette-category"><div class="wf-palette-category-title">${WF_CATEGORY_LABELS[cat] || cat}</div>`;
    for (const item of items) {
      html += `<div class="wf-palette-item" draggable="true" ondragstart="event.dataTransfer.setData('text/plain','${item.tool}')" onclick="wfAddNodeFromPalette('${item.tool}')">
        <span class="wf-palette-icon" style="color:${item.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${item.icon || WF_FALLBACK_ICON}"/></svg></span>
        <span class="wf-palette-label">${item.label}</span>
        <button class="wf-help-trigger" onclick="event.stopPropagation();wfOpenHelpModal('${item.tool}')" title="Help: ${item.label}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
      </div>`;
    }
    html += '</div>';
  }
  container.innerHTML = html;
}

// ── Builder: New Workflow ──
function wfNewWorkflow() {
  wfState.activeWorkflow = {
    name: 'Untitled Workflow',
    description: '',
    steps: [],
    inputs: {},
    defaults: {},
    output: {}
  };
  wfState.builderMode = true;
  wfState.dirtyFlag = false;
  wfState.selectedNodeId = null;
  wfState.executionState = {};
  wfState.executionResults = {};
  wfState.nodePositions = {};
  wfState.layers = [];
  wfState.graph = {};
  wfState.zoom = 1;
  wfState.panX = 0;
  wfState.panY = 0;
  wfState.draggingEdge = null;
  wfState.dragNode = null;

  // Clear canvas
  const svg = document.getElementById('wf-canvas');
  if (svg) svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
  document.getElementById('wfCanvasEmpty').style.display = 'none';
  svg.style.display = '';

  // Enable toolbar buttons
  document.getElementById('wfDryRunBtn').disabled = false;
  document.getElementById('wfRunBtn').disabled = false;
  document.getElementById('wfExportBtn').disabled = false;

  // Switch to palette tab
  wfSwitchLibTab('palette');

  // Open inspector for workflow-level editing
  wfOpenInspector();
  wfUpdateInspector();

  // Set viewBox
  wfApplyViewBox();
  
  // Trigger draft validation with debounce
  wfTriggerDraftValidation(300);
}

// ── Builder: Edit current workflow ──
function wfEditWorkflow() {
  if (!wfState.activeWorkflow) return;
  wfState.builderMode = true;
  wfState.dirtyFlag = false;
  wfState.selectedNodeId = null;
  wfState.draggingEdge = null;
  wfState.dragNode = null;

  // Re-render nodes with builder ports and drag handles
  wfRefreshNodes();

  // Switch to palette tab and open inspector for workflow-level editing
  wfSwitchLibTab('palette');
  wfOpenInspector();
  wfUpdateInspector();
}

// ── Builder: Add node from palette ──
function wfAddNodeFromPalette(tool) {
  if (!wfState.activeWorkflow) wfNewWorkflow();
  wfState.builderMode = true;
  const def = wfState.activeWorkflow;

  // Generate unique step ID
  let baseId = tool;
  let id = baseId;
  let counter = 2;
  const existingIds = new Set(def.steps.map(s => s.id));
  while (existingIds.has(id)) { id = baseId + '_' + counter; counter++; }

  // Build default inputs
  const inputDefs = WF_INPUT_DEFS[tool] || [];
  const inputs = {};
  for (const d of inputDefs) {
    if (d.required) inputs[d.key] = '';
  }

  const meta = WF_NODE_META[tool] || {};
  const step = {
    id,
    name: meta.label || tool,
    tool,
    inputs,
  };
  def.steps.push(step);

  // Position: place to the right of all existing nodes
  let maxX = WF_PAD;
  for (const pos of Object.values(wfState.nodePositions)) {
    if (pos.x + WF_NODE_W + WF_LAYER_GAP > maxX) maxX = pos.x + WF_NODE_W + WF_LAYER_GAP;
  }
  let y = WF_PAD;
  // Stack vertically if there are nodes in the same column
  const nodesAtX = Object.values(wfState.nodePositions).filter(p => Math.abs(p.x - maxX) < 20);
  if (nodesAtX.length > 0) {
    y = Math.max(...nodesAtX.map(p => p.y)) + WF_NODE_H + WF_NODE_GAP;
  }
  wfState.nodePositions[id] = { x: maxX, y };

  // Rebuild graph
  wfBuildGraph();
  wfRefreshNodes();
  wfSelectNode(id);
  wfState.dirtyFlag = true;
}

// ── Builder: Build graph from step inputs (template references) ──
function wfBuildGraph() {
  const def = wfState.activeWorkflow;
  if (!def) return;
  const graph = {};
  const stepIds = new Set(def.steps.map(s => s.id));
  for (const step of def.steps) {
    const deps = new Set();
    // Scan all input values for {{ stepId.xxx }} references
    for (const val of Object.values(step.inputs || {})) {
      const str = typeof val === 'string' ? val : JSON.stringify(val);
      const matches = str.matchAll(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\./g);
      for (const m of matches) {
        if (stepIds.has(m[1]) && m[1] !== step.id) deps.add(m[1]);
      }
    }
    graph[step.id] = Array.from(deps);
  }
  wfState.graph = graph;
}

// ── Builder: Canvas drop (from palette drag) ──
function wfCanvasDrop(e) {
  e.preventDefault();
  const tool = e.dataTransfer.getData('text/plain');
  if (!tool || !WF_NODE_META[tool]) return;
  if (!wfState.activeWorkflow) wfNewWorkflow();
  wfState.builderMode = true;
  const def = wfState.activeWorkflow;

  // Generate unique ID
  let id = tool;
  let counter = 2;
  const existingIds = new Set(def.steps.map(s => s.id));
  while (existingIds.has(id)) { id = tool + '_' + counter; counter++; }

  // Inputs
  const inputDefs = WF_INPUT_DEFS[tool] || [];
  const inputs = {};
  for (const d of inputDefs) { if (d.required) inputs[d.key] = ''; }

  const meta = WF_NODE_META[tool] || {};
  const step = { id, name: meta.label || tool, tool, inputs };
  def.steps.push(step);

  // Convert drop coordinates to SVG space
  const svg = document.getElementById('wf-canvas');
  const rect = svg.getBoundingClientRect();
  const svgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
  const svgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
  wfState.nodePositions[id] = { x: svgX, y: svgY };

  wfBuildGraph();
  wfRefreshNodes();
  wfSelectNode(id);
  wfState.dirtyFlag = true;
  
  // Trigger draft validation with debounce for drag-drop
  wfTriggerDraftValidation(300);
}

// ── Builder: Mutation helpers ──
function wfEditStepField(stepId, field, value) {
  const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
  if (!step) return;
  step[field] = value;
  wfState.dirtyFlag = true;
  if (field === 'name') wfRefreshNodes();
}

function wfEditStepInput(stepId, key, value) {
  const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
  if (!step) return;
  if (!step.inputs) step.inputs = {};
  step.inputs[key] = value;
  wfState.dirtyFlag = true;
  // Rebuild graph in case template refs changed
  wfBuildGraph();
  wfRefreshNodes();
  
  // Trigger draft validation with debounce for config changes
  wfTriggerDraftValidation(500);
}

function wfEditStepId(oldId, newId) {
  const def = wfState.activeWorkflow;
  if (!def) return;
  newId = newId.trim().replace(/[^a-zA-Z0-9_]/g, '_');
  if (!newId || newId === oldId) return;
  if (def.steps.some(s => s.id === newId)) return; // duplicate

  const step = def.steps.find(s => s.id === oldId);
  if (!step) return;
  step.id = newId;

  // Update position map
  if (wfState.nodePositions[oldId]) {
    wfState.nodePositions[newId] = wfState.nodePositions[oldId];
    delete wfState.nodePositions[oldId];
  }
  // Update template references in other steps
  for (const s of def.steps) {
    for (const [k, v] of Object.entries(s.inputs || {})) {
      if (typeof v === 'string' && v.includes('{{ ' + oldId + '.')) {
        s.inputs[k] = v.replaceAll('{{ ' + oldId + '.', '{{ ' + newId + '.');
      }
    }
  }
  if (wfState.selectedNodeId === oldId) wfState.selectedNodeId = newId;
  wfState.dirtyFlag = true;
  wfBuildGraph();
  wfRefreshNodes();
  wfUpdateInspector();
  
  // Trigger draft validation with debounce for ID changes
  wfTriggerDraftValidation(500);
}

function wfDeleteStep(stepId) {
  const def = wfState.activeWorkflow;
  if (!def) return;
  def.steps = def.steps.filter(s => s.id !== stepId);
  delete wfState.nodePositions[stepId];
  if (wfState.selectedNodeId === stepId) wfState.selectedNodeId = null;
  wfState.dirtyFlag = true;
  wfBuildGraph();
  wfRefreshNodes();
  wfUpdateInspector();
  
  // Trigger draft validation immediately on node deletion
  wfTriggerDraftValidation(0);
}

function wfEditWorkflowField(field, value) {
  if (!wfState.activeWorkflow) return;
  wfState.activeWorkflow[field] = value;
  wfState.dirtyFlag = true;
}

// ── Builder: Validate ──
async function wfValidateBuilder() {
  const def = wfState.activeWorkflow;
  if (!def) return null;
  try {
    const res = await fetch('/api/workflows/validate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ definition: def }),
    });
    return await res.json();
  } catch (err) {
    return { valid: false, errors: [err.message] };
  }
}

// ── Client-side Draft Validator ──
const VALID_TOOLS = [
  'query', 'search', 'rerank', 'embed', 'similarity', 'ingest', 
  'collections', 'models', 'explain', 'estimate', 'merge', 'filter', 
  'transform', 'generate', 'conditional', 'loop', 'template', 
  'chunk', 'aggregate', 'http'
];

function draftValidate(workflow) {
  const issues = [];
  const steps = workflow.steps || [];
  const stepIds = new Set(steps.map(s => s.id));

  // Duplicate IDs (always error)
  const seen = new Set();
  for (const step of steps) {
    if (seen.has(step.id)) {
      issues.push({ 
        severity: 'error', 
        stepId: step.id, 
        code: 'DUPLICATE_ID',
        message: `Duplicate step ID "${step.id}"` 
      });
    }
    seen.add(step.id);
  }

  // Invalid tools (always error)
  for (const step of steps) {
    if (step.tool && !VALID_TOOLS.includes(step.tool)) {
      issues.push({ 
        severity: 'error', 
        stepId: step.id, 
        code: 'INVALID_TOOL',
        message: `Unknown tool "${step.tool}"` 
      });
    }
  }

  // Unknown step references (warning in draft)
  const templateRefPattern = /\{\{\s*(\w+)\.output/g;
  for (const step of steps) {
    const json = JSON.stringify(step.inputs || {});
    let match;
    while ((match = templateRefPattern.exec(json)) !== null) {
      const refId = match[1];
      if (refId !== 'inputs' && refId !== 'defaults' && !stepIds.has(refId)) {
        issues.push({ 
          severity: 'warning', 
          stepId: step.id, 
          code: 'UNKNOWN_STEP_REF',
          message: `References unknown step "${refId}"`, 
          referencedStep: refId 
        });
      }
    }
  }

  // Orphan nodes (info in draft)
  const referenced = new Set();
  const hasRefs = new Set();
  for (const step of steps) {
    const json = JSON.stringify(step.inputs || {});
    let match;
    const refPattern = /\{\{\s*(\w+)\.output/g;
    while ((match = refPattern.exec(json)) !== null) {
      if (match[1] !== 'inputs' && match[1] !== 'defaults') {
        referenced.add(match[1]);
        hasRefs.add(step.id);
      }
    }
  }
  
  for (const step of steps) {
    if (!referenced.has(step.id) && !hasRefs.has(step.id) && steps.length > 1) {
      issues.push({ 
        severity: 'info', 
        stepId: step.id, 
        code: 'ORPHAN_NODE',
        message: `Step "${step.id}" is not connected to any other step` 
      });
    }
  }

  // Circular dependencies (always error) - simplified check
  function hasCycle() {
    const visiting = new Set();
    const visited = new Set();
    
    function visit(stepId) {
      if (visiting.has(stepId)) return true; // cycle found
      if (visited.has(stepId)) return false;
      
      visiting.add(stepId);
      const step = steps.find(s => s.id === stepId);
      if (step) {
        const json = JSON.stringify(step.inputs || {});
        let match;
        const refPattern = /\{\{\s*(\w+)\.output/g;
        while ((match = refPattern.exec(json)) !== null) {
          if (match[1] !== 'inputs' && match[1] !== 'defaults') {
            if (visit(match[1])) return true;
          }
        }
      }
      visiting.delete(stepId);
      visited.add(stepId);
      return false;
    }
    
    for (const step of steps) {
      if (!visited.has(step.id) && visit(step.id)) return true;
    }
    return false;
  }
  
  if (hasCycle()) {
    issues.push({
      severity: 'error',
      stepId: steps[0]?.id || '',
      code: 'CIRCULAR_DEPENDENCY',
      message: 'Circular dependency detected in workflow'
    });
  }

  const errors = issues.filter(i => i.severity === 'error');
  return {
    valid: errors.length === 0,
    mode: 'draft',
    issues,
    stats: {
      totalSteps: steps.length,
      errors: errors.length,
      warnings: issues.filter(i => i.severity === 'warning').length,
      info: issues.filter(i => i.severity === 'info').length
    }
  };
}

// ── Validation State Management ──
function wfApplyValidationState(issues) {
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;
  
  // Clear existing validation classes and badges
  svg.querySelectorAll('.wf-node').forEach(node => {
    node.classList.remove('wf-node-warning', 'wf-node-info', 'wf-node-error', 'wf-node-orphan');
    // Remove existing badges
    node.querySelectorAll('.wf-validation-badge-group').forEach(badge => badge.remove());
  });
  
  // Group issues by step ID
  const stepIssues = {};
  for (const issue of issues) {
    if (!stepIssues[issue.stepId]) stepIssues[issue.stepId] = [];
    stepIssues[issue.stepId].push(issue);
  }
  
  // Apply visual states to nodes
  for (const [stepId, stepIssueList] of Object.entries(stepIssues)) {
    const node = svg.querySelector(`[data-step-id="${stepId}"]`);
    if (!node) continue;
    
    // Determine highest severity for visual state
    const hasError = stepIssueList.some(i => i.severity === 'error');
    const hasWarning = stepIssueList.some(i => i.severity === 'warning');
    const hasInfo = stepIssueList.some(i => i.severity === 'info');
    const hasOrphan = stepIssueList.some(i => i.code === 'ORPHAN_NODE');
    
    if (hasError) {
      node.classList.add('wf-node-error');
    } else if (hasWarning) {
      node.classList.add('wf-node-warning');
    } else if (hasOrphan) {
      node.classList.add('wf-node-orphan');
    } else if (hasInfo) {
      node.classList.add('wf-node-info');
    }
    
    // Add badge with tooltip
    const badges = [];
    if (hasError) badges.push({ icon: '❌', type: 'error' });
    else if (hasWarning) badges.push({ icon: '⚠️', type: 'warning' });
    else if (hasOrphan) badges.push({ icon: '', type: 'orphan' }); // No badge for orphan
    else if (hasInfo) badges.push({ icon: '🔧', type: 'info' });
    
    if (badges.length > 0) {
      const badge = badges[0]; // Show only the most severe
      if (badge.icon) { // Only show badge if there's an icon
        const badgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        badgeGroup.className = 'wf-validation-badge-group';
        
        const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        bgRect.className = 'wf-validation-badge-bg';
        bgRect.setAttribute('x', '44');
        bgRect.setAttribute('y', '2');
        bgRect.setAttribute('width', '16');
        bgRect.setAttribute('height', '16');
        bgRect.setAttribute('fill', badge.type === 'error' ? '#EF5350' : badge.type === 'warning' ? '#FFB74D' : '#616161');
        
        const badgeText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        badgeText.className = 'wf-validation-badge';
        badgeText.setAttribute('x', '52');
        badgeText.setAttribute('y', '10');
        badgeText.setAttribute('fill', 'white');
        badgeText.textContent = badge.icon;
        
        badgeGroup.appendChild(bgRect);
        badgeGroup.appendChild(badgeText);
        
        // Add tooltip
        const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
        title.textContent = stepIssueList.map(i => i.message).join('\n');
        badgeGroup.appendChild(title);
        
        node.appendChild(badgeGroup);
      }
    }
  }
  
  // Update validation status bar
  wfUpdateValidationBar(issues);
}

function wfUpdateValidationBar(issues) {
  const bar = document.getElementById('wfValidationBar');
  const text = document.getElementById('wfValidationBarText');
  if (!bar || !text) return;
  
  const errors = issues.filter(i => i.severity === 'error').length;
  const warnings = issues.filter(i => i.severity === 'warning').length;
  const infos = issues.filter(i => i.severity === 'info').length;
  
  if (errors > 0) {
    bar.className = 'wf-validation-bar error';
    bar.style.display = 'flex';
    text.textContent = `❌ ${errors} structural error${errors === 1 ? '' : 's'} — must fix before running`;
  } else if (warnings > 0) {
    bar.className = 'wf-validation-bar warning';
    bar.style.display = 'flex';
    text.textContent = `⚠️ ${warnings} unresolved reference${warnings === 1 ? '' : 's'}`;
  } else if (infos > 0) {
    bar.className = 'wf-validation-bar info';
    bar.style.display = 'flex';
    text.textContent = `🔧 ${infos} step${infos === 1 ? '' : 's'} need${infos === 1 ? 's' : ''} configuration`;
  } else {
    bar.style.display = 'none';
  }
  
  // Make status bar clickable to pan to first issue
  bar.onclick = () => {
    if (issues.length > 0) {
      const firstIssue = issues[0];
      wfPanToNode(firstIssue.stepId);
    }
  };
}

function wfPanToNode(stepId) {
  const position = wfState.nodePositions[stepId];
  if (!position) return;
  
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;
  
  const rect = svg.getBoundingClientRect();
  const centerX = rect.width / 2;
  const centerY = rect.height / 2;
  
  wfState.panX = centerX / wfState.zoom - position.x - 30; // 30 = half node width
  wfState.panY = centerY / wfState.zoom - position.y - 20; // 20 = half node height
  
  wfRefreshNodes();
}

// Show validation errors in dry-run panel instead of toasts
function wfShowValidationErrors(errors, action = 'Action') {
  const canvasArea = document.querySelector('.wf-canvas-area');
  if (!canvasArea) return;
  
  // Remove any existing overlay
  canvasArea.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
  
  const overlay = document.createElement('div');
  overlay.className = 'wf-dryrun-overlay';
  
  const panel = document.createElement('div');
  panel.className = 'wf-dryrun-panel';
  
  const header = document.createElement('div');
  header.className = 'wf-dryrun-header';
  header.innerHTML = `
    <h3>Validation Errors</h3>
    <button onclick="this.closest('.wf-dryrun-overlay').remove()" style="background:none;border:none;color:var(--text-dim);font-size:18px;cursor:pointer;">✕</button>
  `;
  
  const content = document.createElement('div');
  content.className = 'wf-dryrun-content';
  content.innerHTML = `
    <div class="wf-validation-errors">
      <p style="margin-bottom: 12px; color: var(--text-dim);">
        Cannot ${action.toLowerCase()} due to the following errors:
      </p>
      <ul>
        ${errors.map(error => `<li>${error}</li>`).join('')}
      </ul>
    </div>
  `;
  
  const footer = document.createElement('div');
  footer.className = 'wf-dryrun-footer';
  footer.innerHTML = `
    <button class="wf-dryrun-close" onclick="this.closest('.wf-dryrun-overlay').remove()">
      Close
    </button>
  `;
  
  panel.appendChild(header);
  panel.appendChild(content);
  panel.appendChild(footer);
  overlay.appendChild(panel);
  canvasArea.appendChild(overlay);
}

// Standalone validate workflow function for command palette
async function wfValidateWorkflow() {
  const def = wfState.activeWorkflow;
  if (!def) return;
  
  const validation = await wfValidateBuilder();
  if (validation && !validation.valid) {
    wfShowValidationErrors(validation.errors || [], 'Validate');
  } else {
    // Show success message
    const canvasArea = document.querySelector('.wf-canvas-area');
    if (!canvasArea) return;
    
    // Remove any existing overlay
    canvasArea.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
    
    const overlay = document.createElement('div');
    overlay.className = 'wf-dryrun-overlay';
    
    const panel = document.createElement('div');
    panel.className = 'wf-dryrun-panel';
    panel.innerHTML = `
      <div class="wf-dryrun-header">
        <h3>Validation Success</h3>
        <button onclick="this.closest('.wf-dryrun-overlay').remove()" style="background:none;border:none;color:var(--text-dim);font-size:18px;cursor:pointer;">✕</button>
      </div>
      <div class="wf-dryrun-content">
        <p style="color: var(--success); text-align: center; padding: 24px;">
          ✅ Workflow validation passed. Ready to run!
        </p>
      </div>
      <div class="wf-dryrun-footer">
        <button class="wf-dryrun-close" onclick="this.closest('.wf-dryrun-overlay').remove()">Close</button>
      </div>
    `;
    
    overlay.appendChild(panel);
    canvasArea.appendChild(overlay);
  }
}

// Debounced validation triggering
let wfDraftValidateTimeout = null;

function wfTriggerDraftValidation(delayMs = 0) {
  if (!wfState.builderMode || !wfState.activeWorkflow) return;
  
  clearTimeout(wfDraftValidateTimeout);
  wfDraftValidateTimeout = setTimeout(() => {
    const result = draftValidate(wfState.activeWorkflow);
    wfApplyValidationState(result.issues);
  }, delayMs);
}

// ── Builder: Edge drag ──
function wfEdgeDragStart(fromId, fromX, fromY) {
  const svg = document.getElementById('wf-canvas');
  if (!svg) return;
  wfState.draggingEdge = { fromId, fromX, fromY };

  // Create temp edge (dashed bezier), pointer-events: none so it doesn't block port hit-testing
  const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  tempPath.setAttribute('id', 'wf-temp-edge');
  tempPath.setAttribute('fill', 'none');
  tempPath.setAttribute('stroke', 'var(--accent)');
  tempPath.setAttribute('stroke-width', '2');
  tempPath.setAttribute('stroke-dasharray', '6 4');
  tempPath.setAttribute('pointer-events', 'none');
  tempPath.setAttribute('d', `M${fromX},${fromY} L${fromX},${fromY}`);
  svg.appendChild(tempPath);

  function onMove(e) {
    const rect = svg.getBoundingClientRect();
    const mx = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
    const my = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
    const dx = Math.abs(mx - fromX) * 0.5;
    tempPath.setAttribute('d', `M${fromX},${fromY} C${fromX + dx},${fromY} ${mx - dx},${my} ${mx},${my}`);
  }

  function onUp() {
    document.removeEventListener('mousemove', onMove);
    document.removeEventListener('mouseup', onUp);
    const el = document.getElementById('wf-temp-edge');
    if (el) el.remove();
    wfState.draggingEdge = null;
  }

  document.addEventListener('mousemove', onMove);
  document.addEventListener('mouseup', onUp);
}

function wfEdgeDropOnInput(toId) {
  if (!wfState.draggingEdge) return;
  const fromId = wfState.draggingEdge.fromId;
  if (fromId === toId) return; // no self-connections

  // Add template reference to the target step's first empty required input
  const def = wfState.activeWorkflow;
  if (!def) return;
  const targetStep = def.steps.find(s => s.id === toId);
  if (!targetStep) return;

  const inputDefs = WF_INPUT_DEFS[targetStep.tool] || [];
  let connected = false;

  // Try to fill the first empty required input with a template reference
  for (const d of inputDefs) {
    if (!targetStep.inputs) targetStep.inputs = {};
    const current = targetStep.inputs[d.key];
    if (!current || current === '') {
      targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
      connected = true;
      break;
    }
  }

  // If no empty required input, try first empty optional input
  if (!connected) {
    for (const d of inputDefs) {
      const current = targetStep.inputs?.[d.key];
      if (!current || current === '') {
        if (!targetStep.inputs) targetStep.inputs = {};
        targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
        connected = true;
        break;
      }
    }
  }

  if (connected) {
    wfState.dirtyFlag = true;
    wfBuildGraph();
    wfRefreshNodes();
    if (wfState.selectedNodeId === toId) wfUpdateInspector();

    // Trigger draft validation immediately on edge connection
    wfTriggerDraftValidation(0);
  }

  // Clean up drag state
  wfState.draggingEdge = null;
  const el = document.getElementById('wf-temp-edge');
  if (el) el.remove();
}

/// ── Builder: Relayout via auto-layout algorithm ──
// (delegates to wfRelayout defined earlier, which uses wfAutoLayout for
// barycenter crossing minimization + neighbor-aware placement)

// ── Docs shortcut (F1) ──
const DOCS_URLS = {
  embed:      'https://docs.vaicli.com/docs/commands/embeddings/embed',
  compare:    'https://docs.vaicli.com/docs/commands/embeddings/similarity',
  search:     'https://docs.vaicli.com/docs/commands/embeddings/rerank',
  multimodal: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
  generate:   'https://docs.vaicli.com/docs/commands/project-setup/generate',
  chat:       'https://docs.vaicli.com/docs/commands/advanced/chat',
  workflows:  'https://docs.vaicli.com/docs/commands/advanced/workflow-run',
  benchmark:  'https://docs.vaicli.com/docs/commands/evaluation/benchmark',
  explore:    'https://docs.vaicli.com/docs/commands/tools-and-learning/explain',
  about:      'https://docs.vaicli.com/docs/',
  settings:   'https://docs.vaicli.com/docs/commands/tools-and-learning/config',
};
document.addEventListener('keydown', (e) => {
  if (e.key === 'F1') {
    e.preventDefault();
    const tabName = getActiveTabId();
    const url = DOCS_URLS[tabName] || 'https://docs.vaicli.com';
    window.open(url, '_blank', 'noopener');
  }
});

// Keyboard shortcuts (when workflows tab is active)
document.addEventListener('keydown', (e) => {
  if (getActiveTabId() !== 'workflows') return;

  // Ctrl/Cmd+S to save/export in builder mode (works even in inputs)
  if ((e.ctrlKey || e.metaKey) && e.key === 's' && wfState.builderMode) {
    e.preventDefault();
    wfExportJson();
    return;
  }

  // Delete/Backspace to remove selected node in builder mode
  if ((e.key === 'Delete' || e.key === 'Backspace') && wfState.builderMode && wfState.selectedNodeId) {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
    e.preventDefault();
    wfDeleteStep(wfState.selectedNodeId);
    return;
  }

  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

  const PAN_STEP = 40;
  if (e.key === '+' || e.key === '=') { wfZoom(1); e.preventDefault(); }
  else if (e.key === '-') { wfZoom(-1); e.preventDefault(); }
  else if (e.key === '0') { wfFitToView(); e.preventDefault(); }
  else if (e.key === 'l' || e.key === 'L') { wfRelayout(); e.preventDefault(); }
  else if (e.key === 'Escape') { wfDeselectNode(); e.preventDefault(); }
  else if (e.key === 'ArrowLeft')  { wfState.panX -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
  else if (e.key === 'ArrowRight') { wfState.panX += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
  else if (e.key === 'ArrowUp')    { wfState.panY -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
  else if (e.key === 'ArrowDown')  { wfState.panY += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
});

// ── Init ──
// ── Workflow Store ──
const WF_STORE_TOOL_COLORS = {
  query:'#00D4AA',search:'#40E0FF',rerank:'#0EA5E9',embed:'#8B5CF6',similarity:'#A78BFA',
  ingest:'#10B981',collections:'#F59E0B',models:'#EF4444',explain:'#EC4899',estimate:'#F97316',
  merge:'#6366F1',filter:'#14B8A6',transform:'#D946EF',generate:'#F472B6'
};
const WF_STORE_CATEGORIES = [
  { id:'all', label:'All', icon:'◈' }, { id:'retrieval', label:'Retrieval', icon:'⊕' },
  { id:'analysis', label:'Analysis', icon:'◉' }, { id:'domain-specific', label:'Domain', icon:'◆' },
  { id:'ingestion', label:'Ingestion', icon:'⊞' }, { id:'utility', label:'Utility', icon:'⊡' },
  { id:'integration', label:'Integration', icon:'⊗' },
];
// Hardcoded fallback data matching the spec's 20 workflows
const WF_STORE_FALLBACK = [
  { name:'model-shootout', packageName:'@vaicli/vai-workflow-model-shootout', description:'Compare voyage-4-large, voyage-4, and voyage-4-lite side-by-side on your data.', category:'utility', tags:['benchmarking','model-comparison','cost','shared-space'], tools:['query','estimate','similarity','generate'], steps:7, tier:'official', downloads:3241, featured:true, installed:false, gradient:'linear-gradient(135deg, #0D9488, #06B6D4)' },
  { name:'asymmetric-search', packageName:'@vaicli/vai-workflow-asymmetric-search', description:'The canonical shared embedding space demo. Embed with voyage-4-large, query with voyage-4-lite. ~83% cost reduction.', category:'retrieval', tags:['asymmetric','shared-space','cost-savings'], tools:['embed','search','rerank'], steps:3, tier:'official', downloads:4812, featured:true, installed:false, gradient:'linear-gradient(135deg, #00D4AA, #40E0FF)' },
  { name:'cost-optimizer', packageName:'@vaicli/vai-workflow-cost-optimizer', description:'Quantify the exact cost savings of asymmetric retrieval on your collection.', category:'utility', tags:['cost','optimization','asymmetric'], tools:['query','estimate','similarity','generate'], steps:6, tier:'official', downloads:2918, featured:true, installed:false, gradient:'linear-gradient(135deg, #F59E0B, #EF4444)' },
  { name:'question-decomposition', packageName:'@vaicli/vai-workflow-question-decomposition', description:'Break complex questions into focused sub-queries, search each in parallel, merge and rerank.', category:'retrieval', tags:['decomposition','fan-out','synthesis'], tools:['generate','query','merge','rerank'], steps:6, tier:'official', downloads:1876, installed:false, gradient:'linear-gradient(135deg, #8B5CF6, #EC4899)' },
  { name:'contract-clause-finder', packageName:'@vaicli/vai-workflow-contract-clause-finder', description:'Search legal documents for specific clause types using voyage-law-2.', category:'domain-specific', tags:['legal','contracts','voyage-law-2'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:1543, installed:false, gradient:'linear-gradient(135deg, #1E40AF, #7C3AED)' },
  { name:'knowledge-base-bootstrap', packageName:'@vaicli/vai-workflow-knowledge-base-bootstrap', description:'End-to-end onboarding: ingest documents, verify, test a query, and generate a status report.', category:'integration', tags:['onboarding','bootstrap','end-to-end'], tools:['ingest','collections','query','generate'], steps:4, tier:'official', downloads:3654, installed:false, gradient:'linear-gradient(135deg, #059669, #10B981)' },
  { name:'embedding-drift-detector', packageName:'@vaicli/vai-workflow-embedding-drift-detector', description:'Monitor embedding quality over time. Re-embed sample documents and compare against stored vectors.', category:'analysis', tags:['monitoring','drift','operations'], tools:['search','embed','similarity','generate'], steps:5, tier:'official', downloads:987, installed:false, gradient:'linear-gradient(135deg, #DC2626, #F97316)' },
  { name:'multilingual-search', packageName:'@vaicli/vai-workflow-multilingual-search', description:'Translate your query into multiple languages, search each in parallel, merge and rerank.', category:'retrieval', tags:['multilingual','translation','cross-lingual'], tools:['generate','query','merge','rerank'], steps:5, tier:'official', downloads:1234, installed:false, gradient:'linear-gradient(135deg, #0EA5E9, #6366F1)' },
  { name:'financial-risk-scanner', packageName:'@vaicli/vai-workflow-financial-risk-scanner', description:'Scan financial documents for risk signals using voyage-finance-2.', category:'domain-specific', tags:['finance','risk','voyage-finance-2'], tools:['query','rerank','filter','generate'], steps:4, tier:'official', downloads:1102, installed:false, gradient:'linear-gradient(135deg, #B45309, #D97706)' },
  { name:'doc-freshness', packageName:'@vaicli/vai-workflow-doc-freshness', description:'Audit your knowledge base for stale content. Gathers metadata from 5 tools in parallel.', category:'utility', tags:['freshness','maintenance','monitoring'], tools:['collections','models','search','explain','estimate','generate'], steps:6, tier:'official', downloads:1456, installed:false, gradient:'linear-gradient(135deg, #4338CA, #7C3AED)' },
  { name:'incremental-sync', packageName:'@vaicli/vai-workflow-incremental-sync', description:'Smart ingestion with deduplication. Checks similarity before storing.', category:'ingestion', tags:['dedup','sync','conditional'], tools:['search','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:2103, installed:false, gradient:'linear-gradient(135deg, #15803D, #4ADE80)' },
  { name:'rag-ab-test', packageName:'@vaicli/vai-workflow-rag-ab-test', description:'Run the same query through two RAG configurations side-by-side.', category:'integration', tags:['ab-test','comparison','evaluation'], tools:['query','generate'], steps:5, tier:'official', downloads:1678, installed:false, gradient:'linear-gradient(135deg, #BE185D, #F472B6)' },
  { name:'hybrid-precision-search', packageName:'@vaicli/vai-workflow-hybrid-precision-search', description:'Three retrieval strategies in parallel — lite, large, and filtered — merged and reranked.', category:'retrieval', tags:['hybrid','parallel','precision'], tools:['query','search','merge','rerank'], steps:5, tier:'official', downloads:1890, installed:false, gradient:'linear-gradient(135deg, #0891B2, #22D3EE)' },
  { name:'code-migration-helper', packageName:'@vaicli/vai-workflow-code-migration-helper', description:'Find similar code patterns across your codebase using voyage-code-3.', category:'domain-specific', tags:['code','migration','voyage-code-3'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:932, installed:false, gradient:'linear-gradient(135deg, #475569, #94A3B8)' },
  { name:'meeting-action-items', packageName:'@vaicli/vai-workflow-meeting-action-items', description:'Ingest meeting notes and extract structured action items with owners, deadlines, and context.', category:'domain-specific', tags:['meetings','action-items','productivity'], tools:['ingest','query','generate'], steps:3, tier:'official', downloads:2567, installed:false, gradient:'linear-gradient(135deg, #7C2D12, #EA580C)' },
  { name:'collection-overlap-audit', packageName:'@vaicli/vai-workflow-collection-overlap-audit', description:'Detect duplicate content across two collections.', category:'analysis', tags:['dedup','overlap','audit'], tools:['search','similarity','merge','filter','generate'], steps:5, tier:'official', downloads:678, installed:false, gradient:'linear-gradient(135deg, #6D28D9, #A78BFA)' },
  { name:'query-quality-scorer', packageName:'@vaicli/vai-workflow-query-quality-scorer', description:'Evaluate retrieval quality without ground truth labels.', category:'analysis', tags:['evaluation','quality','scoring'], tools:['query','similarity','transform','generate'], steps:4, tier:'official', downloads:823, installed:false, gradient:'linear-gradient(135deg, #9333EA, #C084FC)' },
  { name:'clinical-protocol-match', packageName:'@vaicli/vai-workflow-clinical-protocol-match', description:'Match clinical presentations to treatment protocols.', category:'domain-specific', tags:['clinical','healthcare','protocols'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:712, installed:false, gradient:'linear-gradient(135deg, #0F766E, #2DD4BF)' },
  { name:'batch-quality-gate', packageName:'@vaicli/vai-workflow-batch-quality-gate', description:'Quality-filtered ingestion. Only ingest content that meets your similarity threshold.', category:'ingestion', tags:['quality','gate','filtering'], tools:['search','embed','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:543, installed:false, gradient:'linear-gradient(135deg, #166534, #86EFAC)' },
  { name:'index-health-check', packageName:'@vaicli/vai-workflow-index-health-check', description:'Diagnostic pipeline for your vector index.', category:'utility', tags:['diagnostics','health','index'], tools:['collections','search','query','estimate','generate'], steps:5, tier:'official', downloads:1345, installed:false, gradient:'linear-gradient(135deg, #1D4ED8, #60A5FA)' },
];

// Inject branding into fallback data (matches DEFAULT_BRANDING in catalog API)
const _WF_FALLBACK_BRANDING = {
  'model-shootout':           { icon:'trophy',        color:'#0D9488',  iconPath:'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z' },
  'asymmetric-search':        { icon:'split',         color:'#00D4AA',  iconPath:'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22' },
  'cost-optimizer':           { icon:'dollar-sign',   color:'#F59E0B',  iconPath:'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' },
  'question-decomposition':   { icon:'sparkle',       color:'#8B5CF6',  iconPath:'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z' },
  'contract-clause-finder':   { icon:'file-search',   color:'#1E40AF',  iconPath:'M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM9.5 12.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0-5 0M13.3 14.3 15 16' },
  'knowledge-base-bootstrap': { icon:'database',      color:'#059669',  iconPath:'M21 5c0 1.1-3.134 3-9 3S3 6.1 3 5M21 5c0-1.1-3.134-3-9-3S3 3.9 3 5M21 5v14c0 1.1-3.134 3-9 3s-9-1.9-9-3V5M21 12c0 1.1-3.134 3-9 3s-9-1.9-9-3' },
  'embedding-drift-detector': { icon:'activity',      color:'#DC2626',  iconPath:'M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2' },
  'multilingual-search':      { icon:'globe',         color:'#0EA5E9',  iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' },
  'financial-risk-scanner':   { icon:'shield-alert',  color:'#B45309',  iconPath:'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6zM12 8v4M12 16h.01' },
  'doc-freshness':            { icon:'timer',         color:'#4338CA',  iconPath:'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z' },
  'incremental-sync':         { icon:'refresh-cw',    color:'#15803D',  iconPath:'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16' },
  'rag-ab-test':              { icon:'flask-conical', color:'#BE185D',  iconPath:'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10' },
  'hybrid-precision-search':  { icon:'target',        color:'#0891B2',  iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0' },
  'code-migration-helper':    { icon:'code',          color:'#475569',  iconPath:'M16 18l6-6-6-6M8 6l-6 6 6 6' },
  'meeting-action-items':     { icon:'clipboard-list',color:'#7C2D12',  iconPath:'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M9 2h6a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zM12 11h4M12 16h4M8 11h.01M8 16h.01' },
  'collection-overlap-audit': { icon:'layers',        color:'#6D28D9',  iconPath:'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84zM2 12l8.58 3.91a2 2 0 0 0 1.66 0L22 12M2 17l8.58 3.91a2 2 0 0 0 1.66 0L22 17' },
  'query-quality-scorer':     { icon:'microscope',    color:'#9333EA',  iconPath:'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z' },
  'clinical-protocol-match':  { icon:'heart-pulse',   color:'#0F766E',  iconPath:'M19.5 12.572l-7.5 7.428-7.5-7.428A5 5 0 0 1 7.5 5c1.8 0 3.3.9 4.5 2.7C13.2 5.9 14.7 5 16.5 5a5 5 0 0 1 3 9.572zM12 6l-1 4h4l-1 4' },
  'batch-quality-gate':       { icon:'check-circle',  color:'#166534',  iconPath:'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3' },
  'index-health-check':       { icon:'bar-chart-3',   color:'#1D4ED8',  iconPath:'M12 20V10M18 20V4M6 20v-4' },
};
WF_STORE_FALLBACK.forEach(wf => {
  wf.branding = _WF_FALLBACK_BRANDING[wf.name] || { icon:'zap', color:'#64748B', iconPath:'M13 2 3 14h9l-1 8 10-12h-9l1-8z' };
  wf.verified = true;
  wf.security = [];
  wf.rating = null;
});

function getCapabilityBadges(tools) {
  if (!Array.isArray(tools)) return [];
  const badges = [];
  if (tools.includes('http')) badges.push({ emoji: '🌐', label: 'NETWORK', cls: 'wf-store-badge-cap-network' });
  if (tools.includes('ingest') || tools.includes('aggregate')) badges.push({ emoji: '💾', label: 'WRITE_DB', cls: 'wf-store-badge-cap-writedb' });
  if (tools.includes('generate')) badges.push({ emoji: '🤖', label: 'LLM', cls: 'wf-store-badge-cap-llm' });
  if (tools.includes('loop') || tools.includes('forEach')) badges.push({ emoji: '🔄', label: 'LOOP', cls: 'wf-store-badge-cap-loop' });
  if (tools.some(t => ['query','search','collections','aggregate'].includes(t))) badges.push({ emoji: '📊', label: 'READ_DB', cls: 'wf-store-badge-cap-readdb' });
  return badges;
}

let _wfStoreUserRatings = {};

let _wfStoreCatalog = null;
let _wfStoreCat = 'all';

function wfStoreOpen() {
  const overlay = document.getElementById('wfStoreOverlay');
  if (!overlay) return;
  overlay.classList.add('open');
  document.getElementById('wfStoreBtn').classList.add('active');
  // Inject brand shapes behind store content
  try {
    if (!overlay.querySelector('.wf-store-brand-shapes')) {
      const shapes = document.createElement('div');
      shapes.className = 'wf-store-brand-shapes';
      const w = overlay.offsetWidth || 900, h = overlay.offsetHeight || 600;
      shapes.innerHTML = generateNebulaSVG(w, h, 260, {variant: 'medium', opacity: 0.08}) +
        generateOrbitalArcsSVG(w, h, 410, {opacity: 0.12});
      overlay.insertBefore(shapes, overlay.firstChild);
    }
  } catch (e) { console.warn('Brand shapes injection failed:', e); }
  if (!_wfStoreCatalog) wfStoreFetchCatalog();
  else wfStoreRender();
}

function wfStoreClose() {
  const overlay = document.getElementById('wfStoreOverlay');
  if (overlay) overlay.classList.remove('open');
  document.getElementById('wfStoreBtn').classList.remove('active');
  // Close detail modal if open
  const detail = document.getElementById('wfStoreDetail');
  if (detail) detail.remove();
}

// Store icons map (populated from API response or used from inline fallback)
let _wfStoreIcons = {};

// Render a branding icon as inline SVG
function wfBrandingIcon(wf, size = 16) {
  const b = wf.branding || {};
  const iconPath = b.iconPath || _wfStoreIcons[b.icon] || '';
  if (!iconPath) return '';
  return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${iconPath}"/></svg>`;
}

async function wfStoreFetchCatalog() {
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    const res = await fetch('/api/workflows/catalog', {signal: controller.signal});
    clearTimeout(timeout);
    const data = await res.json();
    if (data.icons) _wfStoreIcons = data.icons;
    if (data.workflows && data.workflows.length > 0) {
      _wfStoreCatalog = data.workflows;
    } else {
      _wfStoreCatalog = WF_STORE_FALLBACK;
    }
  } catch {
    _wfStoreCatalog = WF_STORE_FALLBACK;
  }
  wfStoreRender();
}

function wfStoreRender() {
  const container = document.getElementById('wfStoreContent');
  if (!container || !_wfStoreCatalog) return;
  const search = (document.getElementById('wfStoreSearch')?.value || '').toLowerCase();
  const sort = document.getElementById('wfStoreSort')?.value || 'downloads';

  const filtered = _wfStoreCatalog.filter(wf => {
    const mc = _wfStoreCat === 'all' || wf.category === _wfStoreCat;
    const ms = !search || wf.name.includes(search) || (wf.description||'').toLowerCase().includes(search)
      || (wf.tags||[]).some(t => t.includes(search)) || (wf.tools||[]).some(t => t.includes(search));
    return mc && ms;
  }).sort((a, b) => sort === 'downloads' ? (b.downloads||0) - (a.downloads||0) : sort === 'name' ? a.name.localeCompare(b.name) : (b.steps||0) - (a.steps||0));

  const featured = _wfStoreCatalog.filter(w => w.featured);
  const showFeat = _wfStoreCat === 'all' && !search;

  let html = '';
  // Category chips
  html += '<div class="wf-store-chips">';
  WF_STORE_CATEGORIES.forEach(c => {
    html += `<button class="wf-store-chip ${_wfStoreCat===c.id?'active':''}" onclick="_wfStoreCat='${c.id}';wfStoreRender()">${c.icon} ${c.label}</button>`;
  });
  html += '</div>';

  // Featured section
  if (showFeat && featured.length > 0) {
    html += '<div class="wf-store-section-label">Featured</div>';
    html += '<div class="wf-store-featured-grid">';
    featured.forEach(wf => {
      const featAuthor = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
      const featAvatarHtml = featAuthor ? (featAuthor.avatar
        ? `<div class="wf-store-featured-avatar"><img src="${featAuthor.avatar}" onerror="this.parentNode.textContent='${featAuthor.name.charAt(0).toUpperCase()}'"></div>`
        : `<div class="wf-store-featured-avatar">${featAuthor.name.charAt(0).toUpperCase()}</div>`) : '';
      const featAuthorHtml = featAuthor ? `<div class="wf-store-featured-author">${featAvatarHtml} by ${featAuthor.name}</div>` : '';
      const featIconHtml = wfBrandingIcon(wf, 20) ? `<div class="wf-store-featured-icon">${wfBrandingIcon(wf, 20)}</div>` : '';
      html += `<div class="wf-store-featured-card" style="background:${wf.gradient}" onclick="wfStoreShowDetail('${wf.name}')">
        ${featIconHtml}
        <div class="wf-store-featured-dl">↓ ${(wf.downloads||0).toLocaleString()}</div>
        <div class="wf-store-featured-content"><h3>${wf.name}</h3><p>${wf.description||''}</p>${featAuthorHtml}</div>
      </div>`;
    });
    html += '</div>';
  }

  // Grid label
  const label = _wfStoreCat === 'all' ? 'All Workflows' : (WF_STORE_CATEGORIES.find(c=>c.id===_wfStoreCat)?.label || _wfStoreCat);
  html += `<div class="wf-store-section-label">${label} <span class="wf-store-count">${filtered.length}</span></div>`;

  if (filtered.length === 0) {
    html += '<div class="wf-store-empty"><p>No workflows match your search</p></div>';
  } else {
    html += '<div class="wf-store-grid">';
    filtered.forEach(wf => {
      const dots = (wf.tools||[]).slice(0,5).map(t =>
        `<div class="wf-store-card-dot" style="background:${WF_STORE_TOOL_COLORS[t]||'#666'}"></div>`
      ).join('') + ((wf.tools||[]).length > 5 ? `<span style="font-family:var(--mono);font-size:9px;color:var(--text-muted);margin-left:2px">+${wf.tools.length-5}</span>` : '');
      const badges = (wf.verified ? '<span class="wf-store-badge-verified">✓ VERIFIED</span>' : '') +
        (wf.installed ? '<span class="wf-store-badge-installed">installed</span>' : '') +
        `<span class="wf-store-badge-official">${wf.tier==='official' ? '✓ OFFICIAL' : 'COMMUNITY'}</span>`;
      const capBadges = getCapabilityBadges(wf.tools);
      const capBadgesHtml = capBadges.length > 0
        ? `<div class="wf-store-cap-badges">${capBadges.map(b => `<span class="wf-store-badge-capability ${b.cls}">${b.emoji} ${b.label}</span>`).join('')}</div>`
        : '';
      const cardIconColor = (wf.branding && wf.branding.color) || '#64748B';
      const cardIconSvg = wfBrandingIcon(wf, 16);
      const cardIconHtml = cardIconSvg
        ? `<div class="wf-store-card-icon" style="background:${cardIconColor}18;color:${cardIconColor}">${cardIconSvg}</div>`
        : '';
      html += `<div class="wf-store-card" onclick="wfStoreShowDetail('${wf.name}')">
        <div class="wf-store-card-bar" style="background:${wf.gradient}"></div>
        <div class="wf-store-card-top">
          <div class="wf-store-card-name-wrap">${cardIconHtml}<span class="wf-store-card-name">${wf.name}</span></div>
          <div class="wf-store-card-badges">${badges}</div>
        </div>
        <div class="wf-store-card-desc">${wf.description||''}</div>
        ${wf.author && wf.author.name && wf.author.name !== 'unknown' ? `<div class="wf-store-card-author">by ${wf.author.name}</div>` : ''}
        ${capBadgesHtml}
        <div class="wf-store-card-meta">
          <span class="wf-store-card-cat">${wf.category||'utility'}</span>
          <div class="wf-store-card-dots">${dots}</div>
          <span class="wf-store-card-complexity">${wf.steps||0}s·${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}t</span>
          <span class="wf-store-card-downloads">↓ ${(wf.downloads||0).toLocaleString()}</span>
        </div>
      </div>`;
    });
    html += '</div>';
  }

  container.innerHTML = html;
}

function wfStoreShowDetail(name) {
  const wf = (_wfStoreCatalog || []).find(w => w.name === name);
  if (!wf) return;
  // Remove existing detail
  const existing = document.getElementById('wfStoreDetail');
  if (existing) existing.remove();

  const ic = `vai workflow install ${wf.packageName}`;
  const rc = `vai workflow run ${wf.packageName}`;

  // Escape for use in HTML attributes (single-quoted onclick handlers)
  const icEsc = ic.replace(/'/g, "\\'");
  const rcEsc = rc.replace(/'/g, "\\'");

  const toolsHtml = (wf.tools||[]).map(t => {
    const c = WF_STORE_TOOL_COLORS[t] || '#666';
    return `<span class="wf-store-detail-tool" style="color:${c};border-color:${c}44;background:${c}11">${t}</span>`;
  }).join('');

  const tagsHtml = `<span class="wf-store-detail-tag">${wf.category||'utility'}</span>` +
    (wf.tags||[]).map(t => `<span class="wf-store-detail-tag">${t}</span>`).join('');

  const installBtn = wf.installed
    ? '<button class="wf-store-detail-btn wf-store-detail-btn-installed">✓ Installed</button>'
    : `<button class="wf-store-detail-btn wf-store-detail-btn-primary" onclick="wfStoreInstall('${wf.packageName}',this)">Install</button>`;

  const modal = document.createElement('div');
  modal.id = 'wfStoreDetail';
  modal.className = 'wf-store-detail-bg';
  modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
  modal.innerHTML = `<div class="wf-store-detail-panel" onclick="event.stopPropagation()">
    <div class="wf-store-detail-scroll">
    <div class="wf-store-detail-hero" style="background:${wf.gradient}">
      <button class="wf-store-detail-close" onclick="document.getElementById('wfStoreDetail').remove()">×</button>
      <div class="wf-store-detail-hero-inner">
        ${wfBrandingIcon(wf, 26) ? `<div class="wf-store-detail-hero-icon">${wfBrandingIcon(wf, 26)}</div>` : ''}
        <div>
          <div class="wf-store-detail-name">${wf.name}</div>
          <div class="wf-store-detail-pkg">${wf.packageName}</div>
        </div>
      </div>
    </div>
    <div class="wf-store-detail-body">
      ${(() => {
        const a = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
        if (!a) return '';
        const avatarInner = a.avatar
          ? `<img src="${a.avatar}" onerror="this.parentNode.textContent='${a.name.charAt(0).toUpperCase()}'">` 
          : a.name.charAt(0).toUpperCase();
        const nameHtml = a.url
          ? `<a href="${a.url}" target="_blank" rel="noopener">${a.name}</a>`
          : a.name;
        return `<div class="wf-store-detail-author"><div class="wf-store-detail-avatar">${avatarInner}</div><div class="wf-store-detail-author-name">${nameHtml}</div></div>`;
      })()}
      <p class="wf-store-detail-desc">${wf.description||''}</p>
      ${wf.assets && wf.assets.screenshots && wf.assets.screenshots.length > 0 ? `
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Screenshots</div>
        <div class="wf-store-detail-screenshots">${wf.assets.screenshots.map(s => `<img src="${s}" alt="Screenshot">`).join('')}</div>
      </div>` : ''}
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Stats</div>
        <div class="wf-store-detail-stats">
          <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.steps||0}</div><div class="wf-store-detail-stat-lbl">Steps</div></div>
          <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}</div><div class="wf-store-detail-stat-lbl">Tools</div></div>
          <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${(wf.downloads||0).toLocaleString()}</div><div class="wf-store-detail-stat-lbl">Downloads</div></div>
        </div>
      </div>
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Tools</div>
        <div class="wf-store-detail-tools">${toolsHtml}</div>
      </div>
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Tags</div>
        <div class="wf-store-detail-tags">${tagsHtml}</div>
      </div>
      ${wf.inputs && wf.inputs.length > 0 ? `
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Input Parameters</div>
        <table class="wf-store-detail-inputs-table">
          <thead><tr><th>Name</th><th>Type</th><th>Required / Default</th></tr></thead>
          <tbody>${wf.inputs.map(inp => `<tr><td>${inp.name}</td><td>${inp.type}</td><td>${inp.required ? 'required' : inp.default !== undefined ? 'default: ' + inp.default : 'optional'}</td></tr>`).join('')}</tbody>
        </table>
      </div>` : ''}
      <div class="wf-store-detail-install">
        <div class="wf-store-detail-label">Install</div>
        <div class="wf-store-detail-cmd" onclick="navigator.clipboard.writeText('${icEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
          <span>$ ${ic}</span><span class="wf-store-detail-cmd-hint">copy</span>
        </div>
        <div class="wf-store-detail-label" style="margin-top:12px">Run</div>
        <div class="wf-store-detail-cmd" style="color:var(--text-dim)" onclick="navigator.clipboard.writeText('${rcEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
          <span>$ ${rc}</span><span class="wf-store-detail-cmd-hint">copy</span>
        </div>
      </div>
      ${(() => {
        const capBadges = getCapabilityBadges(wf.tools);
        if (capBadges.length === 0) return '';
        return `<div class="wf-store-detail-section">
          <div class="wf-store-detail-label">Capabilities</div>
          <div style="display:flex;gap:5px;flex-wrap:wrap">${capBadges.map(b => `<span class="wf-store-badge-capability ${b.cls}">${b.emoji} ${b.label}</span>`).join('')}</div>
        </div>`;
      })()}
      ${(() => {
        const sec = wf.security || [];
        if (sec.length === 0) return `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Security</div><div class="wf-store-detail-security-ok">✓ No security issues found</div></div>`;
        return `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Security</div>${sec.map(f => `<div class="wf-store-detail-security-item"><span class="wf-store-detail-security-sev ${f.severity||'low'}">${f.severity||'low'}</span><span>${f.message||f.rule||'Issue found'}</span></div>`).join('')}</div>`;
      })()}
      ${wf.verified ? `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Verified</div><div class="wf-store-detail-security-ok">✓ This workflow has passed all validation checks (L1–L4): schema, security audit, quality audit, and capability analysis.</div></div>` : ''}
      <div class="wf-store-detail-section">
        <div class="wf-store-detail-label">Rating</div>
        <div style="display:flex;align-items:center;gap:12px">
          <span style="font-size:12px;color:var(--text-dim)">${wf.rating != null ? wf.rating.toFixed(1) + ' / 5' : 'Not yet rated'}</span>
        </div>
        <div style="margin-top:8px;font-size:11px;color:var(--text-muted)">Rate this workflow:</div>
        <div class="wf-store-detail-stars" id="wfStoreRateStars">
          ${[1,2,3,4,5].map(i => `<span onclick="_wfStoreUserRatings['${wf.name}']=${i};document.querySelectorAll('#wfStoreRateStars span').forEach((s,j)=>s.className=j<${i}?'filled':'')" class="${(_wfStoreUserRatings[wf.name]||0)>=i?'filled':''}">★</span>`).join('')}
        </div>
      </div>
      <div class="wf-store-detail-section">
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" style="flex:none;width:auto;padding:6px 14px;font-size:11px" onclick="document.getElementById('wfStoreReportForm').style.display=document.getElementById('wfStoreReportForm').style.display==='none'?'block':'none'">⚑ Report</button>
        <div id="wfStoreReportForm" class="wf-store-report-form" style="display:none">
          <select id="wfStoreReportReason"><option value="">Select reason...</option><option value="malicious">Malicious</option><option value="broken">Broken</option><option value="low quality">Low quality</option><option value="spam">Spam</option></select>
          <textarea id="wfStoreReportComment" placeholder="Optional comment..."></textarea>
          <button class="wf-store-report-submit" onclick="document.getElementById('wfStoreReportForm').style.display='none';const t=document.createElement('div');t.style.cssText='position:fixed;bottom:20px;right:20px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 20px;color:var(--text);font-size:13px;z-index:9999;animation:wfStoreDetailFadeIn .2s';t.textContent='✓ Report submitted';document.body.appendChild(t);setTimeout(()=>t.remove(),3000)">Submit Report</button>
        </div>
      </div>
      <div class="wf-store-detail-actions">
        ${installBtn}
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreRun('${wf.name}')">Run</button>
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreCanvas('${wf.name}')">Canvas</button>
      </div>
    </div>
    </div>
  </div>`;
  document.body.appendChild(modal);
}

// ── Workflow Info Modal ──
function wfCloseInfoModal() {
  const bd = document.getElementById('wfInfoModalBackdrop');
  if (bd) bd.style.display = 'none';
}
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    const bd = document.getElementById('wfInfoModalBackdrop');
    if (bd && bd.style.display !== 'none') wfCloseInfoModal();
  }
});

async function wfShowWorkflowInfo(name) {
  // Resolve name: from arg, or from active workflow
  if (!name) name = wfState.activeWorkflowName;
  if (!name) return;

  // Normalize: strip package prefix for matching
  const shortName = name.replace(/^@vaicli\/vai-workflow-/, '').replace(/^vai-workflow-/, '');

  // Ensure catalog is loaded
  if (!_wfStoreCatalog) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      const res = await fetch('/api/workflows/catalog', {signal: controller.signal});
      clearTimeout(timeout);
      const data = await res.json();
      if (data.icons) _wfStoreIcons = data.icons;
      _wfStoreCatalog = (data.workflows && data.workflows.length > 0) ? data.workflows : WF_STORE_FALLBACK;
    } catch {
      _wfStoreCatalog = WF_STORE_FALLBACK;
    }
  }

  // Look up in catalog
  const wf = (_wfStoreCatalog || []).find(w =>
    w.name === shortName || w.name === name || w.packageName === name
  );

  const backdrop = document.getElementById('wfInfoModalBackdrop');
  const content = document.getElementById('wfInfoModalContent');
  if (!backdrop || !content) return;

  if (wf) {
    // Rich info from catalog
    const toolsHtml = (wf.tools || []).map(t => {
      const c = WF_STORE_TOOL_COLORS[t] || '#666';
      return `<span class="wf-info-modal-tool" style="color:${c};border-color:${c}44;background:${c}11">${t}</span>`;
    }).join('');
    const tagsHtml = [`<span class="wf-info-modal-tag">${wf.category || 'utility'}</span>`]
      .concat((wf.tags || []).map(t => `<span class="wf-info-modal-tag">${t}</span>`)).join('');
    const capBadges = getCapabilityBadges(wf.tools);
    const capHtml = capBadges.length > 0
      ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Capabilities</div><div style="display:flex;gap:5px;flex-wrap:wrap">${capBadges.map(b => `<span class="wf-store-badge-capability ${b.cls}">${b.emoji} ${b.label}</span>`).join('')}</div></div>`
      : '';
    const authorObj = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
    const authorHtml = authorObj
      ? `<div class="wf-info-modal-section" style="display:flex;align-items:center;gap:8px;">
          ${authorObj.avatar ? `<img src="${authorObj.avatar}" style="width:24px;height:24px;border-radius:50%;" onerror="this.style.display='none'">` : ''}
          <span style="font-size:12px;color:var(--text-dim);">by ${authorObj.url ? `<a href="${authorObj.url}" target="_blank" rel="noopener" style="color:var(--accent)">${authorObj.name}</a>` : authorObj.name}</span>
        </div>`
      : '';
    const verifiedHtml = wf.verified
      ? `<div class="wf-info-modal-section"><span style="color:var(--accent);font-size:12px;">&#10003; Verified</span></div>`
      : '';
    const ratingHtml = wf.rating != null
      ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Rating</div><span style="font-size:12px;color:var(--text-dim);">${wf.rating.toFixed(1)} / 5</span></div>`
      : '';

    content.innerHTML = `
      <div class="wf-info-modal-hero" style="background:${wf.gradient || 'linear-gradient(135deg, #334155, #64748B)'}">
        <button class="wf-info-modal-close" onclick="wfCloseInfoModal()">&times;</button>
        ${wfBrandingIcon(wf, 26) ? `<div class="wf-info-modal-hero-icon">${wfBrandingIcon(wf, 26)}</div>` : ''}
        <div>
          <div class="wf-info-modal-hero-name">${wf.name}</div>
          <div class="wf-info-modal-hero-pkg">${wf.packageName || ''}</div>
        </div>
      </div>
      <div class="wf-info-modal-body">
        ${authorHtml}
        <p style="font-size:13px;color:var(--text);margin:0 0 14px;line-height:1.5;">${wf.description || ''}</p>
        <div class="wf-info-modal-section">
          <div class="wf-info-modal-label">Stats</div>
          <div class="wf-info-modal-stats">
            <div class="wf-info-modal-stat"><div class="wf-info-modal-stat-val">${wf.steps || 0}</div><div class="wf-info-modal-stat-lbl">Steps</div></div>
            <div class="wf-info-modal-stat"><div class="wf-info-modal-stat-val">${wf.toolCount || (Array.isArray(wf.tools) ? wf.tools.length : 0)}</div><div class="wf-info-modal-stat-lbl">Tools</div></div>
            <div class="wf-info-modal-stat"><div class="wf-info-modal-stat-val">${(wf.downloads || 0).toLocaleString()}</div><div class="wf-info-modal-stat-lbl">Downloads</div></div>
          </div>
        </div>
        ${(wf.tools || []).length ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Tools</div><div class="wf-info-modal-tools">${toolsHtml}</div></div>` : ''}
        ${(wf.tags || []).length || wf.category ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Tags</div><div class="wf-info-modal-tags">${tagsHtml}</div></div>` : ''}
        ${capHtml}
        ${verifiedHtml}
        ${ratingHtml}
      </div>
      <div class="wf-info-modal-actions">
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" style="font-size:12px;padding:6px 14px;" onclick="wfCloseInfoModal();wfStoreOpen();setTimeout(()=>wfStoreShowDetail('${wf.name}'),300)">View in Store</button>
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" style="font-size:12px;padding:6px 14px;" onclick="wfCloseInfoModal()">Close</button>
      </div>`;
  } else {
    // Simple info for built-in/example workflows
    const def = wfState.activeWorkflow;
    const stepCount = def && def.steps ? def.steps.length : 0;
    const allWfs = [...(wfState.workflows || []), ...(wfState.examples || []), ...(wfState.official || []), ...(wfState.community || [])];
    const meta = allWfs.find(w => w.name === name);
    content.innerHTML = `
      <div class="wf-info-modal-hero" style="background:linear-gradient(135deg, #334155, #64748B)">
        <button class="wf-info-modal-close" onclick="wfCloseInfoModal()">&times;</button>
        <div>
          <div class="wf-info-modal-hero-name">${shortName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</div>
          <div class="wf-info-modal-hero-pkg" style="opacity:0.6">${name}</div>
        </div>
      </div>
      <div class="wf-info-modal-body">
        ${meta && meta.author ? `<div style="font-size:12px;color:var(--text-dim);margin-bottom:8px;">by ${meta.author}${meta.version ? ' &middot; v' + meta.version : ''}</div>` : ''}
        <p style="font-size:13px;color:var(--text);margin:0 0 14px;line-height:1.5;">${meta ? meta.description || '' : ''}</p>
        ${stepCount > 0 ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Steps</div><span style="font-size:13px;color:var(--text)">${stepCount}</span></div>` : ''}
        ${meta && (meta.tags || []).length ? `<div class="wf-info-modal-section"><div class="wf-info-modal-label">Tags</div><div class="wf-info-modal-tags">${meta.tags.map(t => `<span class="wf-info-modal-tag">${t}</span>`).join('')}</div></div>` : ''}
        <div class="wf-info-modal-section"><span class="wf-info-modal-tag" style="background:var(--accent);color:#000;font-weight:600;">Built-in</span></div>
      </div>
      <div class="wf-info-modal-actions">
        <button class="wf-store-detail-btn wf-store-detail-btn-secondary" style="font-size:12px;padding:6px 14px;" onclick="wfCloseInfoModal()">Close</button>
      </div>`;
  }

  backdrop.style.display = 'flex';
}

async function wfStoreInstall(packageName, btn) {
  if (btn) { btn.textContent = 'Installing...'; btn.disabled = true; }
  try {
    const res = await fetch('/api/workflows/community/install', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: packageName })
    });
    const text = await res.text();
    let data;
    try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response (status ' + res.status + ')' }; }
    console.log('[Store Install]', packageName, data);
    if (data.success) {
      if (btn) { btn.textContent = '✓ Installed'; btn.className = 'wf-store-detail-btn wf-store-detail-btn-installed'; btn.disabled = true; }
      // Update catalog state
      const wf = (_wfStoreCatalog||[]).find(w => w.packageName === packageName);
      if (wf) wf.installed = true;
      wfStoreRender();
      await wfLoadLibrary(); // Refresh library panel
    } else {
      const errMsg = data.error || 'Unknown error';
      console.error('[Store Install Error]', errMsg);
      if (btn) { btn.textContent = 'Failed'; btn.title = errMsg; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
    }
  } catch (err) {
    console.error('[Store Install Exception]', err);
    if (btn) { btn.textContent = 'Failed'; btn.title = err.message; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
  }
}

async function wfStoreRun(name) {
  const detail = document.getElementById('wfStoreDetail');
  if (detail) detail.remove();
  wfStoreClose();

  // Find the workflow in catalog
  const wf = (_wfStoreCatalog||[]).find(w => w.name === name);
  if (!wf) {
    alert('Workflow not found: ' + name);
    return;
  }

  // Check if installed
  if (!wf.installed) {
    const doInstall = confirm(`"${wf.name}" is not installed. Install it now?`);
    if (doInstall) {
      await wfStoreInstall(wf.packageName, null);
      // Refresh catalog to update installed status
      await wfStoreFetchCatalog();
      // Check again
      const updated = (_wfStoreCatalog||[]).find(w => w.name === name);
      if (!updated || !updated.installed) {
        alert('Installation may have failed. Please try again.');
        return;
      }
    } else {
      return;
    }
  }

  // Switch to workflows tab and load the workflow
  switchTab('workflows');
  // Use the full package name for resolution
  wfSelectWorkflow(wf.packageName);
}

function wfStoreCanvas(name) {
  wfStoreRun(name); // Same behavior — load the workflow into canvas
}

// Escape key handler for store
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    const detail = document.getElementById('wfStoreDetail');
    if (detail) { detail.remove(); return; }
    const overlay = document.getElementById('wfStoreOverlay');
    if (overlay && overlay.classList.contains('open')) { wfStoreClose(); }
  }
});

function wfInit() {
  wfRestoreLayoutState();
  wfLoadLibrary();
  wfInitPan();
}

// Auto-init: use MutationObserver on the panel to detect when it becomes visible
let wfInitialized = false;
(function() {
  const panel = document.getElementById('tab-workflows');
  if (!panel) return;
  const observer = new MutationObserver(() => {
    if (panel.classList.contains('active') && !wfInitialized) {
      wfInitialized = true;
      wfInit();
    }
  });
  observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
  // Also init immediately if the tab is already active (shouldn't be, but safety)
  if (panel.classList.contains('active')) {
    wfInitialized = true;
    wfInit();
  }
})();
</script>

<script>
(function() {
  const BUG_API = 'https://vaicli.com/api/bugs';
  const GITHUB_URL = 'https://github.com/mrlynn/voyageai-cli/issues/new';
  
  function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  async function getEnv() {
    const isElectron = !!(window.vai && window.vai.isElectron);
    const env = {
      platform: navigator.platform,
      source: isElectron ? 'desktop-app' : 'playground',
      currentScreen: getActiveTabLabel(),
      currentUrl: window.location.href,
      sessionId: window._chatExportData?.sessionId || null,
    };

    if (window.vai && window.vai.environment) {
      env.arch = window.vai.environment.arch || null;
      env.electronVersion = window.vai.environment.electronVersion || null;
      env.nodeVersion = window.vai.environment.nodeVersion || null;
    }

    if (isElectron && window.vai && window.vai.getVersion) {
      try {
        const version = await window.vai.getVersion();
        env.appVersion = version?.app || null;
        env.cliVersion = version?.cli || null;
      } catch {}
    }

    return env;
  }
  
  async function showBugReporter() {
    const env = await getEnv();
    document.getElementById('bugEnvInfo').textContent = 
      `${env.platform} • App v${env.appVersion || '?'} • ${env.currentScreen}`;
    document.getElementById('bugOverlay').style.display = 'flex';
    document.getElementById('bugForm').style.display = 'block';
    document.getElementById('bugSuccess').style.display = 'none';
    document.getElementById('bugError').style.display = 'none';
    document.getElementById('bugTitle').value = '';
    document.getElementById('bugDescription').value = '';
    document.getElementById('bugSteps').value = '';
    document.getElementById('bugEmail').value = '';
  }
  
  function hideBugReporter() {
    document.getElementById('bugOverlay').style.display = 'none';
  }
  
  async function submitBug() {
    const title = document.getElementById('bugTitle').value.trim();
    const description = document.getElementById('bugDescription').value.trim();
    const steps = document.getElementById('bugSteps').value.trim();
    const email = document.getElementById('bugEmail').value.trim();
    
    if (!title || title.length < 5) {
      document.getElementById('bugError').textContent = 'Title is required (min 5 characters)';
      document.getElementById('bugError').style.display = 'block';
      return;
    }
    if (!description || description.length < 10) {
      document.getElementById('bugError').textContent = 'Description is required (min 10 characters)';
      document.getElementById('bugError').style.display = 'block';
      return;
    }
    if (email && !isValidEmail(email)) {
      document.getElementById('bugError').textContent = 'Please provide a valid email address';
      document.getElementById('bugError').style.display = 'block';
      return;
    }
    
    document.getElementById('bugSubmit').disabled = true;
    document.getElementById('bugSubmit').textContent = 'Submitting...';
    document.getElementById('bugError').style.display = 'none';
    
    try {
      const env = await getEnv();
      const res = await fetch(BUG_API, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title, description, stepsToReproduce: steps || null, email: email || null,
          ...env
        })
      });
      
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || `HTTP ${res.status}`);
      }
      
      const data = await res.json();
      document.getElementById('bugResultId').textContent = data.bugId;
      document.getElementById('bugForm').style.display = 'none';
      document.getElementById('bugSuccess').style.display = 'block';
    } catch (err) {
      document.getElementById('bugError').textContent = err.message;
      document.getElementById('bugError').style.display = 'block';
    } finally {
      document.getElementById('bugSubmit').disabled = false;
      document.getElementById('bugSubmit').textContent = 'Submit Bug Report';
    }
  }
  
  function openGithub() {
    const title = document.getElementById('bugTitle').value.trim() || 'Bug Report';
    const description = document.getElementById('bugDescription').value.trim();
    const steps = document.getElementById('bugSteps').value.trim();
    const env = getEnv();
    
    const body = `## Description\n${description}\n\n## Steps to Reproduce\n${steps || '1. \\n2. \\n3. '}\n\n## Environment\n- **App Version:** ${env.appVersion || 'N/A'}\n- **Platform:** ${env.platform}\n- **Screen:** ${env.currentScreen}`;
    const url = `${GITHUB_URL}?title=${encodeURIComponent('[Bug] ' + title)}&body=${encodeURIComponent(body)}&labels=bug`;
    window.open(url, '_blank');
  }
  
  // Event listeners
  document.getElementById('bugButton').addEventListener('click', showBugReporter);
  document.getElementById('bugClose').addEventListener('click', hideBugReporter);
  document.getElementById('bugOverlay').addEventListener('click', (e) => {
    if (e.target.id === 'bugOverlay') hideBugReporter();
  });
  document.getElementById('bugSubmit').addEventListener('click', submitBug);
  document.getElementById('bugGithub').addEventListener('click', openGithub);
  document.getElementById('bugSuccessClose').addEventListener('click', hideBugReporter);
})();
</script>

<!-- Cost Dashboard -->
<div id="costDetailPanel">
  <div class="cost-panel-inner">
    <div class="cost-panel-header">
      <h3>💰 Cost Dashboard</h3>
      <button id="costResetBtn" onclick="CostTracker.reset()">Reset Session</button>
    </div>
    <table id="costOpsTable">
      <thead><tr><th>Time</th><th>Operation</th><th>Model</th><th>Tokens</th><th>Cost</th><th>If v4-large</th><th>If OpenAI</th></tr></thead>
      <tbody></tbody>
    </table>
    <div class="cost-projection" id="costProjection"></div>
  </div>
</div>
<div id="costStatusBar">
  <span>💰 Session: <span class="cost-val" id="costTotal">$0.000000</span></span>
  <span class="cost-sep">│</span>
  <span><span id="costTokens">0</span> tokens</span>
  <span class="cost-sep">│</span>
  <span><span id="costOps">0</span> ops</span>
  <span class="cost-sep">│</span>
  <span>LLM: <span class="cost-val" id="costLLM">$0.000000</span> (<span id="costLLMIn">0</span>in/<span id="costLLMOut">0</span>out)</span>
  <span class="cost-sep">│</span>
  <span>If symmetric: <span id="costSymmetric">$0.000000</span> <span id="costSymDelta"></span></span>
  <span class="cost-sep">│</span>
  <span>If OpenAI: <span id="costCompetitor">$0.000000</span> <span id="costCompDelta"></span></span>
  <button id="costDetailsToggle" onclick="CostTracker.togglePanel()">Details ▾</button>
</div>

<script>
(function() {
  const PRICING = {
    'voyage-4-large': 0.12, 'voyage-4': 0.06, 'voyage-4-lite': 0.02,
    'voyage-code-3': 0.18, 'voyage-finance-2': 0.12, 'voyage-law-2': 0.12,
    'rerank-2.5': 0.05, 'rerank-2.5-lite': 0.02,
    'voyage-multimodal-3': 0.12,
  };
  const LARGE_PRICE = 0.12;
  const OPENAI_PRICE = 0.13;

  // LLM pricing: per-million tokens { input, output }
  const LLM_PRICING = {
    'claude-sonnet-4-5-20250929': { input: 3.00, output: 15.00 },
    'claude-opus-4-20250514': { input: 15.00, output: 75.00 },
    'claude-3-5-haiku-20241022': { input: 0.80, output: 4.00 },
    'claude-sonnet-4': { input: 3.00, output: 15.00 },
    'claude-opus-4': { input: 15.00, output: 75.00 },
    'gpt-4o': { input: 2.50, output: 10.00 },
    'gpt-4o-mini': { input: 0.15, output: 0.60 },
    'gpt-4-turbo': { input: 10.00, output: 30.00 },
    'o1': { input: 15.00, output: 60.00 },
    'o1-mini': { input: 3.00, output: 12.00 },
    'o3-mini': { input: 1.10, output: 4.40 },
  };

  function getLLMPrice(model) {
    if (!model) return { input: 0, output: 0 };
    if (LLM_PRICING[model]) return LLM_PRICING[model];
    // Fuzzy match
    for (const k of Object.keys(LLM_PRICING)) {
      if (model.includes(k) || k.includes(model)) return LLM_PRICING[k];
    }
    // Match by prefix (e.g. "claude-sonnet-4-5" matches the full versioned key)
    for (const k of Object.keys(LLM_PRICING)) {
      const base = k.replace(/-\d{8}$/, '');
      if (model.includes(base) || base.includes(model)) return LLM_PRICING[k];
    }
    return { input: 0, output: 0 }; // Unknown model: free (don't inflate costs)
  }

  function llmCostForTokens(model, inputTokens, outputTokens) {
    const prices = getLLMPrice(model);
    return (prices.input / 1_000_000) * inputTokens + (prices.output / 1_000_000) * outputTokens;
  }

  function getPrice(model) {
    if (PRICING[model] != null) return PRICING[model];
    // fuzzy match
    for (const k of Object.keys(PRICING)) {
      if (model.includes(k) || k.includes(model)) return PRICING[k];
    }
    return 0.06; // default fallback
  }

  function costForTokens(pricePerMillion, tokens) {
    return (pricePerMillion / 1_000_000) * tokens;
  }

  const session = {
    operations: [], totalCost: 0, totalTokens: 0, totalOperations: 0,
    symmetricCost: 0, competitorCost: 0,
    llmCost: 0, llmInputTokens: 0, llmOutputTokens: 0,
  };

  window.CostTracker = {
    addOperation(op, model, tokens) {
      if (!tokens || tokens <= 0) return;
      const price = getPrice(model);
      const cost = costForTokens(price, tokens);
      const largeCost = costForTokens(LARGE_PRICE, tokens);
      const competitorCost = costForTokens(OPENAI_PRICE, tokens);

      session.operations.push({
        timestamp: new Date(), operation: op, model, tokens, cost,
        hypotheticalLargeCost: largeCost, hypotheticalCompetitorCost: competitorCost,
      });
      session.totalCost += cost;
      session.totalTokens += tokens;
      session.totalOperations++;
      session.symmetricCost += largeCost;
      session.competitorCost += competitorCost;

      this._updateUI();
      this._showToast(op, model, tokens, cost, largeCost);
    },

    addLLMOperation(op, model, inputTokens, outputTokens) {
      if ((!inputTokens || inputTokens <= 0) && (!outputTokens || outputTokens <= 0)) return;
      const cost = llmCostForTokens(model, inputTokens || 0, outputTokens || 0);
      const totalTokens = (inputTokens || 0) + (outputTokens || 0);

      session.operations.push({
        timestamp: new Date(), operation: op, model: model || 'unknown',
        tokens: totalTokens, cost, isLLM: true,
        inputTokens: inputTokens || 0, outputTokens: outputTokens || 0,
        hypotheticalLargeCost: 0, hypotheticalCompetitorCost: 0,
      });
      session.totalCost += cost;
      session.totalTokens += totalTokens;
      session.totalOperations++;
      session.llmCost += cost;
      session.llmInputTokens += (inputTokens || 0);
      session.llmOutputTokens += (outputTokens || 0);

      this._updateUI();
      this._showLLMToast(op, model, inputTokens || 0, outputTokens || 0, cost);
    },

    reset() {
      session.operations.length = 0;
      session.totalCost = 0; session.totalTokens = 0; session.totalOperations = 0;
      session.symmetricCost = 0; session.competitorCost = 0;
      session.llmCost = 0; session.llmInputTokens = 0; session.llmOutputTokens = 0;
      this._updateUI();
    },

    getProjectedMonthlyCost(queriesPerMonth) {
      if (session.totalOperations === 0) return { actual: 0, symmetric: 0, competitor: 0 };
      const avgCost = session.totalCost / session.totalOperations;
      const avgSym = session.symmetricCost / session.totalOperations;
      const avgComp = session.competitorCost / session.totalOperations;
      return { actual: avgCost * queriesPerMonth, symmetric: avgSym * queriesPerMonth, competitor: avgComp * queriesPerMonth };
    },

    togglePanel() {
      const panel = document.getElementById('costDetailPanel');
      const btn = document.getElementById('costDetailsToggle');
      const isOpen = panel.classList.toggle('open');
      btn.textContent = isOpen ? 'Details ▴' : 'Details ▾';
      if (isOpen && typeof sendTelemetry === 'function') sendTelemetry('cost_panel_viewed');
    },

    _updateUI() {
      document.getElementById('costTotal').textContent = '$' + session.totalCost.toFixed(6);
      document.getElementById('costTokens').textContent = session.totalTokens.toLocaleString();
      document.getElementById('costOps').textContent = session.totalOperations;
      document.getElementById('costLLM').textContent = '$' + session.llmCost.toFixed(6);
      document.getElementById('costLLMIn').textContent = session.llmInputTokens.toLocaleString();
      document.getElementById('costLLMOut').textContent = session.llmOutputTokens.toLocaleString();
      document.getElementById('costSymmetric').textContent = '$' + session.symmetricCost.toFixed(6);
      document.getElementById('costCompetitor').textContent = '$' + session.competitorCost.toFixed(6);

      // Deltas
      const symDelta = document.getElementById('costSymDelta');
      const compDelta = document.getElementById('costCompDelta');
      if (session.totalCost > 0) {
        const symPct = ((session.symmetricCost - session.totalCost) / session.totalCost * 100).toFixed(0);
        const compPct = ((session.competitorCost - session.totalCost) / session.totalCost * 100).toFixed(0);
        symDelta.textContent = symPct > 0 ? `(+${symPct}%)` : `(${symPct}%)`;
        symDelta.className = symPct > 0 ? 'cost-more' : 'cost-savings';
        compDelta.textContent = compPct > 0 ? `(+${compPct}%)` : `(${compPct}%)`;
        compDelta.className = compPct > 0 ? 'cost-more' : 'cost-savings';
      } else {
        symDelta.textContent = ''; compDelta.textContent = '';
      }

      // Table
      const tbody = document.querySelector('#costOpsTable tbody');
      tbody.innerHTML = session.operations.map(o => {
        const tokensDisplay = o.isLLM
          ? `${o.inputTokens.toLocaleString()}in / ${o.outputTokens.toLocaleString()}out`
          : o.tokens.toLocaleString();
        const largeCostDisplay = o.isLLM ? 'n/a' : `$${o.hypotheticalLargeCost.toFixed(6)}`;
        const compCostDisplay = o.isLLM ? 'n/a' : `$${o.hypotheticalCompetitorCost.toFixed(6)}`;
        return `<tr${o.isLLM ? ' style="background:rgba(180,90,242,0.08)"' : ''}>
          <td>${o.timestamp.toLocaleTimeString()}</td>
          <td>${o.operation}</td>
          <td>${o.model}</td>
          <td>${tokensDisplay}</td>
          <td>$${o.cost.toFixed(6)}</td>
          <td>${largeCostDisplay}</td>
          <td>${compCostDisplay}</td>
        </tr>`;
      }).join('');

      // Projection
      const proj = this.getProjectedMonthlyCost(1_000_000);
      const projEl = document.getElementById('costProjection');
      if (session.totalOperations > 0) {
        projEl.innerHTML = `📊 Projected at 1M queries/month: <span>$${proj.actual.toFixed(2)}</span> actual │ $${proj.symmetric.toFixed(2)} symmetric │ $${proj.competitor.toFixed(2)} OpenAI`;
      } else {
        projEl.innerHTML = '';
      }
    },

    _showToast(op, model, tokens, cost, largeCost) {
      const saved = largeCost > 0 ? Math.round((1 - cost / largeCost) * 100) : 0;
      const savingsText = saved > 0 ? ` — saved ${saved}% vs v4-large` : '';
      const toast = document.createElement('div');
      toast.className = 'cost-toast';
      toast.textContent = `✅ ${op} — $${cost.toFixed(6)} (${model}, ${tokens.toLocaleString()} tokens)${savingsText}`;
      document.body.appendChild(toast);
      setTimeout(() => toast.remove(), 3100);
    },

    _showLLMToast(op, model, inputTokens, outputTokens, cost) {
      const toast = document.createElement('div');
      toast.className = 'cost-toast';
      toast.textContent = `🤖 ${op} — $${cost.toFixed(6)} (${model || 'LLM'}, ${inputTokens.toLocaleString()}in/${outputTokens.toLocaleString()}out)`;
      document.body.appendChild(toast);
      setTimeout(() => toast.remove(), 3100);
    }
  };
})();
</script>

<!-- ══════════════════════════════════════════════════════════════════
     Export Dialog Modal
     ══════════════════════════════════════════════════════════════════ -->
<div class="export-modal-overlay" id="exportOverlay" onclick="if(event.target===this)closeExportDialog()">
  <div class="export-modal">
    <button class="export-close" onclick="closeExportDialog()">&times;</button>
    <h2>Export</h2>
    <div class="export-subtitle" id="exportSubtitle">Workflow Canvas</div>
    <div style="font-size:13px;color:var(--text-muted);margin-bottom:8px;">Format</div>
    <div class="export-format-chips" id="exportFormatChips"></div>
    <div class="export-preview" id="exportPreview" style="display:none;">
      <div class="export-preview-tabs" id="exportPreviewTabs"></div>
      <div id="exportPreviewContent"></div>
    </div>
    <div class="export-actions">
      <button onclick="exportToClipboard()">Copy to Clipboard</button>
      <button class="export-primary" onclick="exportToFile()">Save File</button>
    </div>
  </div>
</div>

<!-- ══════════════════════════════════════════════════════════════════
     Command Palette Modal
     ══════════════════════════════════════════════════════════════════ -->
<div class="command-palette-overlay" id="commandPaletteOverlay" onclick="if(event.target===this)closeCommandPalette()">
  <div class="command-palette">
    <!-- Sub-palette header (hidden by default) -->
    <div class="command-palette-header" id="commandPaletteHeader" style="display: none;">
      <button class="command-palette-back" id="commandPaletteBack" onclick="goBackToMainPalette()">←</button>
      <div class="command-palette-title" id="commandPaletteTitle"></div>
    </div>
    
    <input 
      type="text" 
      class="command-palette-input" 
      id="commandPaletteInput"
      placeholder="Type a command or search..." 
      autocomplete="off"
      role="combobox"
      aria-expanded="true"
      aria-haspopup="listbox"
      aria-owns="commandPaletteResults"
      aria-label="Command palette search"
      aria-autocomplete="list"
    />
    
    <div class="command-palette-with-preview">
      <div class="command-palette-main">
        <div class="command-palette-results" id="commandPaletteResults" role="listbox" aria-label="Commands">
          <div class="command-palette-empty">
            Press ⌘K to open • Type to search • ↑↓ to navigate • ⏎ to execute
          </div>
        </div>
      </div>
      
      <!-- Preview pane (shown only for certain items on wide viewports) -->
      <div class="command-palette-preview" id="commandPalettePreview" style="display: none;">
        <div class="command-palette-preview-content" id="commandPalettePreviewContent">
        </div>
      </div>
    </div>
    
    <div class="command-palette-footer">
      <span><kbd class="command-palette-key">↑</kbd><kbd class="command-palette-key">↓</kbd> Navigate</span>
      <span><kbd class="command-palette-key">⏎</kbd> Select</span>
      <span><kbd class="command-palette-key">⎋</kbd> Close</span>
      <span><kbd class="command-palette-key">Tab</kbd> Filter</span>
    </div>
  </div>
</div>

<!-- ══════════════════════════════════════════════════════════════════
     Phase 3: Onboarding Tooltip (dynamically created in JavaScript)
     ══════════════════════════════════════════════════════════════════ -->

<script>
// ══════════════════════════════════════════════════════════════════
// Export System — Playground + Electron Integration
// ══════════════════════════════════════════════════════════════════

const EXPORT_FORMATS = {
  workflow:  ['json', 'mermaid', 'markdown', 'png', 'svg', 'clipboard'],
  search:   ['json', 'jsonl', 'csv', 'markdown', 'clipboard'],
  chat:     ['json', 'markdown', 'pdf', 'clipboard'],
  benchmark:['json', 'csv', 'markdown', 'png', 'svg', 'clipboard'],
  explore:  ['json', 'png', 'svg'],
};

const FORMAT_LABELS = {
  json: 'JSON', jsonl: 'JSONL', csv: 'CSV', markdown: 'Markdown',
  mermaid: 'Mermaid', svg: 'SVG', png: 'PNG', pdf: 'PDF', clipboard: 'Clipboard',
};

const FORMAT_EXTS = {
  json: '.json', jsonl: '.jsonl', csv: '.csv', markdown: '.md',
  mermaid: '.mmd', svg: '.svg', png: '.png', pdf: '.pdf',
};

const CONTEXT_LABELS = {
  workflow: 'Workflow Canvas', search: 'Search Results',
  chat: 'Chat Session', benchmark: 'Benchmark Results', explore: 'Explore Visualization',
};

let _exportContext = null;
let _exportFormat = 'json';

function openExportDialog(context) {
  _exportContext = context;
  _exportPreviewCache = {};
  _exportPreviewTab = 'rendered';
  const formats = EXPORT_FORMATS[context] || ['json'];
  _exportFormat = formats[0];

  document.getElementById('exportSubtitle').textContent = CONTEXT_LABELS[context] || context;

  // Build format chips
  const container = document.getElementById('exportFormatChips');
  container.innerHTML = formats.filter(f => f !== 'clipboard').map(f =>
    `<button class="export-format-chip${f === _exportFormat ? ' active' : ''}" data-fmt="${f}" onclick="selectExportFormat('${f}')">${FORMAT_LABELS[f]}</button>`
  ).join('');

  updateExportPreview();
  document.getElementById('exportOverlay').classList.add('open');
}

function closeExportDialog() {
  document.getElementById('exportOverlay').classList.remove('open');
  _exportContext = null;
}

function selectExportFormat(fmt) {
  _exportFormat = fmt;
  document.querySelectorAll('.export-format-chip').forEach(c => c.classList.toggle('active', c.dataset.fmt === fmt));
  updateExportPreview();
}

let _exportPreviewCache = {};
let _exportPreviewTab = 'rendered'; // 'source' or 'rendered'

function updateExportPreview() {
  const preview = document.getElementById('exportPreview');
  const tabs = document.getElementById('exportPreviewTabs');
  const content = document.getElementById('exportPreviewContent');
  const data = getExportData(_exportContext);

  if (!data) { preview.style.display = 'none'; return; }
  preview.style.display = 'block';

  // Determine which tabs to show
  const hasRendered = ['mermaid', 'markdown', 'png', 'svg', 'pdf'].includes(_exportFormat);
  const hasSource = !['png', 'pdf'].includes(_exportFormat);

  if (hasRendered && hasSource) {
    tabs.innerHTML = `
      <button class="export-preview-tab${_exportPreviewTab === 'rendered' ? ' active' : ''}" onclick="switchPreviewTab('rendered')">Preview</button>
      <button class="export-preview-tab${_exportPreviewTab === 'source' ? ' active' : ''}" onclick="switchPreviewTab('source')">Source</button>`;
    tabs.style.display = '';
  } else if (hasRendered) {
    _exportPreviewTab = 'rendered';
    tabs.innerHTML = `<button class="export-preview-tab active">Preview</button>`;
    tabs.style.display = '';
  } else {
    _exportPreviewTab = 'source';
    tabs.style.display = 'none';
  }

  // Fetch export content
  const cacheKey = `${_exportContext}:${_exportFormat}`;
  if (_exportPreviewCache[cacheKey]) {
    renderPreviewContent(content, _exportPreviewCache[cacheKey], _exportFormat, _exportPreviewTab);
    return;
  }

  content.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted);font-size:13px;">Generating preview…</div>';

  // Generate preview: use server API for binary formats, client-side for text formats
  if (_exportFormat === 'svg' || _exportFormat === 'png') {
    // SVG/PNG: render Mermaid client-side, no server needed
    generateVisualPreview(_exportContext, _exportFormat, data, cacheKey, content);
  } else if (_exportFormat === 'pdf') {
    // PDF: needs server-side Playwright
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 20000);
    fetch('/api/export/' + _exportContext, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ format: _exportFormat, data, options: {} }),
      signal: controller.signal,
    })
      .then(r => { clearTimeout(timeoutId); if (!r.ok) return r.text().then(t => { throw new Error(t); }); return r.blob(); })
      .then(blob => {
        const url = URL.createObjectURL(blob);
        _exportPreviewCache[cacheKey] = { blobUrl: url, mimeType: 'application/pdf' };
        renderPreviewContent(content, _exportPreviewCache[cacheKey], _exportFormat, _exportPreviewTab);
      })
      .catch(err => { content.innerHTML = `<div style="padding:16px;color:#f44;">PDF preview error: ${escapeHtmlExport(err.message || String(err))}</div>`; });
  } else {
    // Text formats: generate client-side (instant)
    try {
      const text = generatePreviewClientSide(_exportContext, _exportFormat, data);
      _exportPreviewCache[cacheKey] = { text };
      renderPreviewContent(content, _exportPreviewCache[cacheKey], _exportFormat, _exportPreviewTab);
    } catch (err) {
      content.innerHTML = `<div style="padding:16px;color:#f44;">Preview error: ${escapeHtmlExport(err.message || String(err))}</div>`;
    }
  }
}

function switchPreviewTab(tab) {
  _exportPreviewTab = tab;
  document.querySelectorAll('.export-preview-tab').forEach(t => t.classList.toggle('active', t.textContent.toLowerCase().includes(tab === 'rendered' ? 'preview' : 'source')));
  const content = document.getElementById('exportPreviewContent');
  const cacheKey = `${_exportContext}:${_exportFormat}`;
  const cached = _exportPreviewCache[cacheKey];
  if (cached) renderPreviewContent(content, cached, _exportFormat, tab);
}

function renderPreviewContent(el, cached, format, tab) {
  if (tab === 'source' && cached.text) {
    // Source view with syntax highlighting
    if (format === 'json' || format === 'jsonl') {
      el.innerHTML = `<div class="export-preview-source">${highlightJson(cached.text)}</div>`;
    } else if (format === 'mermaid') {
      el.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(cached.text)}</div>`;
    } else if (format === 'csv') {
      el.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(cached.text)}</div>`;
    } else if (format === 'svg') {
      el.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(cached.text)}</div>`;
    } else {
      el.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(cached.text)}</div>`;
    }
    return;
  }

  // Rendered view
  switch (format) {
    case 'json':
    case 'jsonl':
      el.innerHTML = `<div class="export-preview-source">${highlightJson(cached.text)}</div>`;
      break;

    case 'csv':
      el.innerHTML = `<div class="export-preview-rendered">${csvToHtmlTable(cached.text)}</div>`;
      break;

    case 'markdown':
      el.innerHTML = `<div class="export-preview-rendered">${renderMarkdownPreview(cached.text)}</div>`;
      break;

    case 'mermaid':
      el.innerHTML = `<div class="export-preview-mermaid" id="exportMermaidRender"></div>`;
      renderMermaidPreview(cached.text);
      break;

    case 'svg':
      // Render the actual SVG inline
      el.innerHTML = `<div class="export-preview-image">${cached.text}</div>`;
      // Constrain SVG size
      const svgEl = el.querySelector('svg');
      if (svgEl) { svgEl.style.maxWidth = '100%'; svgEl.style.maxHeight = '300px'; }
      break;

    case 'png':
      if (cached.blobUrl) {
        el.innerHTML = `<div class="export-preview-image"><img src="${cached.blobUrl}" alt="PNG Preview"></div>`;
      }
      break;

    case 'pdf':
      if (cached.blobUrl) {
        el.innerHTML = `<div style="padding:16px;text-align:center;color:var(--text-muted);">
          <div style="font-size:32px;margin-bottom:8px;">📄</div>
          PDF generated (${_exportContext} export)<br/>
          <a href="${cached.blobUrl}" target="_blank" style="color:var(--accent);">Open in new tab</a>
        </div>`;
      }
      break;

    default:
      el.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(cached.text || '')}</div>`;
  }
}

// ── Client-side visual preview (SVG/PNG via mermaid.js in browser) ──

async function generateVisualPreview(context, format, data, cacheKey, contentEl) {
  try {
    // Generate Mermaid source
    const mermaidSrc = generateMermaidPreview(data);

    // Ensure mermaid is loaded
    await loadMermaidLib();

    // Render to SVG
    const id = 'export-visual-' + Date.now();
    const { svg } = await mermaid.render(id, mermaidSrc);

    if (format === 'svg') {
      // Add watermark
      const svgWithWatermark = svg.replace('</svg>',
        '<text x="99%" y="99%" text-anchor="end" font-size="10" fill="#666" opacity="0.5">Generated by vai</text></svg>');
      _exportPreviewCache[cacheKey] = { text: svgWithWatermark, svgRaw: svg };
      renderPreviewContent(contentEl, _exportPreviewCache[cacheKey], format, _exportPreviewTab);
    } else if (format === 'png') {
      // Rasterize SVG to PNG via canvas
      const pngBlob = await svgToPngBlob(svg, { scale: 2 });
      const url = URL.createObjectURL(pngBlob);
      _exportPreviewCache[cacheKey] = { blobUrl: url, mimeType: 'image/png', pngBlob };
      renderPreviewContent(contentEl, _exportPreviewCache[cacheKey], format, _exportPreviewTab);
    }
  } catch (err) {
    contentEl.innerHTML = `<div style="padding:16px;color:#f44;">Render error: ${escapeHtmlExport(err.message || String(err))}</div>`;
  }
}

function loadMermaidLib() {
  return new Promise((resolve, reject) => {
    if (typeof mermaid !== 'undefined') { resolve(); return; }
    const timeout = setTimeout(() => reject(new Error('Mermaid load timed out')), 15000);
    const script = document.createElement('script');
    script.src = '/vendor/mermaid.min.js';
    script.onload = () => {
      clearTimeout(timeout);
      mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#00D4AA' } });
      resolve();
    };
    script.onerror = () => { clearTimeout(timeout); reject(new Error('Failed to load mermaid.js')); };
    document.head.appendChild(script);
  });
}

function svgToPngBlob(svgString, opts = {}) {
  return new Promise((resolve, reject) => {
    const scale = opts.scale || 2;
    const timeout = setTimeout(() => reject(new Error('PNG render timed out')), 10000);

    // Parse SVG to get dimensions
    const parser = new DOMParser();
    const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');
    const svgEl = svgDoc.documentElement;

    // Get width/height from SVG attributes or viewBox
    let width = parseFloat(svgEl.getAttribute('width')) || 800;
    let height = parseFloat(svgEl.getAttribute('height')) || 400;
    const viewBox = svgEl.getAttribute('viewBox');
    if (viewBox) {
      const parts = viewBox.split(/\s+/);
      if (parts.length === 4) {
        width = parseFloat(parts[2]) || width;
        height = parseFloat(parts[3]) || height;
      }
    }

    // Replace <foreignObject> elements with <text> equivalents.
    // Mermaid v11 uses foreignObject for node labels, which blocks
    // Image loading due to browser security restrictions on embedded HTML.
    svgEl.querySelectorAll('foreignObject').forEach(fo => {
      const foX = parseFloat(fo.getAttribute('x')) || 0;
      const foY = parseFloat(fo.getAttribute('y')) || 0;
      const foW = parseFloat(fo.getAttribute('width')) || 100;
      const foH = parseFloat(fo.getAttribute('height')) || 30;
      const textContent = (fo.textContent || '').trim();

      const textEl = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'text');
      textEl.setAttribute('x', foX + foW / 2);
      textEl.setAttribute('y', foY + foH / 2);
      textEl.setAttribute('text-anchor', 'middle');
      textEl.setAttribute('dominant-baseline', 'central');
      textEl.setAttribute('font-size', '14');
      textEl.setAttribute('fill', '#fff');
      textEl.textContent = textContent;
      fo.parentNode.replaceChild(textEl, fo);
    });

    // Ensure the SVG has explicit dimensions for the image
    svgEl.setAttribute('width', width);
    svgEl.setAttribute('height', height);
    const serializer = new XMLSerializer();
    const svgData = serializer.serializeToString(svgEl);

    const img = new Image();
    const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
    const url = URL.createObjectURL(svgBlob);

    img.onload = () => {
      clearTimeout(timeout);
      const canvas = document.createElement('canvas');
      canvas.width = width * scale;
      canvas.height = height * scale;
      const ctx = canvas.getContext('2d');
      ctx.scale(scale, scale);
      ctx.drawImage(img, 0, 0, width, height);
      URL.revokeObjectURL(url);
      canvas.toBlob(blob => {
        if (blob) resolve(blob);
        else reject(new Error('Canvas toBlob failed'));
      }, 'image/png');
    };
    img.onerror = () => { clearTimeout(timeout); URL.revokeObjectURL(url); reject(new Error('SVG image load failed')); };
    img.src = url;
  });
}

// ── Client-side preview generation (no server round-trip) ──

function generatePreviewClientSide(context, format, data) {
  if (format === 'json') {
    return JSON.stringify(data, null, 2);
  }
  if (format === 'jsonl') {
    const items = data.results || data.rows || [];
    return items.map(r => JSON.stringify(r)).join('\n');
  }
  if (format === 'csv') {
    return generateCsvPreview(data);
  }
  if (format === 'markdown') {
    return generateMarkdownPreviewData(context, data);
  }
  if (format === 'mermaid') {
    return generateMermaidPreview(data);
  }
  if (format === 'svg') {
    // SVG needs server-side Playwright — trigger fallback
    throw new Error('SVG requires server');
  }
  throw new Error('Unknown format');
}

function generateCsvPreview(data) {
  const rows = data.results || data.rows || [];
  if (rows.length === 0) return '';
  const cols = Object.keys(rows[0]);
  const escapeCsv = (v) => {
    const s = String(v ?? '');
    return s.includes(',') || s.includes('"') || s.includes('\n') ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  return [cols.join(','), ...rows.map(r => cols.map(c => escapeCsv(r[c])).join(','))].join('\n');
}

function generateMarkdownPreviewData(context, data) {
  if (context === 'workflow') {
    const lines = [`# ${data.name || 'Workflow'}`, ''];
    if (data.description) { lines.push(data.description, ''); }
    if (data.version) lines.push(`**Version:** ${data.version}`);
    const tools = (data.steps || []).map(s => s.tool).filter(Boolean);
    if (tools.length) lines.push(`**Tools used:** ${[...new Set(tools)].join(', ')}`);
    lines.push('');
    if (data.inputs && Object.keys(data.inputs).length > 0) {
      lines.push('## Inputs', '', '| Parameter | Type | Required | Default |', '|-----------|------|----------|---------|');
      for (const [k, v] of Object.entries(data.inputs)) {
        lines.push(`| ${k} | ${v.type || 'string'} | ${v.required ? 'Yes' : 'No'} | ${v.default ?? '—'} |`);
      }
      lines.push('');
    }
    if (data.steps) {
      lines.push('## Steps', '');
      data.steps.forEach((s, i) => {
        lines.push(`### ${i+1}. ${s.name || s.description || s.id}`);
        if (s.tool) lines.push(`- **Tool:** ${s.tool}`);
        lines.push('');
      });
    }
    return lines.join('\n');
  }
  if (context === 'search') {
    const lines = ['# Search Results', ''];
    if (data.query) lines.push(`**Query:** ${data.query}`, '');
    (data.results || []).forEach((r, i) => {
      lines.push(`### ${i+1}. ${r.source || 'Result'} (score: ${r.score ?? '—'})`);
      if (r.text) lines.push('', `> ${r.text.slice(0, 200)}`, '');
    });
    return lines.join('\n');
  }
  if (context === 'chat') {
    const lines = ['# Chat Session', ''];
    (data.turns || []).forEach(t => {
      lines.push(`**${t.role === 'user' ? 'User' : 'vai'}:**`, t.content || '', '', '---', '');
    });
    return lines.join('\n');
  }
  if (context === 'benchmark') {
    const results = data.results || data.rows || [];
    if (results.length === 0) return '# Benchmark Results\n\nNo data.';
    const cols = Object.keys(results[0]);
    const lines = ['# Benchmark Results', '', '| ' + cols.join(' | ') + ' |', '| ' + cols.map(() => '---').join(' | ') + ' |'];
    results.forEach(r => lines.push('| ' + cols.map(c => String(r[c] ?? '')).join(' | ') + ' |'));
    return lines.join('\n');
  }
  return JSON.stringify(data, null, 2);
}

function generateMermaidPreview(data) {
  const steps = data.steps || [];
  if (steps.length === 0) return 'graph TD\n    empty["No steps"]';

  const TOOL_CATS = { query: 'retrieval', search: 'retrieval', rerank: 'retrieval', embed: 'embedding', similarity: 'embedding', generate: 'generation', ingest: 'ingestion' };
  const EMOJI = { retrieval: '🔍', embedding: '🧬', generation: '✨', ingestion: '📥', control: '⚙️', utility: '🔧' };
  const getCat = (t) => TOOL_CATS[t] || (['merge','filter','transform','conditional','loop','template'].includes(t) ? 'control' : 'utility');

  const lines = ["%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#00D4AA' }}}%%", 'graph TD'];
  const cats = new Set();

  // Nodes
  for (const s of steps) {
    const cat = getCat(s.tool);
    cats.add(cat);
    const label = `${EMOJI[cat] || '🔧'} ${s.name || s.description || s.id}`;
    lines.push(`    ${s.id}(["${label}"]):::${cat}`);
  }
  lines.push('');

  // Edges — scan inputs for {{ ref.output }} template patterns
  const stepIds = new Set(steps.map(s => s.id));
  for (const s of steps) {
    const inputStr = JSON.stringify(s.inputs || {});
    for (const sid of stepIds) {
      if (sid !== s.id && inputStr.includes(sid)) {
        lines.push(`    ${sid} --> ${s.id}`);
      }
    }
  }
  lines.push('');

  // Class defs
  const classDefs = {
    retrieval: 'classDef retrieval fill:#00838F,stroke:#00D4AA,color:#fff',
    embedding: 'classDef embedding fill:#4A148C,stroke:#CE93D8,color:#fff',
    generation: 'classDef generation fill:#6A1B9A,stroke:#CE93D8,color:#fff',
    control: 'classDef control fill:#424242,stroke:#9E9E9E,color:#fff',
    utility: 'classDef utility fill:#E65100,stroke:#FFB74D,color:#fff',
    ingestion: 'classDef ingestion fill:#1B5E20,stroke:#66BB6A,color:#fff',
  };
  for (const cat of cats) {
    if (classDefs[cat]) lines.push(`    ${classDefs[cat]}`);
  }

  return lines.join('\n');
}

// ── Preview helpers ──

function escapeHtmlExport(str) {
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function highlightJson(str) {
  // Syntax-highlight JSON
  return escapeHtmlExport(str)
    .replace(/"([^"\\]|\\.)*"\s*:/g, (m) => `<span class="json-key">${m}</span>`)
    .replace(/:\s*"([^"\\]|\\.)*"/g, (m) => `: <span class="json-str">${m.slice(2)}</span>`)
    .replace(/:\s*(\d+\.?\d*)/g, (m, n) => `: <span class="json-num">${n}</span>`)
    .replace(/:\s*(true|false)/g, (m, b) => `: <span class="json-bool">${b}</span>`)
    .replace(/:\s*(null)/g, (m, n) => `: <span class="json-null">${n}</span>`);
}

function csvToHtmlTable(csv) {
  const lines = csv.trim().split('\n');
  if (lines.length === 0) return '';
  const headers = parseCsvLine(lines[0]);
  let html = '<table><thead><tr>' + headers.map(h => `<th>${escapeHtmlExport(h)}</th>`).join('') + '</tr></thead><tbody>';
  for (let i = 1; i < lines.length; i++) {
    const cols = parseCsvLine(lines[i]);
    html += '<tr>' + cols.map(c => `<td>${escapeHtmlExport(c)}</td>`).join('') + '</tr>';
  }
  return html + '</tbody></table>';
}

function parseCsvLine(line) {
  const result = []; let current = ''; let inQuotes = false;
  for (let i = 0; i < line.length; i++) {
    if (inQuotes) {
      if (line[i] === '"' && line[i+1] === '"') { current += '"'; i++; }
      else if (line[i] === '"') { inQuotes = false; }
      else { current += line[i]; }
    } else {
      if (line[i] === '"') { inQuotes = true; }
      else if (line[i] === ',') { result.push(current); current = ''; }
      else { current += line[i]; }
    }
  }
  result.push(current);
  return result;
}

function renderMarkdownPreview(md) {
  // Use the existing renderMarkdown if available, otherwise basic conversion
  if (typeof renderMarkdown === 'function') {
    return renderMarkdown(md);
  }
  // Basic markdown → HTML
  let html = escapeHtmlExport(md);
  // Headers
  html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
  html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
  html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
  // Bold
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  // Code
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
  // Blockquotes
  html = html.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
  // HR
  html = html.replace(/^---$/gm, '<hr>');
  // Lists
  html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
  html = html.replace(/(<li>.*<\/li>\n?)+/g, (m) => `<ul>${m}</ul>`);
  // Tables (pipe-delimited)
  html = html.replace(/(\|.+\|\n\|[-| :]+\|\n(?:\|.+\|\n?)*)/g, (table) => {
    const rows = table.trim().split('\n').filter(r => !r.match(/^\|[-| :]+\|$/));
    if (rows.length === 0) return table;
    const headerCells = rows[0].split('|').filter(c => c.trim());
    let t = '<table><thead><tr>' + headerCells.map(c => `<th>${c.trim()}</th>`).join('') + '</tr></thead><tbody>';
    for (let i = 1; i < rows.length; i++) {
      const cells = rows[i].split('|').filter(c => c.trim());
      t += '<tr>' + cells.map(c => `<td>${c.trim()}</td>`).join('') + '</tr>';
    }
    return t + '</tbody></table>';
  });
  // Paragraphs
  html = html.replace(/\n\n/g, '</p><p>');
  html = '<p>' + html + '</p>';
  html = html.replace(/<p><(h[1-3]|table|ul|hr|blockquote)/g, '<$1');
  html = html.replace(/<\/(h[1-3]|table|ul|hr|blockquote)><\/p>/g, '</$1>');
  return html;
}

async function renderMermaidPreview(src) {
  const container = document.getElementById('exportMermaidRender');
  if (!container) return;

  container.innerHTML = '<div style="color:var(--text-muted);font-size:13px;">Loading diagram renderer…</div>';
  try {
    await loadMermaidLib();
    const { svg } = await mermaid.render('export-mermaid-preview', src);
    container.innerHTML = svg;
  } catch (e) {
    container.innerHTML = `<div class="export-preview-source">${escapeHtmlExport(src)}</div>`;
  }
}

function getExportData(context) {
  switch (context) {
    case 'workflow':
      return typeof wfState !== 'undefined' && wfState.activeWorkflow ? wfState.activeWorkflow : null;
    case 'search':
      return window._lastSearchResults || null;
    case 'chat':
      return window._chatExportData || null;
    case 'benchmark':
      return window._lastBenchmarkResults || null;
    default:
      return null;
  }
}

async function exportToFile() {
  const data = getExportData(_exportContext);
  if (!data) { showExportSnack('Nothing to export'); return; }

  const cacheKey = `${_exportContext}:${_exportFormat}`;
  const cached = _exportPreviewCache[cacheKey];
  const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
  const name = (data.name || _exportContext).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
  const filename = `${_exportContext}-${name}-${ts}${FORMAT_EXTS[_exportFormat] || '.txt'}`;

  try {
    let blob;
    let content;
    const isBinary = ['png', 'pdf'].includes(_exportFormat);

    // Use cached client-side data when available
    if (cached && cached.pngBlob && _exportFormat === 'png') {
      blob = cached.pngBlob;
    } else if (cached && cached.text && !isBinary) {
      content = cached.text;
    } else if (!isBinary) {
      // Generate client-side
      content = generatePreviewClientSide(_exportContext, _exportFormat, data);
    } else if (_exportFormat === 'svg') {
      // Generate SVG client-side
      await loadMermaidLib();
      const mermaidSrc = generateMermaidPreview(data);
      const id = 'export-save-' + Date.now();
      const { svg } = await mermaid.render(id, mermaidSrc);
      content = svg.replace('</svg>', '<text x="99%" y="99%" text-anchor="end" font-size="10" fill="#666" opacity="0.5">Generated by vai</text></svg>');
    } else {
      // PDF: server-side fallback
      const resp = await fetch('/api/export/' + _exportContext, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ format: _exportFormat, data, options: {} }),
      });
      if (!resp.ok) throw new Error(await resp.text());
      blob = await resp.blob();
    }

    // Electron: native save dialog
    if (window.vai && window.vai.export) {
      let saveContent;
      if (blob) {
        const buf = await blob.arrayBuffer();
        saveContent = btoa(String.fromCharCode(...new Uint8Array(buf)));
      } else {
        saveContent = content;
      }
      const filters = [{ name: FORMAT_LABELS[_exportFormat], extensions: [FORMAT_EXTS[_exportFormat]?.slice(1) || '*'] }];
      const result = await window.vai.export.saveFile({ content: saveContent, filename, filters, isBinary: !!blob });
      if (result && result.success) {
        showExportSnack(`Saved to ${result.filePath}`);
        closeExportDialog();
      }
    } else {
      // Browser: download via blob
      if (!blob) blob = new Blob([content], { type: isBinary ? 'application/octet-stream' : 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = filename;
      document.body.appendChild(a); a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      showExportSnack(`Downloaded ${filename}`);
      closeExportDialog();
    }
  } catch (err) {
    showExportSnack('Export failed: ' + err.message);
  }
}

async function exportToClipboard() {
  const data = getExportData(_exportContext);
  if (!data) { showExportSnack('Nothing to export'); return; }

  try {
    // For visual formats, copy the underlying text (SVG source or JSON)
    const cacheKey = `${_exportContext}:${_exportFormat}`;
    const cached = _exportPreviewCache[cacheKey];
    let text;

    if (cached && cached.text) {
      text = cached.text;
    } else if (['png', 'pdf'].includes(_exportFormat)) {
      // Copy JSON instead for binary formats
      text = JSON.stringify(data, null, 2);
    } else {
      text = generatePreviewClientSide(_exportContext, _exportFormat, data);
    }

    if (window.vai && window.vai.export) {
      await window.vai.export.copyToClipboard({ content: text, type: 'text' });
    } else {
      await navigator.clipboard.writeText(text);
    }
    showExportSnack('Copied to clipboard');
    closeExportDialog();
  } catch (err) {
    showExportSnack('Copy failed: ' + err.message);
  }
}

function showExportSnack(msg) {
  const existing = document.querySelector('.export-snack');
  if (existing) existing.remove();
  const el = document.createElement('div');
  el.className = 'export-snack';
  el.textContent = msg;
  document.body.appendChild(el);
  setTimeout(() => el.remove(), 3000);
}

// ── Keyboard shortcut: ⌘E / Ctrl+E ──
document.addEventListener('keydown', (e) => {
  if ((e.metaKey || e.ctrlKey) && e.key === 'e' && !e.shiftKey) {
    e.preventDefault();
    const overlay = document.getElementById('exportOverlay');
    if (overlay.classList.contains('open')) {
      closeExportDialog();
    } else {
      // Detect current tab context
      const tab = getActiveTabId();
      const contextMap = {
        workflows: 'workflow', search: 'search', chat: 'chat',
        benchmark: 'benchmark', explore: 'explore',
      };
      const context = contextMap[tab];
      if (context && getExportData(context)) {
        openExportDialog(context);
      }
    }
  }
});

// ── Electron menu trigger ──
if (window.vai && window.vai.export && window.vai.export.onTrigger) {
  window.vai.export.onTrigger(() => {
    const tab = getActiveTabId();
    const contextMap = {
      workflows: 'workflow', search: 'search', chat: 'chat',
      benchmark: 'benchmark', explore: 'explore',
    };
    const context = contextMap[tab];
    if (context) openExportDialog(context);
  });
}

// ── Show export buttons when data becomes available ──
// Hooks into existing search/chat/benchmark result handlers

// Search: show export btn after results arrive
const _origDoSearch = typeof doSearch === 'function' ? doSearch : null;
if (_origDoSearch) {
  // We patch via a MutationObserver on the result container instead
}
const searchResultEl = document.getElementById('searchResult');
if (searchResultEl) {
  new MutationObserver(() => {
    const btn = document.getElementById('searchExportBtn');
    if (btn && searchResultEl.children.length > 0) btn.style.display = '';
  }).observe(searchResultEl, { childList: true });
}

// Chat: show export btn after first message
const chatMessagesEl = document.getElementById('chatMessages');
if (chatMessagesEl) {
  new MutationObserver(() => {
    const btn = document.getElementById('chatExportBtn');
    const msgs = chatMessagesEl.querySelectorAll('.chat-message:not(.system-msg)');
    if (btn) btn.style.display = msgs.length > 0 ? '' : 'none';
  }).observe(chatMessagesEl, { childList: true });
}

// ══════════════════════════════════════════════════════════════════
// Command Palette Implementation — Phase 2
// ══════════════════════════════════════════════════════════════════

// Fuzzy search algorithm - from spec section 4.1
function fuzzyScore(query, text) {
  const q = query.toLowerCase();
  const t = text.toLowerCase();

  // Exact prefix match
  if (t.startsWith(q)) return 1.0;

  // Contiguous substring
  if (t.includes(q)) return 0.9;

  // Fuzzy subsequence with gap penalty
  let qi = 0;
  let lastMatchIndex = -1;
  let gapPenalty = 0;
  let wordBoundaryBonus = 0;

  for (let ti = 0; ti < t.length && qi < q.length; ti++) {
    if (t[ti] === q[qi]) {
      if (ti > 0 && (t[ti - 1] === ' ' || t[ti - 1] === '-' || t[ti - 1] === '_')) {
        wordBoundaryBonus += 0.05;
      }
      if (lastMatchIndex >= 0) {
        gapPenalty += (ti - lastMatchIndex - 1) * 0.02;
      }
      lastMatchIndex = ti;
      qi++;
    }
  }

  if (qi < q.length) return 0; // Not all query characters matched
  return Math.max(0, 0.8 - gapPenalty + wordBoundaryBonus);
}

// Search registry function - enhanced with recency bonus
function searchRegistry(query, registry, recentIds = []) {
  if (!query.trim()) {
    // Empty query: show recent items first, then suggested actions
    return [
      ...registry.filter(item => recentIds.includes(item.id)),
      ...registry.filter(item => item.priority <= 5 && !recentIds.includes(item.id))
    ].slice(0, 12);
  }

  return registry
    .map(item => {
      const labelScore = fuzzyScore(query, item.label) * 1.0;
      const keywordScore = Math.max(...(item.keywords || []).map(kw => fuzzyScore(query, kw))) * 0.8;
      const descScore = fuzzyScore(query, item.description || '') * 0.5;
      const catScore = fuzzyScore(query, item.category) * 0.3;
      const recencyBonus = recentIds.includes(item.id) ? 0.15 : 0;

      const score = Math.max(labelScore, keywordScore, descScore, catScore) + recencyBonus;
      return { ...item, _score: score };
    })
    .filter(item => item._score > 0.2) // Minimum relevance threshold
    .sort((a, b) => b._score - a._score || a.priority - b.priority)
    .slice(0, 12); // Max 12 visible results
}

// Get current context for contextual commands
function getCommandPaletteContext() {
  const activeTab = document.querySelector('.tab-btn.active')?.dataset?.tab || 'home';
  return {
    activeTab,
    hasResults: true, // Could be enhanced to check actual data
    hasActiveWorkflow: activeTab === 'workflows' && wfState?.activeWorkflow,
    hasActiveChatSession: activeTab === 'chat',
    isConnected: true // Could check actual connection state
  };
}

// Generate dynamic workflow commands from wfState
function generateWorkflowCommands() {
  const commands = [];
  
  if (!wfState) return commands;
  
  // Built-in workflows
  if (wfState.workflows) {
    wfState.workflows.forEach(wf => {
      commands.push({
        id: `workflow-${wf.name.toLowerCase().replace(/\s+/g, '-')}`,
        label: `Run "${wf.name}"`,
        description: wf.description || 'Built-in workflow',
        category: 'workflow',
        keywords: [...(wf.tags || []), ...(wf.category ? [wf.category] : []), 'builtin', 'template'],
        icon: '🔧',
        priority: 3,
        workflowData: wf,
        execute: () => {
          switchTab('workflows');
          // Could load the workflow in the canvas
          if (typeof loadWorkflow === 'function') {
            loadWorkflow(wf);
          }
        }
      });
    });
  }
  
  // Community workflows
  if (wfState.community) {
    wfState.community.forEach(wf => {
      commands.push({
        id: `workflow-community-${wf.name.toLowerCase().replace(/\s+/g, '-')}`,
        label: `Run "${wf.name}"`,
        description: wf.description || 'Community workflow',
        category: 'workflow',
        keywords: [...(wf.tags || []), ...(wf.category ? [wf.category] : []), 'community', 'external'],
        icon: '🌐',
        priority: 4,
        workflowData: wf,
        execute: () => {
          switchTab('workflows');
          if (typeof loadWorkflow === 'function') {
            loadWorkflow(wf);
          }
        }
      });
    });
  }
  
  return commands;
}

// Generate contextual commands based on active tab
function generateContextualCommands() {
  const context = getCommandPaletteContext();
  const commands = [];
  
  // Export commands based on active tab
  const exportCommands = [
    { id: 'action-export-mermaid', label: 'Export as Mermaid', description: 'Export workflow as Mermaid diagram', when: ctx => ctx.activeTab === 'workflows', icon: '📊' },
    { id: 'action-export-png', label: 'Export as PNG', description: 'Export current view as PNG image', when: ctx => ['workflows', 'benchmark', 'explore'].includes(ctx.activeTab), icon: '🖼️' },
    { id: 'action-export-svg', label: 'Export as SVG', description: 'Export current view as SVG', when: ctx => ['workflows', 'benchmark', 'explore'].includes(ctx.activeTab), icon: '🎨' },
    { id: 'action-export-csv', label: 'Export as CSV', description: 'Export results as CSV', when: ctx => ['search', 'benchmark'].includes(ctx.activeTab), icon: '📄' },
    { id: 'action-export-markdown', label: 'Export as Markdown', description: 'Export current view as Markdown', when: ctx => ['workflows', 'search', 'chat'].includes(ctx.activeTab), icon: '📝' },
    { id: 'action-export-pdf', label: 'Export as PDF', description: 'Export chat session as PDF', when: ctx => ctx.activeTab === 'chat', icon: '📋' },
    { id: 'action-validate-workflow', label: 'Validate Workflow', description: 'Check workflow definition for errors', when: ctx => ctx.activeTab === 'workflows', icon: '✅' },
    { id: 'action-dry-run', label: 'Dry Run Workflow', description: 'Preview execution plan without running', when: ctx => ctx.activeTab === 'workflows', icon: '🔍' }
  ];
  
  exportCommands.forEach(cmd => {
    if (!cmd.when || cmd.when(context)) {
      commands.push({
        ...cmd,
        category: 'action',
        keywords: ['export', 'save', cmd.label.toLowerCase().split(' ')].flat(),
        priority: 3,
        execute: () => {
          // Special handlers for specific actions
          if (cmd.id === 'action-validate-workflow') {
            wfValidateWorkflow();
          } else if (cmd.id === 'action-dry-run') {
            wfDryRun();
          } else if (typeof openExportDialog === 'function') {
            openExportDialog(context.activeTab);
          } else {
            alert(`${cmd.label} not yet implemented`);
          }
        }
      });
    }
  });
  
  return commands;
}

// Static command registry
const staticCommandRegistry = [
  // Navigation Commands
  { id: 'nav-home', label: 'Overview', description: 'Dashboard, status, and recent activity', category: 'navigation', keywords: ['dashboard', 'landing', 'start', 'home'], icon: '🏠', shortcut: '⌘1', priority: 1, execute: () => switchTab('home') },
  { id: 'nav-embed', label: 'Embed', description: 'Generate and inspect embeddings', category: 'navigation', keywords: ['embedding', 'generate', 'vector'], icon: '⚡', shortcut: '⌘2', priority: 1, execute: () => switchTab('embed') },
  { id: 'nav-compare', label: 'Compare', description: 'Inspect similarity between inputs and scores', category: 'navigation', keywords: ['similarity', 'cosine', 'compare', 'evaluate'], icon: '🔗', shortcut: '⌘3', priority: 1, execute: () => switchTab('compare') },
  { id: 'nav-search', label: 'Rerank', description: 'Re-order candidate documents by relevance', category: 'navigation', keywords: ['find', 'query', 'vector', 'lookup', 'rerank', 'search'], icon: '🔍', shortcut: '⌘4', priority: 1, execute: () => switchTab('search') },
  { id: 'nav-multimodal', label: 'Multimodal', description: 'Cross-modal search with images and text', category: 'navigation', keywords: ['image', 'text', 'cross-modal', 'vision'], icon: '🖼️', shortcut: '⌘5', priority: 1, execute: () => switchTab('multimodal') },
  { id: 'nav-generate', label: 'Generate', description: 'Code generation and templates', category: 'navigation', keywords: ['code', 'template', 'scaffold'], icon: '💻', shortcut: '⌘6', priority: 1, execute: () => switchTab('generate') },
  { id: 'nav-chat', label: 'Chat', description: 'Chat with your knowledge base', category: 'navigation', keywords: ['conversation', 'ask', 'rag', 'talk'], icon: '💬', shortcut: '⌘7', priority: 1, execute: () => switchTab('chat') },
  { id: 'nav-workflows', label: 'Workflows', description: 'Visual workflow canvas', category: 'navigation', keywords: ['pipeline', 'dag', 'canvas'], icon: '🔄', shortcut: '⌘8', priority: 1, execute: () => switchTab('workflows') },
  { id: 'nav-models', label: 'Models', description: 'Voyage AI model catalog and specs', category: 'navigation', keywords: ['model', 'voyage', 'catalog', 'specs', 'embedding', 'rerank', 'pricing'], icon: '🤖', priority: 1, execute: () => switchTab('models') },
  { id: 'nav-benchmark', label: 'Benchmark', description: 'Model comparison on your data', category: 'navigation', keywords: ['performance', 'test', 'evaluate', 'benchmark'], icon: '📊', shortcut: '⌘9', priority: 1, execute: () => switchTab('benchmark') },
  { id: 'nav-explore', label: 'Explore', description: 'Embedding space visualization', category: 'navigation', keywords: ['pca', 'tsne', 'visualize', 'scatter'], icon: '🎯', shortcut: '⌘0', priority: 1, execute: () => switchTab('explore') },
  { id: 'nav-about', label: 'About', description: 'Version and project information', category: 'navigation', keywords: ['version', 'info', 'credits'], icon: 'ℹ️', priority: 5, execute: () => switchTab('about') },
  { id: 'nav-settings', label: 'Settings', description: 'App configuration', category: 'navigation', keywords: ['preferences', 'config', 'options'], icon: '⚙️', shortcut: '⌘,', priority: 2, execute: () => switchTab('settings') },

  // Action Commands
  { id: 'action-export', label: 'Export...', description: 'Export the current view', category: 'action', keywords: ['export', 'share', 'save', 'download'], icon: '📤', shortcut: '⌘E', priority: 3, 
    execute: () => {
      const context = getCommandPaletteContext();
      if (typeof openExportDialog === 'function') {
        openExportDialog(context.activeTab);
      } else {
        alert('Export dialog not available');
      }
    }
  },
  
  // Sub-palette actions
  { id: 'action-run-workflow', label: 'Run Workflow...', description: 'Select and run a workflow', category: 'action', keywords: ['workflow', 'run', 'execute', 'pipeline'], icon: '🔄', priority: 2, 
    execute: () => showWorkflowSubPalette()
  },
  { id: 'action-resume-chat', label: 'Resume Chat Session...', description: 'Continue a previous chat', category: 'action', keywords: ['chat', 'resume', 'continue', 'history'], icon: '💬', priority: 2, 
    execute: () => showChatSessionSubPalette()
  },
  
  { id: 'action-toggle-theme', label: 'Toggle Dark/Light Mode', description: 'Switch between dark and light themes', category: 'action', keywords: ['theme', 'dark', 'light', 'mode'], icon: '🌙', priority: 4, 
    execute: () => {
      const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
      document.documentElement.setAttribute('data-theme', currentTheme === 'dark' ? 'light' : 'dark');
    }
  },
  
  // Settings Commands
  { id: 'setting-api-key', label: 'Configure API Key', description: 'Set your Voyage AI API key', category: 'setting', keywords: ['api', 'key', 'voyage', 'auth'], icon: '🔐', priority: 6, 
    execute: () => {
      switchTab('settings');
      setTimeout(() => {
        if (typeof switchSettingsSection === 'function') switchSettingsSection('api');
      }, 100);
    }
  },
  { id: 'setting-mongodb-uri', label: 'Configure MongoDB URI', description: 'Set your MongoDB connection string', category: 'setting', keywords: ['mongo', 'mongodb', 'uri', 'connection', 'database'], icon: '🍃', priority: 6, 
    execute: () => {
      switchTab('settings');
      setTimeout(() => {
        if (typeof switchSettingsSection === 'function') switchSettingsSection('connection');
      }, 100);
    }
  },

  // Explainer Commands - key topics
  { id: 'explain-embeddings', label: 'Learn: Embeddings', description: 'What are vector embeddings?', category: 'explainer', keywords: ['embeddings', 'vectors', 'what are', 'introduction'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
  { id: 'explain-reranking', label: 'Learn: Reranking', description: 'Two-stage retrieval with rerankers', category: 'explainer', keywords: ['reranking', 'two-stage', 'retrieval', 'precision'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
  { id: 'explain-vector-search', label: 'Learn: Vector Search', description: 'MongoDB Atlas Vector Search', category: 'explainer', keywords: ['vector', 'search', 'atlas', 'similarity', 'knn'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
  { id: 'explain-rag', label: 'Learn: RAG Pipelines', description: 'Retrieval-Augmented Generation', category: 'explainer', keywords: ['rag', 'retrieval', 'augmented', 'generation', 'pipeline'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
  { id: 'explain-models', label: 'Learn: Voyage AI Models', description: 'Choosing the right model', category: 'explainer', keywords: ['models', 'voyage-4', 'large', 'lite', 'domain'], icon: '📚', priority: 7, execute: () => switchTab('models') },
  { id: 'explain-quantization', label: 'Learn: Quantization', description: 'Reduce storage costs with lower-precision embeddings', category: 'explainer', keywords: ['quantization', 'int8', 'ubinary', 'compression', 'storage'], icon: '📚', priority: 7, execute: () => switchTab('explore') }
];

// Build complete command registry (static + dynamic)
function buildCommandRegistry() {
  return [
    ...staticCommandRegistry,
    ...generateWorkflowCommands(),
    ...generateContextualCommands()
  ];
}

// Enhanced recent items tracking
let recentItemsTracker = {
  items: [], // Array of { id, action, timestamp, details }
  
  addItem(id, action, details = {}) {
    this.items = this.items.filter(item => item.id !== id); // Remove duplicate
    this.items.unshift({ id, action, timestamp: Date.now(), details });
    this.items = this.items.slice(0, 10); // Keep last 10
  },
  
  getRecentIds() {
    return this.items.map(item => item.id);
  },
  
  clear() {
    this.items = [];
  }
};

// Track various user actions
function trackUserAction(type, details) {
  const timestamp = Date.now();
  let id, action;
  
  switch (type) {
    case 'tab-navigation':
      id = `nav-${details.tab}`;
      action = `Visited ${details.tab}`;
      break;
    case 'workflow-run':
      id = `workflow-${details.workflowName.toLowerCase().replace(/\s+/g, '-')}`;
      action = `Ran "${details.workflowName}"`;
      break;
    case 'export':
      id = `action-export-${details.format}`;
      action = `Exported as ${details.format.toUpperCase()}`;
      break;
    case 'chat-session':
      id = `chat-session-${details.sessionId}`;
      action = `Chat session`;
      break;
    default:
      return;
  }
  
  recentItemsTracker.addItem(id, action, details);
}

// Command Palette State - enhanced for Phase 2
let commandPaletteState = {
  isOpen: false,
  query: '',
  selectedIndex: 0,
  categoryFilter: null,
  results: [],
  recentCommands: [],
  subPalette: null, // { type: 'workflow' | 'chat', items: [...] }
  previewData: null,
  lastToggleTime: 0, // Phase 3: for debouncing
  onboardingShown: false // Phase 3: first-time onboarding
};

// Sub-palette functions
function showWorkflowSubPalette() {
  const header = document.getElementById('commandPaletteHeader');
  const title = document.getElementById('commandPaletteTitle');
  const input = document.getElementById('commandPaletteInput');
  
  header.style.display = 'flex';
  title.textContent = 'Select a workflow';
  input.placeholder = 'Type to filter workflows...';
  
  const workflows = generateWorkflowCommands();
  commandPaletteState.subPalette = { type: 'workflow', items: workflows };
  commandPaletteState.query = '';
  commandPaletteState.selectedIndex = 0;
  
  input.value = '';
  renderCommandPaletteResults();
}

function showChatSessionSubPalette() {
  const header = document.getElementById('commandPaletteHeader');
  const title = document.getElementById('commandPaletteTitle');
  const input = document.getElementById('commandPaletteInput');
  
  header.style.display = 'flex';
  title.textContent = 'Resume chat session';
  input.placeholder = 'Type to filter chat sessions...';
  
  // Generate mock chat sessions (could be enhanced to use real data)
  const chatSessions = [
    { id: 'chat-session-1', label: 'Legal Document Analysis', description: 'Last message: "Can you summarize the key terms?"', category: 'recent', icon: '💬', priority: 1, 
      execute: () => { switchTab('chat'); /* Load session */ } },
    { id: 'chat-session-2', label: 'Research Query', description: 'Last message: "What are the latest developments?"', category: 'recent', icon: '💬', priority: 1,
      execute: () => { switchTab('chat'); /* Load session */ } },
    { id: 'chat-session-3', label: 'Code Review', description: 'Last message: "Please explain this function."', category: 'recent', icon: '💬', priority: 1,
      execute: () => { switchTab('chat'); /* Load session */ } }
  ];
  
  commandPaletteState.subPalette = { type: 'chat', items: chatSessions };
  commandPaletteState.query = '';
  commandPaletteState.selectedIndex = 0;
  
  input.value = '';
  renderCommandPaletteResults();
}

function goBackToMainPalette() {
  const header = document.getElementById('commandPaletteHeader');
  const input = document.getElementById('commandPaletteInput');
  
  header.style.display = 'none';
  input.placeholder = 'Type a command or search...';
  
  commandPaletteState.subPalette = null;
  commandPaletteState.query = '';
  commandPaletteState.selectedIndex = 0;
  
  input.value = '';
  renderCommandPaletteResults();
}

// Preview functions
function updatePreviewPane(selectedItem) {
  const preview = document.getElementById('commandPalettePreview');
  const previewContent = document.getElementById('commandPalettePreviewContent');
  
  // Hide preview on narrow viewports
  if (window.innerWidth <= 800) {
    preview.style.display = 'none';
    return;
  }
  
  if (!selectedItem) {
    preview.style.display = 'none';
    return;
  }
  
  let content = '';
  
  switch (selectedItem.category) {
    case 'workflow':
      preview.style.display = 'block';
      content = `
        <div class="command-palette-preview-title">${selectedItem.label}</div>
        <div class="command-palette-preview-description">${selectedItem.description}</div>
        ${selectedItem.workflowData ? `
          <div class="command-palette-preview-steps">
            <li>Input processing</li>
            <li>Vector embedding</li>
            <li>Search & retrieval</li>
            <li>Output generation</li>
          </div>
        ` : ''}
      `;
      break;
      
    case 'explainer':
      preview.style.display = 'block';
      content = `
        <div class="command-palette-preview-title">${selectedItem.label}</div>
        <div class="command-palette-preview-description">${selectedItem.description}</div>
        <div style="font-style: italic; color: var(--text-muted);">
          Click to open detailed explanation and examples in the Explore tab.
        </div>
      `;
      break;
      
    case 'recent':
      if (selectedItem.id.startsWith('chat-session-')) {
        preview.style.display = 'block';
        content = `
          <div class="command-palette-preview-title">${selectedItem.label}</div>
          <div class="command-palette-preview-description">${selectedItem.description}</div>
        `;
      } else {
        preview.style.display = 'none';
      }
      break;
      
    case 'setting':
      preview.style.display = 'block';
      content = `
        <div class="command-palette-preview-title">${selectedItem.label}</div>
        <div class="command-palette-preview-description">${selectedItem.description}</div>
        <div style="font-style: italic; color: var(--text-muted); margin-top: 8px;">
          Current: Not configured
        </div>
      `;
      break;
      
    default:
      preview.style.display = 'none';
  }
  
  previewContent.innerHTML = content;
}

// Command Palette Functions
function openCommandPalette() {
  const overlay = document.getElementById('commandPaletteOverlay');
  const input = document.getElementById('commandPaletteInput');
  
  commandPaletteState.isOpen = true;
  commandPaletteState.query = '';
  commandPaletteState.selectedIndex = 0;
  commandPaletteState.subPalette = null;
  
  // Reset to main palette view
  document.getElementById('commandPaletteHeader').style.display = 'none';
  input.placeholder = 'Type a command or search...';
  
  overlay.classList.add('open');
  input.value = '';
  input.focus();
  
  renderCommandPaletteResults();
  
  // Phase 3: Show onboarding tooltip on first open (if not already shown)
  showOnboardingTooltipIfNeeded();
}

// Phase 3: Onboarding tooltip for first-time users
function showOnboardingTooltipIfNeeded() {
  if (commandPaletteState.onboardingShown) return;
  
  // Create tooltip element
  const tooltip = document.createElement('div');
  tooltip.className = 'onboarding-tooltip';
  tooltip.textContent = 'Press ⌘K to search commands';
  document.body.appendChild(tooltip);
  
  // Show after 2 seconds, dismiss after 5 seconds or on keypress
  setTimeout(() => {
    tooltip.classList.add('show');
    
    // Auto-dismiss after 5 seconds
    const dismissTimeout = setTimeout(() => {
      dismissOnboardingTooltip(tooltip);
    }, 5000);
    
    // Dismiss on any keypress
    const keypressHandler = () => {
      clearTimeout(dismissTimeout);
      dismissOnboardingTooltip(tooltip);
      document.removeEventListener('keydown', keypressHandler, true);
    };
    
    document.addEventListener('keydown', keypressHandler, true);
  }, 2000);
  
  commandPaletteState.onboardingShown = true;
}

function dismissOnboardingTooltip(tooltip) {
  if (tooltip && tooltip.parentNode) {
    tooltip.classList.remove('show');
    setTimeout(() => {
      if (tooltip.parentNode) {
        tooltip.parentNode.removeChild(tooltip);
      }
    }, 300);
  }
}

function closeCommandPalette() {
  const overlay = document.getElementById('commandPaletteOverlay');
  
  commandPaletteState.isOpen = false;
  overlay.classList.remove('open');
  
  // Hide preview
  document.getElementById('commandPalettePreview').style.display = 'none';
}

function toggleCommandPalette() {
  // Phase 3 Edge Case: Debounce rapid ⌘K presses at 200ms
  const now = Date.now();
  if (now - commandPaletteState.lastToggleTime < 200) {
    return;
  }
  commandPaletteState.lastToggleTime = now;

  if (commandPaletteState.isOpen) {
    closeCommandPalette();
  } else {
    openCommandPalette();
  }
}

function renderCommandPaletteResults() {
  const resultsContainer = document.getElementById('commandPaletteResults');
  const { query, selectedIndex, categoryFilter, subPalette } = commandPaletteState;
  
  let registry;
  if (subPalette) {
    registry = subPalette.items;
  } else {
    registry = buildCommandRegistry();
    
    if (categoryFilter) {
      registry = registry.filter(item => item.category === categoryFilter);
    }
  }
  
  const results = searchRegistry(query, registry, recentItemsTracker.getRecentIds());
  commandPaletteState.results = results;
  
  if (results.length === 0) {
    resultsContainer.innerHTML = `
      <div class="command-palette-empty">
        ${query ? `
          <div style="margin-bottom: 8px;">No commands matching <strong>"${query}"</strong></div>
          <div style="font-size: 12px; color: var(--text-muted);">Try a different search term or press <kbd style="background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 1px 4px; font-family: var(--mono);">Tab</kbd> to filter by category</div>
        ` : 'Type to search commands...'}
      </div>
    `;
    updatePreviewPane(null);
    return;
  }
  
  // Group results by category for main palette only
  let html = '';
  
  if (!subPalette && !query) {
    // Show grouped sections for empty query in main palette
    const groupedResults = {};
    results.forEach(item => {
      if (!groupedResults[item.category]) {
        groupedResults[item.category] = [];
      }
      groupedResults[item.category].push(item);
    });
    
    const categories = Object.keys(groupedResults);
    
    categories.forEach((category, categoryIndex) => {
      if (categories.length > 1) {
        let categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
        if (category === 'recent') categoryTitle = 'Recently Used';
        html += `<div class="command-palette-section-title">${categoryTitle}</div>`;
      }
      
      groupedResults[category].forEach((item) => {
        const globalIndex = results.indexOf(item);
        const isSelected = globalIndex === selectedIndex;
        const isRecent = recentItemsTracker.getRecentIds().includes(item.id);
        
        html += `
          <div class="command-palette-item ${isSelected ? 'selected' : ''} ${isRecent ? 'recent' : ''}" 
               data-index="${globalIndex}"
               role="option" 
               aria-selected="${isSelected}">
            <div class="command-palette-icon">${isRecent ? '⏱' : (item.icon || '•')}</div>
            <div class="command-palette-content">
              <div class="command-palette-label">${item.label}</div>
              <div class="command-palette-description">${item.description}</div>
            </div>
            <div class="command-palette-badge ${item.category}">${item.category}</div>
            ${item.shortcut ? `<div class="command-palette-key">${item.shortcut}</div>` : ''}
          </div>
        `;
      });
    });
  } else {
    // Flat list for sub-palettes or when searching
    results.forEach((item, index) => {
      const isSelected = index === selectedIndex;
      const isRecent = recentItemsTracker.getRecentIds().includes(item.id);
      
      html += `
        <div class="command-palette-item ${isSelected ? 'selected' : ''} ${isRecent ? 'recent' : ''}" 
             data-index="${index}"
             role="option" 
             aria-selected="${isSelected}">
          <div class="command-palette-icon">${isRecent ? '⏱' : (item.icon || '•')}</div>
          <div class="command-palette-content">
            <div class="command-palette-label">${item.label}</div>
            <div class="command-palette-description">${item.description}</div>
          </div>
          <div class="command-palette-badge ${item.category}">${item.category}</div>
          ${item.shortcut ? `<div class="command-palette-key">${item.shortcut}</div>` : ''}
        </div>
      `;
    });
  }
  
  resultsContainer.innerHTML = html;
  
  // Update preview pane
  const selectedItem = results[selectedIndex];
  updatePreviewPane(selectedItem);
  
  // Update ARIA attributes
  const input = document.getElementById('commandPaletteInput');
  input.setAttribute('aria-activedescendant', selectedItem ? `command-item-${selectedIndex}` : '');
}

function executeSelectedCommand() {
  const { results, selectedIndex } = commandPaletteState;
  const selectedCommand = results[selectedIndex];
  
  if (selectedCommand && selectedCommand.execute) {
    // Add to recent commands using enhanced tracker
    recentItemsTracker.addItem(selectedCommand.id, selectedCommand.label, { 
      category: selectedCommand.category,
      timestamp: Date.now()
    });
    
    // Phase 3 Edge Case: Close palette immediately on execute
    // so subsequent keystrokes go to the new view
    closeCommandPalette();
    
    // Small delay to ensure palette is closed before execution
    setTimeout(() => {
      try {
        selectedCommand.execute();
      } catch (error) {
        console.error('Command execution failed:', error);
        alert('Failed to execute command: ' + error.message);
      }
    }, 10);
  }
}

function handleCommandPaletteKeydown(e) {
  if (!commandPaletteState.isOpen) return;
  
  const { results, subPalette } = commandPaletteState;
  
  switch (e.key) {
    case 'Escape':
      e.preventDefault();
      if (subPalette) {
        goBackToMainPalette();
      } else {
        closeCommandPalette();
      }
      break;
      
    case 'ArrowDown':
      e.preventDefault();
      commandPaletteState.selectedIndex = Math.min(commandPaletteState.selectedIndex + 1, results.length - 1);
      renderCommandPaletteResults();
      break;
      
    case 'ArrowUp':
      e.preventDefault();
      commandPaletteState.selectedIndex = Math.max(commandPaletteState.selectedIndex - 1, 0);
      renderCommandPaletteResults();
      break;
      
    case 'Enter':
      e.preventDefault();
      executeSelectedCommand();
      break;
      
    case 'Tab':
      e.preventDefault();
      // Could implement category filtering here
      break;
  }
}

function handleCommandPaletteInput(e) {
  commandPaletteState.query = e.target.value;
  commandPaletteState.selectedIndex = 0;
  renderCommandPaletteResults();
}

// Event Listeners
document.addEventListener('keydown', (e) => {
  // Global ⌘K / Ctrl+K shortcut
  if ((e.metaKey || e.ctrlKey) && e.key === 'k' && !e.shiftKey) {
    e.preventDefault();
    
    // Phase 3 Edge Case: Ensure command palette opens above modals
    // Check for existing modals and set higher z-index if needed
    const existingModals = document.querySelectorAll('.modal, .explore-modal, .cost-modal, [class*="modal"]');
    const commandPaletteOverlay = document.getElementById('commandPaletteOverlay');
    let maxZIndex = 3000; // Default z-index
    
    existingModals.forEach(modal => {
      const zIndex = parseInt(window.getComputedStyle(modal).zIndex, 10);
      if (!isNaN(zIndex) && zIndex >= maxZIndex) {
        maxZIndex = zIndex + 10;
      }
    });
    
    if (commandPaletteOverlay) {
      commandPaletteOverlay.style.zIndex = maxZIndex;
    }
    
    toggleCommandPalette();
    return;
  }
  
  // Handle command palette navigation
  handleCommandPaletteKeydown(e);
});

// Input event listener
document.getElementById('commandPaletteInput')?.addEventListener('input', handleCommandPaletteInput);

// Click handlers for command items
document.getElementById('commandPaletteResults')?.addEventListener('click', (e) => {
  const item = e.target.closest('.command-palette-item');
  if (item) {
    const index = parseInt(item.dataset.index);
    commandPaletteState.selectedIndex = index;
    executeSelectedCommand();
  }
});

// Hook into tab switching to track navigation
const originalSwitchTab = window.switchTab || switchTab;
if (typeof switchTab === 'function') {
  window.switchTab = function(tabId) {
    trackUserAction('tab-navigation', { tab: tabId });
    return originalSwitchTab(tabId);
  };
}

// Phase 3: Electron menu integration
if (window.electronAPI && window.electronAPI.ipcRenderer) {
  window.electronAPI.ipcRenderer.on('command-palette:toggle', () => {
    toggleCommandPalette();
  });
}

// Also listen for the export trigger from menu
if (window.electronAPI && window.electronAPI.ipcRenderer) {
  window.electronAPI.ipcRenderer.on('export:trigger', () => {
    const context = getCommandPaletteContext();
    if (typeof openExportDialog === 'function') {
      openExportDialog(context.activeTab);
    }
  });
}

// Expose command palette globally
window.commandPalette = {
  open: openCommandPalette,
  close: closeCommandPalette,
  toggle: toggleCommandPalette,
  state: commandPaletteState,
  trackAction: trackUserAction,
  recentItems: recentItemsTracker
};
</script>

<!-- Chart.js for Optimize tab -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>

<!-- Optimize Tab Module -->
<script src="./js/optimize-charts.js"></script>

<!-- Knowledge Base Manager -->
<script src="./js/kb-manager.js"></script>
<script src="./js/kb-ui.js"></script>

</body>
</html>