<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LightML Dashboard</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
/* ============================== RESET & BASE ============================== */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
    color: #e0e0e0;
    min-height: 100vh;
    overflow-x: hidden;
}

/* ============================== HEADER ============================== */
.header {
    position: sticky;
    top: 0;
    z-index: 100;
    background: rgba(15, 15, 26, 0.95);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid #2d2d44;
    padding: 12px 24px;
    display: flex;
    align-items: center;
    gap: 20px;
}

.logo {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 18px;
    font-weight: 700;
    color: #60a5fa;
    white-space: nowrap;
}
.logo .icon { font-size: 24px; }
.logo .sub {
    font-size: 11px;
    font-weight: 400;
    color: #6b7280;
    margin-left: 4px;
}

.header-controls {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: 1;
    justify-content: flex-end;
}

.db-badge {
    background: rgba(59, 130, 246, 0.15);
    border: 1px solid rgba(59, 130, 246, 0.3);
    color: #93c5fd;
    padding: 5px 12px;
    border-radius: 6px;
    font-size: 12px;
    font-family: monospace;
}

select, .btn {
    background: #1f2937;
    border: 1px solid #374151;
    color: #e0e0e0;
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s;
}
select:hover, .btn:hover { border-color: #60a5fa; }
select:focus, .btn:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.2); }

.btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; font-weight: 600; }
.btn-primary:hover { background: #2563eb; }
.btn-icon { display: flex; align-items: center; gap: 4px; }

/* ============================== VIEW TOGGLE ============================== */
.view-toggle {
    display: flex;
    border: 1px solid #374151;
    border-radius: 6px;
    overflow: hidden;
}
.view-toggle .vt-btn {
    background: #1f2937;
    border: none;
    color: #9ca3af;
    padding: 6px 14px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
}
.view-toggle .vt-btn:not(:last-child) { border-right: 1px solid #374151; }
.view-toggle .vt-btn:hover { background: #374151; color: #e0e0e0; }
.view-toggle .vt-btn.active { background: #3b82f6; color: #fff; }

/* ============================== TABS ============================== */
.tabs-bar {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 8px 24px 0;
    background: rgba(15, 15, 26, 0.6);
    border-bottom: 1px solid #2d2d44;
    overflow-x: auto;
}

.tab {
    padding: 10px 20px;
    font-size: 12px;
    font-weight: 500;
    color: #9ca3af;
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    white-space: nowrap;
    transition: all 0.2s;
}
.tab:hover { color: #e0e0e0; background: rgba(255,255,255,0.03); }
.tab.active {
    color: #60a5fa;
    border-bottom-color: #3b82f6;
    background: rgba(59, 130, 246, 0.08);
}

.tab-all { color: #a78bfa; }
.tab-all.active { color: #a78bfa; border-bottom-color: #7c3aed; }

/* ============================== CONTENT ============================== */
.content {
    padding: 20px 24px 40px;
    max-width: 100%;
    overflow-x: auto;
}

/* ============================== STATS BAR ============================== */
.stats-bar {
    display: flex;
    gap: 16px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}

.stat-card {
    background: rgba(30, 30, 50, 0.8);
    border: 1px solid #2d2d44;
    border-radius: 10px;
    padding: 14px 20px;
    min-width: 140px;
    flex: 1;
    max-width: 220px;
}
.stat-card .label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #6b7280; margin-bottom: 4px; }
.stat-card .value { font-size: 22px; font-weight: 700; color: #60a5fa; }
.stat-card .value.green { color: #34d399; }
.stat-card .value.purple { color: #a78bfa; }
.stat-card .value.amber { color: #fbbf24; }

/* ============================== TOGGLE BAR ============================== */
.toggle-bar {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
    padding: 8px 0;
}

.toggle-label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    color: #9ca3af;
    cursor: pointer;
    user-select: none;
}
.toggle-label input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 32px;
    height: 18px;
    background: #374151;
    border-radius: 9px;
    position: relative;
    cursor: pointer;
    transition: background 0.2s ease;
    flex-shrink: 0;
}
.toggle-label input[type="checkbox"]::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    background: #6b7280;
    border-radius: 50%;
    transition: transform 0.2s ease, background 0.2s ease;
}
.toggle-label input[type="checkbox"]:checked {
    background: #3b82f6;
}
.toggle-label input[type="checkbox"]:checked::after {
    transform: translateX(14px);
    background: #fff;
}

.search-box {
    background: #1f2937;
    border: 1px solid #374151;
    color: #e0e0e0;
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 12px;
    width: 220px;
    transition: border-color 0.2s;
}
.search-box:focus { outline: none; border-color: #3b82f6; }
.search-box::placeholder { color: #4b5563; }

/* ============================== TABLE ============================== */
.table-wrapper {
    border-radius: 12px;
    overflow-x: auto;
    overflow-y: visible;
    border: 1px solid #2d2d44;
    background: rgba(20, 20, 35, 0.9);
    max-width: 100%;
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
}

thead th {
    background: #1f2937;
    padding: 10px 14px;
    text-align: center;
    font-weight: 600;
    color: #9ca3af;
    border-bottom: 2px solid #374151;
    position: sticky;
    top: 0;
    z-index: 10;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: background 0.2s;
}
thead th:hover { background: #374151; color: #e0e0e0; }
thead th.sorted-asc::after { content: ' ▲'; font-size: 9px; color: #60a5fa; }
thead th.sorted-desc::after { content: ' ▼'; font-size: 9px; color: #60a5fa; }

thead th:nth-child(2) {
    text-align: left;
    position: sticky;
    left: 0;
    z-index: 11;
    min-width: 200px;
}

thead th.family-header {
    background: #1e293b;
    color: #60a5fa;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 1px;
    border-bottom: 1px solid #3b82f6;
    cursor: default;
}

tbody td {
    padding: 8px 14px;
    text-align: center;
    border-bottom: 1px solid rgba(55, 65, 81, 0.4);
    transition: background 0.15s;
}

tbody tr:hover td { background: rgba(59, 130, 246, 0.06); }
tbody tr:nth-child(even) td { background: rgba(55, 65, 81, 0.15); }
tbody tr:hover:nth-child(even) td { background: rgba(59, 130, 246, 0.08); }

tbody td:nth-child(2) {
    text-align: left;
    font-weight: 600;
    position: sticky;
    left: 0;
    background: rgba(20, 20, 35, 0.98);
    z-index: 5;
}
tbody tr:nth-child(even) td:nth-child(2) { background: rgba(25, 25, 40, 0.98); }
tbody tr:hover td:nth-child(2) { background: rgba(30, 40, 60, 0.98); }

/* Cell coloring */
.cell-best { color: #34d399; font-weight: 700; background: rgba(16, 185, 129, 0.12) !important; }
.cell-good { color: #34d399; }
.cell-mid  { color: #fbbf24; }
.cell-low  { color: #f87171; }
.cell-na   { color: #4b5563; }

/* Model name styling */
.model-name { display: flex; align-items: center; gap: 8px; }
.model-name .badge {
    font-size: 9px;
    padding: 1px 6px;
    border-radius: 4px;
    font-weight: 600;
    text-transform: uppercase;
}
.badge-model { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
.badge-ckpt  { background: rgba(251, 191, 36, 0.2); color: #fbbf24; }

.btn-delete {
    background: transparent;
    border: none;
    color: #6b7280;
    cursor: pointer;
    font-size: 14px;
    padding: 2px 6px;
    border-radius: 4px;
    transition: all 0.2s;
    opacity: 0.5;
}
.btn-delete:hover { color: #ef4444; opacity: 1; background: rgba(239, 68, 68, 0.1); }

.model-name .run-tag {
    font-size: 9px;
    color: #6b7280;
    margin-left: auto;
    font-weight: 400;
}

/* ============================== GRAPH VIEW ============================== */
#graphView {
    display: none;
    position: relative;
    width: 100%;
    height: calc(100vh - 160px);
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid #2d2d44;
    background: rgba(10, 10, 20, 0.95);
}
#graphView svg { width: 100%; height: 100%; }

/* Graph elements */
.link { stroke: #4a5568; stroke-width: 2; stroke-opacity: 0.6; fill: none; }
.link:hover { stroke: #718096; stroke-width: 3; }
.arrow { fill: #4a5568; }

.node-group { cursor: pointer; }
.node-circle {
    stroke: #fff;
    stroke-width: 2;
    filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));
    transition: all 0.2s;
}
.node-group:hover .node-circle {
    stroke-width: 4;
    filter: drop-shadow(0 6px 20px rgba(0,0,0,0.6));
}
.node-circle.selected { stroke: #3b82f6; stroke-width: 4px; }

.node-label {
    font-size: 11px;
    font-weight: 600;
    fill: #fff;
    text-shadow: 0 2px 4px rgba(0,0,0,0.8);
    pointer-events: none;
}

/* ============================== GRAPH TOOLTIP ============================== */
.graph-tooltip {
    position: absolute;
    background: rgba(30, 30, 50, 0.95);
    border: 1px solid #4a5568;
    border-radius: 12px;
    padding: 15px;
    font-size: 12px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s;
    max-width: 450px;
    min-width: 300px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.5);
    backdrop-filter: blur(10px);
    z-index: 2000;
}
.graph-tooltip.show { opacity: 1; pointer-events: auto; }
.graph-tooltip h3 { color: #60a5fa; font-size: 14px; margin-bottom: 8px; }
.graph-tooltip .gt-type {
    display: inline-block;
    font-size: 9px;
    padding: 2px 8px;
    border-radius: 4px;
    font-weight: 600;
    text-transform: uppercase;
    margin-bottom: 8px;
}
.gt-type-model { background: rgba(59,130,246,0.2); color: #60a5fa; }
.gt-type-ckpt  { background: rgba(251,191,36,0.2); color: #fbbf24; }

.graph-tooltip .gt-path {
    font-family: monospace;
    font-size: 10px;
    color: #9ca3af;
    background: rgba(0,0,0,0.3);
    padding: 6px 8px;
    border-radius: 4px;
    margin: 6px 0;
    word-break: break-all;
}
.benchmark-dots {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
    padding: 8px;
    background: rgba(0,0,0,0.2);
    border-radius: 6px;
}
.dot-group {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 10px;
    color: #9ca3af;
}
.dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
}
.dot.has { background: #10b981; }
.dot.no  { background: #374151; }
.graph-tooltip .gt-close {
    position: absolute;
    top: 8px;
    right: 10px;
    background: #374151;
    border: none;
    color: #9ca3af;
    width: 22px;
    height: 22px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
}
.graph-tooltip .gt-close:hover { background: #4b5563; color: #fff; }

/* ── Graph Detail Panel (right sidebar) ── */
.graph-detail-panel {
    position: absolute;
    top: 0; right: 0;
    width: 340px;
    height: 100%;
    background: rgba(17, 20, 33, 0.96);
    backdrop-filter: blur(16px);
    border-left: 1px solid rgba(255,255,255,0.08);
    z-index: 60;
    transform: translateX(100%);
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.graph-detail-panel.open { transform: translateX(0); }
.gdp-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 16px;
    border-bottom: 1px solid rgba(255,255,255,0.06);
    flex-shrink: 0;
}
.gdp-header h3 {
    color: #f1f5f9;
    font-size: 14px;
    font-weight: 600;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
}
.gdp-close {
    background: rgba(255,255,255,0.06);
    border: none;
    color: #9ca3af;
    width: 26px; height: 26px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    flex-shrink: 0;
    transition: all 0.15s;
}
.gdp-close:hover { background: rgba(255,255,255,0.12); color: #fff; }
.gdp-body {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
}
.gdp-section {
    margin-bottom: 18px;
}
.gdp-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: #6b7280;
    margin-bottom: 6px;
    font-weight: 600;
}
.gdp-value {
    font-size: 12px;
    color: #e2e8f0;
    word-break: break-all;
}
.gdp-path {
    font-family: 'SF Mono', 'Fira Code', monospace;
    font-size: 10px;
    color: #9ca3af;
    background: rgba(0,0,0,0.3);
    padding: 8px 10px;
    border-radius: 8px;
    word-break: break-all;
    line-height: 1.5;
}
.gdp-notes {
    width: 100%;
    min-height: 80px;
    background: rgba(0,0,0,0.25);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 10px;
    padding: 10px 12px;
    color: #e2e8f0;
    font-size: 12px;
    font-family: inherit;
    resize: vertical;
    transition: border-color 0.2s;
    line-height: 1.5;
}
.gdp-notes:focus {
    outline: none;
    border-color: rgba(99,102,241,0.5);
}
.gdp-notes::placeholder { color: #4b5563; }
.gdp-save-hint {
    font-size: 9px;
    color: #4b5563;
    margin-top: 4px;
    font-style: italic;
}
.gdp-parent-select {
    width: 100%;
    background: rgba(0,0,0,0.25);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 10px;
    padding: 8px 12px;
    color: #e2e8f0;
    font-size: 12px;
    cursor: pointer;
    transition: border-color 0.2s;
    -webkit-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%236b7280'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
}
.gdp-parent-select:focus {
    outline: none;
    border-color: rgba(99,102,241,0.5);
}
.gdp-parent-select option {
    background: #1a1a2e;
    color: #e2e8f0;
}
.gdp-link-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 8px;
    padding: 6px 12px;
    font-size: 11px;
    font-weight: 600;
    color: #818cf8;
    background: rgba(99,102,241,0.1);
    border: 1px solid rgba(99,102,241,0.2);
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s;
}
.gdp-link-btn:hover { background: rgba(99,102,241,0.2); }
.gdp-link-btn.active {
    background: rgba(99,102,241,0.25);
    border-color: #818cf8;
    color: #a5b4fc;
    animation: linkPulse 1.5s ease-in-out infinite;
}
@keyframes linkPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(99,102,241,0.3); }
    50% { box-shadow: 0 0 0 6px rgba(99,102,241,0); }
}
.gdp-status {
    font-size: 11px;
    padding: 4px 10px;
    border-radius: 6px;
    margin-top: 6px;
    display: none;
}
.gdp-status.success {
    display: block;
    background: rgba(16,185,129,0.1);
    color: #34d399;
}
.gdp-status.error {
    display: block;
    background: rgba(239,68,68,0.1);
    color: #f87171;
}

/* Graph controls box */
.graph-search {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 12px;
    background: rgba(17, 24, 39, 0.85);
    backdrop-filter: blur(8px);
    border: 1px solid #374151;
    border-radius: 10px;
    padding: 6px 14px;
}
.graph-search .search-box { margin: 0; }
.graph-search .toggle-label { white-space: nowrap; }

/* ============================== CELL TOOLTIP ============================== */
.cell-tooltip {
    position: fixed;
    background: rgba(30, 30, 50, 0.97);
    border: 1px solid #4a5568;
    border-radius: 8px;
    padding: 10px 14px;
    font-size: 11px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 2000;
    max-width: 300px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
.cell-tooltip.show { opacity: 1; }
.cell-tooltip .tt-label { color: #9ca3af; font-size: 10px; }
.cell-tooltip .tt-value { color: #60a5fa; font-weight: 600; font-size: 14px; margin-top: 2px; }
.cell-tooltip .tt-rank { color: #6b7280; font-size: 10px; margin-top: 4px; }

/* ============================== EMPTY STATE ============================== */
.empty-state {
    text-align: center;
    padding: 80px 20px;
    color: #4b5563;
}
.empty-state .icon { font-size: 48px; margin-bottom: 16px; }
.empty-state h3 { color: #6b7280; font-size: 16px; margin-bottom: 8px; }
.empty-state p { font-size: 13px; }

/* ============================== LOADING ============================== */
.loading {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 60px;
    gap: 12px;
    color: #6b7280;
}
.spinner {
    width: 20px; height: 20px;
    border: 2px solid #374151;
    border-top-color: #3b82f6;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ============================== SELECTION BAR ============================== */
.selection-bar {
    display: none;
    align-items: center;
    gap: 12px;
    padding: 8px 16px;
    background: #1e293b;
    border: 1px solid #3b82f6;
    border-radius: 8px;
    margin-bottom: 10px;
    font-size: 13px;
    color: #93c5fd;
}
.selection-bar.active { display: flex; }
.selection-bar .sel-count { font-weight: 700; color: #60a5fa; }
.selection-bar .sel-actions { margin-left: auto; display: flex; gap: 8px; }
.selection-bar .sel-btn {
    background: #334155; border: 1px solid #475569; border-radius: 6px;
    color: #e2e8f0; padding: 4px 12px; cursor: pointer; font-size: 12px;
}
.selection-bar .sel-btn:hover { background: #475569; }
.selection-bar .sel-btn.danger { border-color: #ef4444; color: #fca5a5; }
.selection-bar .sel-btn.danger:hover { background: #7f1d1d; }
.selection-bar .sel-btn.primary { border-color: #3b82f6; color: #93c5fd; }
.selection-bar .sel-btn.primary:hover { background: #1e3a5f; }

/* Selection checkbox in table — collapse column to content */
.sel-th, .sel-td { width: 1px; white-space: nowrap; padding: 4px 6px !important; text-align: center; }
.sel-th { cursor: default; }
.sel-th:hover { background: #1f2937 !important; }
.sel-cb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border: 2px solid #4b5563;
    border-radius: 4px;
    background: transparent;
    cursor: pointer;
    vertical-align: middle;
    position: relative;
    transition: background 0.15s ease, border-color 0.15s ease;
    flex-shrink: 0;
}
.sel-cb:checked {
    background: #3b82f6;
    border-color: #3b82f6;
}
.sel-cb:checked::after {
    content: '';
    position: absolute;
    top: 1px;
    left: 4px;
    width: 5px;
    height: 9px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.sel-cb:hover { border-color: #6b7280; }
tr.row-selected { background: rgba(59, 130, 246, 0.12) !important; }
tr.row-selected td:first-child { border-left: 3px solid #3b82f6; }

/* Graph node selection glow */
.node-circle.selected { stroke: #3b82f6; stroke-width: 3px; filter: drop-shadow(0 0 6px rgba(59,130,246,0.6)); }

/* ============================== FILTER DROPDOWN ============================== */
.filter-wrap {
    position: relative;
    display: inline-block;
}
.filter-btn {
    background: #1f2937;
    border: 1px solid #374151;
    color: #e0e0e0;
    padding: 5px 10px;
    border-radius: 6px;
    font-size: 12px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    transition: all 0.2s;
    white-space: nowrap;
}
.filter-btn:hover { border-color: #60a5fa; }
.filter-btn.has-filter { border-color: #3b82f6; background: rgba(59,130,246,0.12); color: #93c5fd; }
.filter-btn .arrow { font-size: 8px; color: #6b7280; }
.filter-btn .count {
    background: #3b82f6;
    color: #fff;
    font-size: 9px;
    padding: 1px 5px;
    border-radius: 10px;
    font-weight: 700;
    min-width: 16px;
    text-align: center;
}

.filter-dd {
    display: none;
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 200;
    background: #1e293b;
    border: 1px solid #374151;
    border-radius: 10px;
    min-width: 240px;
    max-height: 360px;
    box-shadow: 0 12px 40px rgba(0,0,0,0.5);
    overflow: hidden;
}
.filter-dd.open { display: block; }
.filter-dd.right { left: auto; right: 0; }

.filter-dd-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 12px;
    border-bottom: 1px solid #2d2d44;
    flex-wrap: wrap;
}
.filter-dd-header .fq-btn {
    background: #334155;
    border: 1px solid #475569;
    color: #cbd5e1;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 10px;
    cursor: pointer;
    transition: all 0.15s;
}
.filter-dd-header .fq-btn:hover { background: #475569; color: #fff; }
.filter-dd-header .fq-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }

.filter-dd-search {
    width: 100%;
    background: #0f172a;
    border: 1px solid #334155;
    color: #e0e0e0;
    padding: 5px 8px;
    border-radius: 4px;
    font-size: 11px;
    margin-top: 6px;
}
.filter-dd-search::placeholder { color: #4b5563; }
.filter-dd-search:focus { outline: none; border-color: #3b82f6; }

.filter-dd-list {
    max-height: 260px;
    overflow-y: auto;
    padding: 4px 0;
}
.filter-dd-list::-webkit-scrollbar { width: 5px; }
.filter-dd-list::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; }

.filter-dd-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 12px;
    font-size: 12px;
    color: #cbd5e1;
    cursor: pointer;
    transition: background 0.1s;
}
.filter-dd-item:hover { background: rgba(59,130,246,0.08); }
.filter-dd-item input { accent-color: #3b82f6; width: 13px; height: 13px; flex-shrink: 0; cursor: pointer; }
.filter-dd-item .item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ============================== RESPONSIVE ============================== */
@media (max-width: 768px) {
    .header { flex-wrap: wrap; gap: 10px; padding: 10px 16px; }
    .content { padding: 12px 16px; }
    .stats-bar { gap: 10px; }
}

/* ============================== CHIP BAR ============================== */
.customize-area {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 16px;
    padding: 14px 16px;
    background: rgba(30, 30, 50, 0.5);
    border: 1px solid #2d2d44;
    border-radius: 12px;
}
.chip-section {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 36px;
}
.chip-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 1.2px;
    color: #6b7280;
    font-weight: 600;
    min-width: 60px;
}
.chip-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    flex: 1;
    min-height: 28px;
    padding: 4px;
    border-radius: 8px;
    transition: background 0.2s;
}
.chip-bar.drag-over { background: rgba(59, 130, 246, 0.08); }
.chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    background: rgba(59, 130, 246, 0.15);
    border: 1px solid rgba(59, 130, 246, 0.3);
    border-radius: 20px;
    font-size: 11px;
    color: #93c5fd;
    cursor: grab;
    user-select: none;
    transition: all 0.2s;
    white-space: nowrap;
}
.chip:hover {
    background: rgba(59, 130, 246, 0.25);
    border-color: rgba(59, 130, 246, 0.5);
    transform: translateY(-1px);
}
.chip:active, .chip.dragging {
    cursor: grabbing;
    opacity: 0.5;
    transform: scale(0.95);
}
.chip .chip-remove {
    background: none;
    border: none;
    color: #6b7280;
    cursor: pointer;
    font-size: 14px;
    padding: 0 2px;
    line-height: 1;
    transition: color 0.15s;
}
.chip .chip-remove:hover { color: #f87171; }
.chip .chip-fam-tag {
    color: #6b7280;
    font-size: 10px;
}
.chip-model {
    background: rgba(167, 139, 250, 0.15);
    border-color: rgba(167, 139, 250, 0.3);
    color: #c4b5fd;
}
.chip-model:hover {
    background: rgba(167, 139, 250, 0.25);
    border-color: rgba(167, 139, 250, 0.5);
}
.add-chip-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed #4b5563;
    border-radius: 50%;
    color: #6b7280;
    font-size: 16px;
    cursor: pointer;
    transition: all 0.2s;
    flex-shrink: 0;
}
.add-chip-btn:hover {
    background: rgba(59, 130, 246, 0.15);
    border-color: #3b82f6;
    color: #60a5fa;
    transform: rotate(90deg);
}
/* ============================== SAVED FILTERS ============================== */
.saved-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 12px;
    background: rgba(52, 211, 153, 0.12);
    border: 1px solid rgba(52, 211, 153, 0.3);
    border-radius: 20px;
    font-size: 11px;
    color: #6ee7b7;
    cursor: pointer;
    user-select: none;
    transition: all 0.2s;
    white-space: nowrap;
}
.saved-filter-chip:hover {
    background: rgba(52, 211, 153, 0.25);
    border-color: rgba(52, 211, 153, 0.5);
    transform: translateY(-1px);
}
.saved-filter-chip.active {
    background: rgba(52, 211, 153, 0.3);
    border-color: #34d399;
    box-shadow: 0 0 8px rgba(52, 211, 153, 0.2);
}
.saved-filter-chip .sf-remove {
    background: none;
    border: none;
    color: #6b7280;
    cursor: pointer;
    font-size: 14px;
    padding: 0 2px;
    line-height: 1;
    transition: color 0.15s;
}
.saved-filter-chip .sf-remove:hover { color: #f87171; }
.save-filter-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: rgba(52, 211, 153, 0.1);
    border: 1px dashed rgba(52, 211, 153, 0.4);
    border-radius: 50%;
    color: #6ee7b7;
    font-size: 14px;
    cursor: pointer;
    transition: all 0.2s;
    flex-shrink: 0;
}
.save-filter-btn:hover {
    background: rgba(52, 211, 153, 0.25);
    border-color: #34d399;
    transform: scale(1.1);
}

/* Presets + controls row */
.customize-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding-top: 8px;
    border-top: 1px solid rgba(45, 45, 68, 0.5);
}


/* ============================== CHARTS VIEW ============================== */
.charts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
    grid-auto-rows: 380px;
    gap: 16px;
}
.chart-card {
    background: rgba(22, 22, 38, 0.85);
    backdrop-filter: blur(12px);
    border: 1px solid rgba(255,255,255,0.06);
    border-radius: 16px;
    overflow: visible;
    position: relative;
    display: flex;
    flex-direction: column;
    transition: box-shadow 0.3s, border-color 0.3s, grid-column 0.3s, grid-row 0.3s;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3), 0 0 0 0 rgba(59,130,246,0);
}
.chart-card:hover {
    border-color: rgba(255,255,255,0.12);
    box-shadow: 0 4px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(59,130,246,0.15);
}
.chart-card.size-1x1 { grid-column: span 1; grid-row: span 1; }
.chart-card.size-2x1 { grid-column: span 2; grid-row: span 1; }
.chart-card.size-1x2 { grid-column: span 1; grid-row: span 2; }
.chart-card.size-2x2 { grid-column: span 2; grid-row: span 2; }
.chart-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 14px;
    border-bottom: 1px solid rgba(255,255,255,0.05);
    flex-shrink: 0;
}
.chart-card-header h4 {
    color: #f1f5f9;
    font-size: 13px;
    font-weight: 600;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    letter-spacing: -0.01em;
}
.chart-card-header .chart-type-badge {
    font-size: 9px;
    padding: 2px 7px;
    border-radius: 6px;
    background: rgba(99, 102, 241, 0.15);
    color: #818cf8;
    text-transform: uppercase;
    font-weight: 700;
    letter-spacing: 0.5px;
}
.chart-card-actions {
    display: flex;
    gap: 4px;
}
.chart-card-actions button {
    background: rgba(255,255,255,0.04);
    border: 1px solid rgba(255,255,255,0.08);
    color: #6b7280;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 12px;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
}
.chart-card-actions button:hover { background: rgba(255,255,255,0.1); color: #e2e8f0; }
.chart-card-actions button.danger:hover { background: rgba(239,68,68,0.15); color: #f87171; }
.chart-card-body {
    flex: 1;
    padding: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    min-height: 0;
}
.chart-card-body svg { width: 100%; height: 100%; }

/* ── Chart table (table/pivot type) ── */
.chart-table-wrap {
    width: 100%; height: 100%; overflow: auto;
    scrollbar-width: thin; scrollbar-color: #374151 transparent;
}
.chart-table-wrap table {
    border-collapse: collapse; width: 100%; font-size: 11px;
}
.chart-table-wrap th {
    position: sticky; top: 0; z-index: 2;
    background: #1e293b; color: #94a3b8; font-weight: 600; text-transform: uppercase;
    font-size: 9px; letter-spacing: 0.5px; padding: 6px 10px;
    border-bottom: 2px solid #334155; text-align: left; white-space: nowrap;
}
.chart-table-wrap td {
    padding: 5px 10px; color: #cbd5e1; border-bottom: 1px solid rgba(255,255,255,0.04);
    white-space: nowrap;
}
.chart-table-wrap tr:hover td { background: rgba(59,130,246,0.06); }
.chart-table-wrap .ct-best { color: #34d399; font-weight: 600; }
.chart-table-wrap .ct-model { color: #e2e8f0; font-weight: 500; }
.chart-table-wrap .ct-pivot-hdr {
    background: rgba(99,102,241,0.08); color: #a78bfa; font-weight: 600;
    font-size: 10px; padding: 4px 10px;
}

/* ── Drag grip (top of card) ── */
.chart-drag-grip {
    position: absolute;
    top: 0; left: 50%; transform: translateX(-50%);
    width: 40px; height: 14px;
    cursor: grab;
    z-index: 6;
    display: flex; align-items: center; justify-content: center;
    opacity: 0; transition: opacity 0.25s;
}
.chart-card:hover .chart-drag-grip { opacity: 1; }
.chart-drag-grip::after {
    content: '';
    width: 28px; height: 4px;
    background: #4b5563; border-radius: 2px;
    transition: background 0.2s;
}
.chart-drag-grip:hover::after { background: #60a5fa; }
.chart-drag-grip:active { cursor: grabbing; }
.chart-card.dragging-card {
    opacity: 0.5; transform: scale(0.97);
    transition: opacity 0.15s, transform 0.15s;
}
.chart-card.drop-target {
    border-color: #3b82f6;
    box-shadow: 0 0 0 2px rgba(59,130,246,0.3), 0 4px 24px rgba(0,0,0,0.4);
}

/* ── Resize grip ── */
.chart-resize-grip {
    position: absolute;
    bottom: 0; right: 0;
    width: 24px; height: 24px;
    cursor: nwse-resize;
    z-index: 5;
    border-radius: 0 0 16px 0;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.25s;
}
.chart-card:hover .chart-resize-grip { opacity: 1; }
.chart-resize-grip svg {
    width: 12px; height: 12px;
    pointer-events: none;
}
.chart-resize-grip:active { opacity: 1 !important; }

/* ── Size preview overlay during drag ── */
.chart-size-toast {
    position: absolute;
    bottom: 8px; right: 32px;
    background: rgba(0,0,0,0.7);
    backdrop-filter: blur(8px);
    color: #e2e8f0;
    font-size: 11px;
    font-weight: 600;
    padding: 3px 10px;
    border-radius: 8px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.chart-size-toast.visible { opacity: 1; }

.charts-empty {
    grid-column: 1 / -1;
    text-align: center;
    padding: 80px 20px;
    color: #4b5563;
}
.charts-empty .icon { font-size: 48px; margin-bottom: 16px; }
.charts-empty h3 { color: #6b7280; font-size: 16px; margin-bottom: 8px; }
.charts-empty p { font-size: 13px; }

/* ── Chart Builder Modal ────────────── */
.cb-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 600;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(4px);
    align-items: center;
    justify-content: center;
}
.cb-overlay.open { display: flex; }
.cb-modal {
    background: #1e293b;
    border: 1px solid #334155;
    border-radius: 14px;
    width: 720px;
    max-width: 95vw;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    animation: cbFadeIn 0.2s ease-out;
}
@keyframes cbFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.cb-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 18px 24px;
    border-bottom: 1px solid #334155;
}
.cb-header h3 { color: #e2e8f0; font-size: 16px; margin: 0; }
.cb-body {
    padding: 20px 24px;
    overflow-y: auto;
    flex: 1;
}
.cb-row {
    margin-bottom: 18px;
}
.cb-label {
    display: block;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #6b7280;
    font-weight: 600;
    margin-bottom: 8px;
}
.cb-type-group {
    display: flex;
    gap: 10px;
}
.cb-type-btn {
    flex: 1;
    padding: 12px;
    background: #0f172a;
    border: 2px solid #334155;
    border-radius: 10px;
    color: #9ca3af;
    cursor: pointer;
    text-align: center;
    transition: all 0.2s;
}
.cb-type-btn:hover { border-color: #60a5fa; color: #e0e0e0; }
.cb-type-btn.selected { border-color: #3b82f6; background: rgba(59, 130, 246, 0.1); color: #93c5fd; }
.cb-type-btn .cb-type-icon { font-size: 24px; display: block; margin-bottom: 4px; }
.cb-type-btn .cb-type-name { font-size: 12px; font-weight: 600; }
.cb-picker {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 4px;
    max-height: 180px;
    overflow-y: auto;
    background: #0f172a;
    border: 1px solid #334155;
    border-radius: 8px;
    padding: 8px;
}
.cb-picker::-webkit-scrollbar { width: 4px; }
.cb-picker::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; }
.cb-pick-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
    color: #cbd5e1;
    transition: background 0.1s;
}
.cb-pick-item:hover { background: rgba(59, 130, 246, 0.08); }
.cb-pick-item input { accent-color: #3b82f6; width: 14px; height: 14px; cursor: pointer; }
.cb-pick-group-hdr {
    grid-column: 1 / -1;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #60a5fa;
    padding: 6px 8px 2px;
    font-weight: 600;
}
.cb-name-input {
    width: 100%;
    background: #0f172a;
    border: 1px solid #334155;
    color: #e0e0e0;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 13px;
}
.cb-name-input:focus { outline: none; border-color: #3b82f6; }
.cb-name-input::placeholder { color: #4b5563; }
.cb-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 16px 24px;
    border-top: 1px solid #334155;
}
.cb-footer .btn { padding: 8px 20px; font-size: 13px; }

/* ============================== BUILDER PANEL ============================== */
.builder-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 500;
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
}
.builder-overlay.open { display: flex; justify-content: flex-end; }
.builder-panel {
    width: 340px;
    height: 100%;
    background: #1e293b;
    border-left: 1px solid #374151;
    display: flex;
    flex-direction: column;
    box-shadow: -8px 0 40px rgba(0, 0, 0, 0.4);
    animation: slideIn 0.25s ease-out;
}
@keyframes slideIn {
    from { transform: translateX(100%); }
    to { transform: translateX(0); }
}
.builder-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px;
    border-bottom: 1px solid #334155;
}
.builder-header h3 { color: #e2e8f0; font-size: 15px; margin: 0; }
.builder-close {
    background: #334155;
    border: none;
    color: #9ca3af;
    width: 28px;
    height: 28px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
    transition: all 0.15s;
}
.builder-close:hover { background: #475569; color: #fff; }
.builder-search {
    margin: 12px 16px;
    padding: 8px 12px;
    background: #0f172a;
    border: 1px solid #334155;
    color: #e0e0e0;
    border-radius: 8px;
    font-size: 12px;
    width: calc(100% - 32px);
}
.builder-search:focus { outline: none; border-color: #3b82f6; }
.builder-search::placeholder { color: #4b5563; }
.builder-actions {
    display: flex;
    gap: 8px;
    padding: 0 16px 12px;
}
.builder-act-btn {
    background: #334155;
    border: 1px solid #475569;
    color: #cbd5e1;
    padding: 4px 12px;
    border-radius: 6px;
    font-size: 11px;
    cursor: pointer;
    transition: all 0.15s;
}
.builder-act-btn:hover { background: #475569; color: #fff; }
.builder-list {
    flex: 1;
    overflow-y: auto;
    padding: 0 12px 16px;
}
.builder-list::-webkit-scrollbar { width: 5px; }
.builder-list::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; }
.builder-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 4px;
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid transparent;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.15s;
}
.builder-item:hover {
    background: rgba(59, 130, 246, 0.08);
    border-color: rgba(59, 130, 246, 0.2);
}
.builder-item.active {
    background: rgba(59, 130, 246, 0.12);
    border-color: rgba(59, 130, 246, 0.3);
}
.builder-item .bi-check {
    width: 18px;
    height: 18px;
    border: 2px solid #4b5563;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    color: transparent;
    transition: all 0.15s;
    flex-shrink: 0;
}
.builder-item.active .bi-check {
    background: #3b82f6;
    border-color: #3b82f6;
    color: #fff;
}
.builder-item .bi-name { font-size: 13px; color: #e2e8f0; flex: 1; }
.builder-item .bi-count { font-size: 10px; color: #6b7280; }

/* Drag indicator */
.chip-drop-indicator {
    width: 2px;
    height: 24px;
    background: #3b82f6;
    border-radius: 1px;
    flex-shrink: 0;
}
</style>
</head>
<body>

<!-- ==================== HEADER ==================== -->
<div class="header">
    <div class="logo">
        <span class="icon">⚡</span>
        <span>LightML<span class="sub">Dashboard</span></span>
    </div>
    <div class="header-controls">
        <div class="view-toggle">
            <button class="vt-btn active" id="btnTableView" onclick="switchView('table')">📊 Table</button>
            <button class="vt-btn" id="btnGraphView" onclick="switchView('graph')">🔗 Graph</button>
            <button class="vt-btn" id="btnChartsView" onclick="switchView('charts')">📈 Charts</button>
        </div>
        <span class="db-badge" id="dbBadge">—</span>
        <select id="runSelect" title="Filter by run">
            <option value="">All runs</option>
        </select>
        <button class="btn btn-icon" onclick="refresh()" title="Refresh data">↻ Refresh</button>
        <button class="btn btn-icon" onclick="exportExcel()" title="Export as Excel">⬇ Excel</button>
    </div>
</div>

<!-- ==================== FAMILY TABS ==================== -->
<div class="tabs-bar" id="tabsBar"></div>

<!-- ==================== CONTENT ==================== -->
<div class="content">
    <!-- Stats -->
    <div class="stats-bar" id="statsBar"></div>

    <!-- Selection Bar -->
    <div class="selection-bar" id="selectionBar">
        <span><span class="sel-count" id="selCount">0</span> models selected</span>
        <div class="sel-actions">
            <button class="sel-btn" onclick="filterToSelected()">Filter table</button>
            <button class="sel-btn primary" onclick="compareSelected()">Compare</button>
            <button class="sel-btn danger" onclick="clearSelection()">Clear</button>
        </div>
    </div>

    <!-- Table View -->
    <div id="tableView">
        <!-- Customize Area: Chip bars + controls -->
        <div class="customize-area" id="customizeArea">
            <div class="chip-section" id="metricChipSection">
                <span class="chip-label">Metrics</span>
                <div class="chip-bar" id="metricChipBar"></div>
                <button class="add-chip-btn" onclick="openBuilder('metric')" title="Add/remove metrics">+</button>
            </div>
            <div class="chip-section">
                <span class="chip-label">Models</span>
                <div class="chip-bar" id="modelChipBar"></div>
                <button class="add-chip-btn" onclick="openBuilder('model')" title="Add/remove models">+</button>
                <button class="save-filter-btn" onclick="saveCurrentFilter()" title="Save current model filter">💾</button>
            </div>
            <div class="chip-section" id="savedFilterSection" style="display:none;">
                <span class="chip-label">Filters</span>
                <div class="chip-bar" id="savedFilterBar"></div>
            </div>
            <div class="customize-controls">
                <input class="search-box" type="text" id="searchBox" placeholder="Search models..." oninput="renderTable()" style="width:160px;">
                <label class="toggle-label">
                    <input type="checkbox" id="showCheckpoints" onchange="renderTable()">
                    Checkpoints
                </label>
                <label class="toggle-label">
                    <input type="checkbox" id="highlightBest" checked onchange="renderTable()">
                    Best
                </label>
                <label class="toggle-label">
                    <input type="checkbox" id="colorCells" checked onchange="renderTable()">
                    Colors
                </label>
                <label class="toggle-label">
                    <input type="checkbox" id="showHiddenTable" onchange="renderTable()">
                    Hidden
                </label>
            </div>
        </div>
        <div id="tableContainer">
            <div class="loading"><div class="spinner"></div> Loading data…</div>
        </div>
    </div>

    <!-- Graph View -->
    <div id="graphView">
        <div class="graph-search">
            <input class="search-box" type="text" id="graphSearchBox" placeholder="Search nodes..." oninput="filterGraphNodes()">
            <label class="toggle-label">
                <input type="checkbox" id="graphShowCheckpoints" onchange="if(GRAPH) renderGraph(GRAPH);">
                Show checkpoints
            </label>
            <label class="toggle-label">
                <input type="checkbox" id="graphShowHidden" onchange="if(GRAPH) renderGraph(GRAPH);">
                Show hidden
            </label>
        </div>
        <svg id="graphSvg"></svg>
        <div class="graph-tooltip" id="graphTooltip"></div>
        <!-- Detail Panel -->
        <div class="graph-detail-panel" id="graphDetailPanel">
            <div class="gdp-header">
                <h3 id="gdpTitle"></h3>
                <button class="gdp-close" onclick="closeDetailPanel()">&#x2715;</button>
            </div>
            <div class="gdp-body">
                <div class="gdp-section">
                    <div class="gdp-label">Type &amp; Run</div>
                    <div class="gdp-value" id="gdpTypeRun"></div>
                </div>
                <div class="gdp-section">
                    <div class="gdp-label">Path</div>
                    <div class="gdp-path" id="gdpPath"></div>
                </div>
                <div class="gdp-section">
                    <div class="gdp-label">Benchmarks</div>
                    <div id="gdpBenchmarks"></div>
                </div>
                <div class="gdp-section" id="gdpParentSection">
                    <div class="gdp-label">Parent Model</div>
                    <select class="gdp-parent-select" id="gdpParentSelect" onchange="onParentChange()"></select>
                    <button class="gdp-link-btn" id="gdpLinkBtn" onclick="toggleLinkMode()">
                        🔗 Click a node to set as parent
                    </button>
                    <div class="gdp-status" id="gdpParentStatus"></div>
                </div>
                <div class="gdp-section" id="gdpNotesSection">
                    <div class="gdp-label">Notes</div>
                    <textarea class="gdp-notes" id="gdpNotes" placeholder="Add notes about this model..."></textarea>
                    <button class="gdp-link-btn" onclick="saveNotes()" style="margin-top:6px;">💾 Save Notes</button>
                    <div class="gdp-status" id="gdpNotesStatus"></div>
                </div>
                <div class="gdp-section" id="gdpHideSection">
                    <button class="gdp-link-btn" id="gdpHideBtn" onclick="toggleHideModel()" style="margin-top:4px;">👁 Hide this model</button>
                </div>
            </div>
        </div>
    </div>

    <!-- Charts View -->
    <div id="chartsView" style="display:none;">
        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
            <h2 style="font-size:18px;color:#e2e8f0;font-weight:600;">Saved Charts</h2>
            <button class="btn btn-primary btn-icon" onclick="openChartBuilder()">＋ New Chart</button>
        </div>
        <div id="chartsGrid" class="charts-grid"></div>
    </div>
</div>

<!-- ==================== TABLE TOOLTIP ==================== -->
<div class="cell-tooltip" id="tooltip"></div>

<!-- ==================== CHART BUILDER MODAL ==================== -->
<div class="cb-overlay" id="cbOverlay" onclick="if(event.target===this) closeChartBuilder()">
    <div class="cb-modal">
        <div class="cb-header">
            <h3>New Chart</h3>
            <button class="builder-close" onclick="closeChartBuilder()">&#x2715;</button>
        </div>
        <div class="cb-body">
            <div class="cb-row">
                <span class="cb-label">Chart Name</span>
                <input class="cb-name-input" type="text" id="cbName" placeholder="e.g. ENG Benchmarks Overview">
            </div>
            <div class="cb-row">
                <span class="cb-label">Chart Type</span>
                <div class="cb-type-group" id="cbTypeGroup">
                    <div class="cb-type-btn selected" data-type="radar" onclick="selectChartType('radar')">
                        <span class="cb-type-icon">🕸️</span>
                        <span class="cb-type-name">Radar</span>
                    </div>
                    <div class="cb-type-btn" data-type="bar" onclick="selectChartType('bar')">
                        <span class="cb-type-icon">📊</span>
                        <span class="cb-type-name">Bar</span>
                    </div>
                    <div class="cb-type-btn" data-type="hbar" onclick="selectChartType('hbar')">
                        <span class="cb-type-icon">📉</span>
                        <span class="cb-type-name">Horizontal Bar</span>
                    </div>
                    <div class="cb-type-btn" data-type="table" onclick="selectChartType('table')">
                        <span class="cb-type-icon">📋</span>
                        <span class="cb-type-name">Table</span>
                    </div>
                    <div class="cb-type-btn" data-type="pivot" onclick="selectChartType('pivot')">
                        <span class="cb-type-icon">🔄</span>
                        <span class="cb-type-name">Pivot</span>
                    </div>
                </div>
            </div>
            <div class="cb-row">
                <span class="cb-label">Models <small style="color:#4b5563;text-transform:none;letter-spacing:0;">(select 2+)</small></span>
                <div class="cb-picker" id="cbModelPicker"></div>
            </div>
            <div class="cb-row">
                <span class="cb-label">Metrics <small style="color:#4b5563;text-transform:none;letter-spacing:0;">(select 3+ for radar)</small></span>
                <div class="cb-picker" id="cbMetricPicker"></div>
            </div>
        </div>
        <div class="cb-footer">
            <button class="btn" onclick="closeChartBuilder()">Cancel</button>
            <button class="btn btn-primary" onclick="saveChart()">Save Chart</button>
        </div>
    </div>
</div>

<!-- ==================== BUILDER PANEL ==================== -->
<div class="builder-overlay" id="builderOverlay" onclick="if(event.target===this) closeBuilder()">
    <div class="builder-panel">
        <div class="builder-header">
            <h3 id="builderTitle">Select Columns</h3>
            <button class="builder-close" onclick="closeBuilder()">&#x2715;</button>
        </div>
        <input class="builder-search" type="text" id="builderSearch" placeholder="Search..." oninput="filterBuilderItems()">
        <div class="builder-actions">
            <button class="builder-act-btn" onclick="builderSelectAll()">Select All</button>
            <button class="builder-act-btn" onclick="builderSelectNone()">None</button>
        </div>
        <div class="builder-list" id="builderList"></div>
    </div>
</div>

<script>
/* ======================================================================
   STATE
   ====================================================================== */
let DATA = null;
let TABLE = null;
let GRAPH = null;
let activeFamily = null;
let sortCol = null;
let sortDir = 'desc';
let currentView = 'table';
let graphSimulation = null;

// ── Graph detail panel state ─────────────────
let detailNode = null;       // currently selected node data
let linkMode = false;        // "click to set parent" mode

// ── Selection sync state ─────────────────────
const selectedModels = new Set();
let filterBySelection = false;

// ── Filter state ─────────────────────────────
const visibleFamilies = new Set();
const visibleModels   = new Set();
const visibleMetrics  = new Set();
let allMetricKeys     = [];          // all family/metric keys from DATA
let columnOrder       = [];          // ordered list of visible metric keys
let builderType       = null;

/* ======================================================================
   VIEW SWITCH
   ====================================================================== */
function switchView(view) {
    currentView = view;
    document.getElementById('tableView').style.display = view === 'table' ? 'block' : 'none';
    document.getElementById('graphView').style.display = view === 'graph' ? 'block' : 'none';
    document.getElementById('chartsView').style.display = view === 'charts' ? 'block' : 'none';
    document.getElementById('btnTableView').classList.toggle('active', view === 'table');
    document.getElementById('btnGraphView').classList.toggle('active', view === 'graph');
    document.getElementById('btnChartsView').classList.toggle('active', view === 'charts');

    if (view === 'graph' && !GRAPH) loadGraph();
    if (view === 'charts') renderSavedCharts();
}

/* ======================================================================
   INIT
   ====================================================================== */
document.addEventListener('DOMContentLoaded', () => refresh());

async function refresh() {
    try {
        const resp = await fetch('/api/data');
        DATA = await resp.json();
        document.getElementById('dbBadge').textContent = DATA.db_name;
        populateRuns();
        populateTabs();
        initFilters();
        renderSavedFilters();
        renderStats();
        await loadTable();

        if (currentView === 'graph') { GRAPH = null; loadGraph(); }
    } catch (e) {
        document.getElementById('tableContainer').innerHTML =
            `<div class="empty-state"><div class="icon">⚠️</div><h3>Connection Error</h3><p>${e.message}</p></div>`;
    }
}

/* ======================================================================
   RUNS
   ====================================================================== */
function populateRuns() {
    const sel = document.getElementById('runSelect');
    const current = sel.value;
    sel.innerHTML = '<option value="">All runs</option>';
    for (const r of DATA.runs) {
        const opt = document.createElement('option');
        opt.value = r.id;
        opt.textContent = r.run_name;
        sel.appendChild(opt);
    }
    sel.value = current;
    sel.onchange = () => loadTable();
}

/* ======================================================================
   TABS
   ====================================================================== */
function populateTabs() {
    const bar = document.getElementById('tabsBar');
    bar.innerHTML = '';

    const allTab = document.createElement('button');
    allTab.className = 'tab tab-all' + (activeFamily === null ? ' active' : '');
    allTab.textContent = '✦ All Families';
    allTab.onclick = () => { activeFamily = null; sortCol = null; populateTabs(); loadTable(); };
    bar.appendChild(allTab);

    for (const f of DATA.families) {
        const tab = document.createElement('button');
        tab.className = 'tab' + (activeFamily === f ? ' active' : '');
        tab.textContent = f;
        tab.onclick = () => { activeFamily = f; sortCol = null; populateTabs(); loadTable(); };
        bar.appendChild(tab);
    }
}

/* ======================================================================
   STATS
   ====================================================================== */
function renderStats() {
    const bar = document.getElementById('statsBar');
    const hiddenIds = new Set(DATA.models.filter(m => m.hidden).map(m => m.id));
    const visibleModels = DATA.models.length - hiddenIds.size;
    const visibleCkpts = DATA.checkpoints.filter(c => !hiddenIds.has(c.model_id)).length;
    const visibleMetrics = DATA.metrics.filter(me => {
        if (me.model_id && hiddenIds.has(me.model_id)) return false;
        if (me.checkpoint_id) {
            const ck = DATA.checkpoints.find(c => c.id === me.checkpoint_id);
            if (ck && hiddenIds.has(ck.model_id)) return false;
        }
        return true;
    }).length;
    const stats = [
        { label: 'Runs',        value: DATA.runs.length,   cls: '' },
        { label: 'Models',      value: visibleModels,      cls: 'green' },
        { label: 'Checkpoints', value: visibleCkpts,       cls: 'amber' },
        { label: 'Metrics',     value: visibleMetrics,     cls: 'purple' },
        { label: 'Families',    value: DATA.families.length, cls: '' },
    ];
    bar.innerHTML = stats.map(s => `
        <div class="stat-card">
            <div class="label">${s.label}</div>
            <div class="value ${s.cls}">${s.value}</div>
        </div>
    `).join('');
}

/* ======================================================================
   LOAD TABLE DATA
   ====================================================================== */
async function loadTable() {
    // Toggle metric chip visibility (hide when single-family tab is active)
    const metricSection = document.getElementById('metricChipSection');
    if (metricSection) metricSection.style.display = activeFamily ? 'none' : '';

    const runId = document.getElementById('runSelect').value;
    let url = '/api/table?';
    if (activeFamily) url += `family=${encodeURIComponent(activeFamily)}&`;
    if (runId) url += `run_id=${runId}&`;

    const resp = await fetch(url);
    TABLE = await resp.json();
    renderTable();
}

/* ======================================================================
   RENDER TABLE
   ====================================================================== */
function renderTable() {
    const container = document.getElementById('tableContainer');
    if (!TABLE || !TABLE.rows.length) {
        container.innerHTML = `
            <div class="empty-state">
                <div class="icon">📊</div>
                <h3>No data yet</h3>
                <p>Register models and log metrics to see them here.</p>
            </div>`;
        return;
    }

    const showCkpt   = document.getElementById('showCheckpoints').checked;
    const showHidden  = document.getElementById('showHiddenTable').checked;
    const highlight   = document.getElementById('highlightBest').checked;
    const colorize    = document.getElementById('colorCells').checked;
    const search      = document.getElementById('searchBox').value.toLowerCase();

    let rows = TABLE.rows.filter(r => {
        if (!showCkpt && r.type === 'checkpoint') return false;
        if (!showHidden && r.hidden) return false;
        if (search && !r.name.toLowerCase().includes(search)) return false;
        // For checkpoints, match against the parent model_name
        const matchName = r.type === 'checkpoint' ? (r.model_name || r.name) : r.name;
        if (filterBySelection && selectedModels.size > 0 && !selectedModels.has(matchName)) return false;
        // Model filter: hide rows whose model is deselected
        if (visibleModels.size > 0 && !visibleModels.has(matchName)) return false;
        return true;
    });

    let families;
    if (activeFamily) {
        families = [{ name: activeFamily, metrics: TABLE.metric_names }];
    } else {
        // Build from visible metrics (individual metric-level selection)
        const fm = {};
        const visibleSet = columnOrder.length ? new Set(columnOrder) : null;
        for (const key of (columnOrder.length ? columnOrder : allMetricKeys)) {
            const slashIdx = key.indexOf('/');
            if (slashIdx < 0) continue;
            const fam = key.substring(0, slashIdx);
            if (!fm[fam]) fm[fam] = [];
            fm[fam].push(key);
        }
        // Preserve family ordering from columnOrder
        const seenFamilies = [];
        const seenSet = new Set();
        for (const key of (columnOrder.length ? columnOrder : allMetricKeys)) {
            const fam = key.substring(0, key.indexOf('/'));
            if (!seenSet.has(fam)) { seenSet.add(fam); seenFamilies.push(fam); }
        }
        families = seenFamilies.filter(f => fm[f]).map(name => ({
            name,
            metrics: fm[name]
        }));
    }

    const allMetrics = families.flatMap(f => f.metrics);

    if (sortCol && allMetrics.includes(sortCol)) {
        rows.sort((a, b) => {
            const va = a.metrics[sortCol] ?? -Infinity;
            const vb = b.metrics[sortCol] ?? -Infinity;
            return sortDir === 'asc' ? va - vb : vb - va;
        });
    }

    const best = {};
    if (highlight) {
        for (const metric of allMetrics) {
            let maxVal = -Infinity;
            for (const r of rows) {
                const v = r.metrics[metric];
                if (v != null && v > maxVal) maxVal = v;
            }
            if (maxVal > -Infinity) best[metric] = maxVal;
        }
    }

    const ranges = {};
    if (colorize) {
        for (const metric of allMetrics) {
            let min = Infinity, max = -Infinity;
            for (const r of rows) {
                const v = r.metrics[metric];
                if (v != null) { if (v < min) min = v; if (v > max) max = v; }
            }
            if (min < Infinity) ranges[metric] = { min, max };
        }
    }

    let html = '<div class="table-wrapper"><table>';

    if (!activeFamily && families.length > 1) {
        html += '<thead><tr><th></th><th></th><th></th>';
        for (const f of families) {
            html += `<th colspan="${f.metrics.length}" class="family-header">${esc(f.name)}</th>`;
        }
        html += '</tr>';
    } else {
        html += '<thead>';
    }

    html += '<tr><th class="sel-th"><input type="checkbox" class="sel-cb" id="selectAllCb" onchange="toggleSelectAll(this.checked)"></th><th onclick="toggleSort(\'__name__\')">Model</th>';
    html += '<th style="width:36px"></th>';
    for (const f of families) {
        for (const m of f.metrics) {
            const sortCls = sortCol === m ? (sortDir === 'asc' ? 'sorted-asc' : 'sorted-desc') : '';
            // Display only metric name (strip family/ prefix if composite key)
            const displayName = m.includes('/') ? m.split('/').slice(1).join('/') : m;
            html += `<th class="${sortCls}" onclick="toggleSort('${esc(m)}')">${esc(displayName)}</th>`;
        }
    }
    html += '</tr></thead><tbody>';

    for (const r of rows) {
        const isSelected = selectedModels.has(r.name);
        html += `<tr class="${isSelected ? 'row-selected' : ''}"`;
        html += ` data-model-name="${esc(r.name)}">`;
        html += `<td class="sel-td"><input type="checkbox" class="sel-cb" ${isSelected ? 'checked' : ''} onchange="toggleModelSelection('${esc(r.name)}', this.checked)"></td>`;
        const badge = r.type === 'model'
            ? '<span class="badge badge-model">M</span>'
            : '<span class="badge badge-ckpt">C</span>';
        const runTag = r.run ? `<span class="run-tag">${esc(r.run)}</span>` : '';
        html += `<td><div class="model-name" ${r.notes ? `title="${esc(r.notes)}"` : ''}>${badge} ${esc(r.name)} ${runTag}${r.notes ? ' <span style="color:#6b7280;font-size:10px;">📝</span>' : ''}</div></td>`;
        if (r.type === 'model') {
            html += `<td><button class="btn-delete" title="Delete model" onclick="event.stopPropagation(); deleteModel('${esc(r.name)}')">🗑️</button></td>`;
        } else {
            html += '<td></td>';
        }

        for (const f of families) {
            for (const m of f.metrics) {
                const v = r.metrics[m];
                if (v == null) {
                    html += '<td class="cell-na">—</td>';
                } else {
                    let cls = '';
                    if (highlight && best[m] === v) cls = 'cell-best';
                    else if (colorize && ranges[m]) cls = colorClass(v, ranges[m]);
                    html += `<td class="${cls}" data-metric="${esc(m)}" data-value="${v}" data-model="${esc(r.name)}">${formatVal(v)}</td>`;
                }
            }
        }
        html += '</tr>';
    }
    html += '</tbody></table></div>';
    container.innerHTML = html;

    container.querySelectorAll('td[data-metric]').forEach(td => {
        td.addEventListener('mouseenter', showTooltip);
        td.addEventListener('mouseleave', hideTooltip);
    });
}

/* ======================================================================
   DELETE MODEL
   ====================================================================== */
async function deleteModel(modelName) {
    if (!confirm(`Delete model "${modelName}" and all its checkpoints and metrics?`)) return;

    try {
        const resp = await fetch(`/api/models/${encodeURIComponent(modelName)}`, { method: 'DELETE' });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            alert(err.detail || `Delete failed (${resp.status})`);
            return;
        }
        const result = await resp.json();
        // Refresh data
        selectedModels.delete(modelName);
        await refresh();
    } catch (e) {
        alert(`Error: ${e.message}`);
    }
}

/* ======================================================================
   SORTING
   ====================================================================== */
function toggleSort(col) {
    if (col === '__name__') {
        if (sortCol === '__name__') sortDir = sortDir === 'asc' ? 'desc' : 'asc';
        else { sortCol = '__name__'; sortDir = 'asc'; }
        TABLE.rows.sort((a, b) => {
            const cmp = a.name.localeCompare(b.name);
            return sortDir === 'asc' ? cmp : -cmp;
        });
        sortCol = '__name__';
        renderTable();
        return;
    }
    if (sortCol === col) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
    else { sortCol = col; sortDir = 'desc'; }
    renderTable();
}

/* ======================================================================
   COLOR HELPERS
   ====================================================================== */
function colorClass(value, range) {
    if (range.max === range.min) return '';
    const pct = (value - range.min) / (range.max - range.min);
    if (pct >= 0.75) return 'cell-good';
    if (pct >= 0.4)  return 'cell-mid';
    return 'cell-low';
}

function formatVal(v) {
    if (v == null) return '—';
    if (v > 0 && v <= 1) return (v * 100).toFixed(2) + '%';
    if (Number.isInteger(v)) return v.toString();
    return v.toFixed(2);
}

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

/* ======================================================================
   TABLE TOOLTIP
   ====================================================================== */
function showTooltip(e) {
    const td = e.currentTarget;
    const tt = document.getElementById('tooltip');
    tt.innerHTML = `
        <div class="tt-label">${esc(td.dataset.model)}</div>
        <div class="tt-value">${esc(td.dataset.metric)}: ${formatVal(parseFloat(td.dataset.value))}</div>
        <div class="tt-rank">Raw: ${td.dataset.value}</div>
    `;
    tt.classList.add('show');
    const rect = td.getBoundingClientRect();
    tt.style.left = rect.left + rect.width / 2 - tt.offsetWidth / 2 + 'px';
    tt.style.top  = rect.top - tt.offsetHeight - 8 + 'px';
}

function hideTooltip() {
    document.getElementById('tooltip').classList.remove('show');
}

/* ======================================================================
   EXCEL EXPORT (server-side via /api/export)
   ====================================================================== */
function exportExcel() {
    window.location.href = '/api/export';
}

/* ======================================================================
   SELECTION SYNC (graph ↔ table)
   ====================================================================== */
function toggleModelSelection(name, checked) {
    if (checked) selectedModels.add(name);
    else selectedModels.delete(name);
    syncSelectionUI();
}

function toggleSelectAll(checked) {
    if (!TABLE) return;
    for (const r of TABLE.rows) {
        if (checked) selectedModels.add(r.name);
        else selectedModels.delete(r.name);
    }
    syncSelectionUI();
}

function clearSelection() {
    selectedModels.clear();
    filterBySelection = false;
    syncSelectionUI();
}

function filterToSelected() {
    if (selectedModels.size === 0) return;
    filterBySelection = !filterBySelection;
    // Auto-enable "Show checkpoints" when filtering so user can see them
    if (filterBySelection) {
        document.getElementById('showCheckpoints').checked = true;
    }
    renderTable();
}

async function compareSelected() {
    const names = [...selectedModels];
    if (names.length < 2) { alert('Select at least 2 models to compare.'); return; }
    if (names.length > 2) { alert('Compare supports exactly 2 models. Deselect extras.'); return; }
    try {
        const url = `/api/compare?model_a=${encodeURIComponent(names[0])}&model_b=${encodeURIComponent(names[1])}`;
        const resp = await fetch(url);
        const data = await resp.json();
        if (data.error) { alert(data.error); return; }
        showCompareModal(data);
    } catch (e) { alert('Compare failed: ' + e.message); }
}

function showCompareModal(data) {
    let existing = document.getElementById('compareModal');
    if (existing) existing.remove();

    let html = `<div id="compareModal" style="position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;" onclick="if(event.target===this)this.remove()">`;
    html += `<div style="background:#1e293b;border:1px solid #334155;border-radius:12px;max-width:800px;width:95%;max-height:80vh;overflow:auto;padding:24px;">`;
    html += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">`;
    html += `<h3 style="margin:0;color:#e2e8f0;">Compare: ${esc(data.model_a)} vs ${esc(data.model_b)}</h3>`;
    html += `<button onclick="this.closest('#compareModal').remove()" style="background:none;border:none;color:#94a3b8;font-size:20px;cursor:pointer;">✕</button></div>`;

    // Summary
    const s = data.summary;
    html += `<div style="display:flex;gap:16px;margin-bottom:16px;font-size:13px;">`;
    html += `<span style="color:#34d399;">✅ ${s.improved} improved</span>`;
    html += `<span style="color:#f87171;">❌ ${s.regressed} regressed</span>`;
    html += `<span style="color:#94a3b8;">➖ ${s.unchanged} unchanged</span>`;
    html += `<span style="color:#fbbf24;">❓ ${s.missing} missing</span></div>`;

    // Table
    html += `<table style="width:100%;border-collapse:collapse;font-size:13px;">`;
    html += `<thead><tr style="border-bottom:1px solid #475569;color:#94a3b8;">`;
    html += `<th style="text-align:left;padding:6px;">Family</th>`;
    html += `<th style="text-align:left;padding:6px;">Metric</th>`;
    html += `<th style="text-align:right;padding:6px;">${esc(data.model_a)}</th>`;
    html += `<th style="text-align:right;padding:6px;">${esc(data.model_b)}</th>`;
    html += `<th style="text-align:right;padding:6px;">Δ</th>`;
    html += `<th style="text-align:right;padding:6px;">%</th>`;
    html += `</tr></thead><tbody>`;

    for (const d of data.deltas) {
        const va = d.value_a != null ? d.value_a.toFixed(2) : '—';
        const vb = d.value_b != null ? d.value_b.toFixed(2) : '—';
        let dColor = '#94a3b8';
        let deltaStr = '—', pctStr = '—';
        if (d.delta != null) {
            deltaStr = (d.delta >= 0 ? '+' : '') + d.delta.toFixed(2);
            pctStr = d.pct_change != null ? ((d.pct_change >= 0 ? '+' : '') + d.pct_change.toFixed(1) + '%') : '—';
            dColor = d.delta > 0 ? '#34d399' : d.delta < 0 ? '#f87171' : '#94a3b8';
        }
        html += `<tr style="border-bottom:1px solid #1e293b;">`;
        html += `<td style="padding:5px 6px;color:#cbd5e1;">${esc(d.family)}</td>`;
        html += `<td style="padding:5px 6px;color:#e2e8f0;">${esc(d.metric)}</td>`;
        html += `<td style="padding:5px 6px;text-align:right;color:#94a3b8;">${va}</td>`;
        html += `<td style="padding:5px 6px;text-align:right;color:#94a3b8;">${vb}</td>`;
        html += `<td style="padding:5px 6px;text-align:right;color:${dColor};font-weight:600;">${deltaStr}</td>`;
        html += `<td style="padding:5px 6px;text-align:right;color:${dColor};">${pctStr}</td>`;
        html += `</tr>`;
    }
    html += `</tbody></table></div></div>`;
    document.body.insertAdjacentHTML('beforeend', html);
}

function syncSelectionUI() {
    // Update selection bar
    const bar = document.getElementById('selectionBar');
    const count = selectedModels.size;
    bar.classList.toggle('active', count > 0);
    document.getElementById('selCount').textContent = count;

    // Update table row highlights + checkboxes
    document.querySelectorAll('#tableContainer tr[data-model-name]').forEach(tr => {
        const name = tr.dataset.modelName;
        const isSel = selectedModels.has(name);
        tr.classList.toggle('row-selected', isSel);
        const cb = tr.querySelector('.sel-cb');
        if (cb) cb.checked = isSel;
    });

    // Update graph node glows
    d3.selectAll('#graphSvg .node-circle').each(function(d) {
        d3.select(this).classed('selected', selectedModels.has(d.id));
    });

    // Reset filter if selection cleared
    if (count === 0) filterBySelection = false;
}

/* ======================================================================
   ██████  ██████   █████  ██████  ██   ██
  ██       ██   ██ ██   ██ ██   ██ ██   ██
  ██   ███ ██████  ███████ ██████  ███████
  ██    ██ ██   ██ ██   ██ ██     ██    ██
   ██████  ██   ██ ██   ██ ██     ██   ██
   D3 FORCE-DIRECTED GRAPH VIEW
   ====================================================================== */

const COLOR_PALETTE = [
    "#f472b6", "#a78bfa", "#60a5fa", "#34d399", "#fbbf24", "#fb923c",
    "#f87171", "#e879f9", "#22d3ee", "#a3e635", "#818cf8", "#2dd4bf",
    "#facc15", "#f97316"
];
const NEUTRAL_COLOR = "#6b7280";

async function loadGraph() {
    const resp = await fetch('/api/graph');
    const data = await resp.json();
    GRAPH = data;
    renderGraph(data);
}

function renderGraph(data) {
    const container = document.getElementById('graphView');
    const svgEl = document.getElementById('graphSvg');

    // Preserve the search + tooltip elements, clear only the SVG
    svgEl.innerHTML = '';

    const showCkpt = document.getElementById('graphShowCheckpoints').checked;
    const showHidden = document.getElementById('graphShowHidden').checked;

    // Filter out checkpoints and hidden models unless the toggles are on
    const nodes = data.nodes.filter(n => {
        if (n.type === 'checkpoint' && !showCkpt) return false;
        if (n.hidden && !showHidden) return false;
        return true;
    });
    const nodeIds = new Set(nodes.map(n => n.id));
    const links = data.links.filter(l => {
        const src = typeof l.source === 'object' ? l.source.id : l.source;
        const tgt = typeof l.target === 'object' ? l.target.id : l.target;
        return nodeIds.has(src) && nodeIds.has(tgt);
    });

    if (!nodes.length) {
        svgEl.outerHTML = `<div class="empty-state" style="padding-top:120px;">
            <div class="icon">🔗</div>
            <h3>No models to graph</h3>
            <p>Register models with parent relationships to see the graph.</p>
        </div>`;
        return;
    }

    const width = container.clientWidth || 900;
    const height = container.clientHeight || 600;

    const svg = d3.select(svgEl)
        .attr("viewBox", [0, 0, width, height]);

    // Zoom group
    const g = svg.append("g");
    svg.call(d3.zoom()
        .scaleExtent([0.2, 4])
        .on("zoom", (e) => g.attr("transform", e.transform)));

    // Assign group colors
    const groupColors = {};
    let colorIdx = 0;
    const groups = [...new Set(nodes.map(n => n.group))];
    groups.forEach(grp => {
        groupColors[grp] = COLOR_PALETTE[colorIdx % COLOR_PALETTE.length];
        colorIdx++;
    });

    // Compute genealogy levels for initial positioning
    const nodeById = {};
    nodes.forEach(n => { nodeById[n.id] = n; });

    function getLevel(node, visited = new Set()) {
        if (visited.has(node.id)) return 0;
        visited.add(node.id);
        if (!node.parent || !nodeById[node.parent]) return 0;
        return 1 + getLevel(nodeById[node.parent], visited);
    }

    nodes.forEach(n => { n.level = getLevel(n); });
    const maxLevel = Math.max(1, ...nodes.map(n => n.level));
    const levelHeight = height / (maxLevel + 2);

    // Group by family for initial X spread
    const famMap = {};
    nodes.forEach(n => {
        const fam = n.group || n.id;
        if (!famMap[fam]) famMap[fam] = [];
        famMap[fam].push(n);
    });
    const famNames = Object.keys(famMap).sort((a, b) => famMap[b].length - famMap[a].length);
    const famSpacing = Math.max(200, width / (famNames.length + 1));
    let famX = famSpacing / 2;

    famNames.forEach(fam => {
        const famNodes = famMap[fam];
        famNodes.sort((a, b) => a.level - b.level);
        famNodes.forEach((n, i) => {
            n.x = famX + (i % 3) * 60 - 30;
            n.y = 80 + n.level * levelHeight;
        });
        famX += famSpacing;
    });

    // Arrow marker
    svg.append("defs").append("marker")
        .attr("id", "arrowhead")
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 25)
        .attr("refY", 0)
        .attr("markerWidth", 8)
        .attr("markerHeight", 8)
        .attr("orient", "auto")
        .append("path")
        .attr("d", "M0,-5L10,0L0,5")
        .attr("class", "arrow");

    // Links
    const link = g.append("g")
        .selectAll("path")
        .data(links)
        .join("path")
        .attr("class", "link")
        .attr("marker-end", "url(#arrowhead)");

    // Force simulation
    graphSimulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d => d.id).distance(150).strength(0.3))
        .force("charge", d3.forceManyBody().strength(-300))
        .force("center", d3.forceCenter(width / 2, height / 2))
        .force("collision", d3.forceCollide().radius(30))
        .force("y", d3.forceY(d => 80 + d.level * levelHeight).strength(0.1))
        .alphaDecay(0.02)
        .velocityDecay(0.4);

    // Node groups
    const node = g.append("g")
        .selectAll("g")
        .data(nodes)
        .join("g")
        .attr("class", "node-group")
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));

    // Node circles — radius based on score, color by group
    node.append("circle")
        .attr("class", d => `node-circle${selectedModels.has(d.id) ? ' selected' : ''}`)
        .attr("r", 10)
        .attr("fill", d => groupColors[d.group] || NEUTRAL_COLOR)
        .attr("stroke", "#fff")
        .attr("stroke-width", 2);

    // Labels below node
    node.append("text")
        .attr("class", "node-label")
        .attr("dy", 24)
        .attr("text-anchor", "middle")
        .text(d => d.id.length > 25 ? d.id.slice(0, 22) + '…' : d.id);

    // Step badge for checkpoints
    node.filter(d => d.type === 'checkpoint')
        .append("text")
        .attr("dy", -18)
        .attr("text-anchor", "middle")
        .attr("fill", "#fbbf24")
        .attr("font-size", "9px")
        .attr("font-weight", "bold")
        .attr("pointer-events", "none")
        .text(d => `step-${d.step}`);

    // Tick update
    graphSimulation.on("tick", () => {
        link.attr("d", d => `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`);
        node.attr("transform", d => `translate(${d.x},${d.y})`);
    });

    // Tooltip interactions
    const tooltip = d3.select("#graphTooltip");

    node.on("mouseover", (e, d) => {
        if (detailNode) return;
        showGraphTooltip(e, d, tooltip, container, false);
    })
    .on("mouseout", () => {
        if (detailNode) return;
        tooltip.classed("show", false);
    })
    .on("click", (e, d) => {
        e.stopPropagation();
        tooltip.classed("show", false);

        // Link mode: clicking another node sets it as parent of detailNode
        if (linkMode && detailNode && d.type === 'model' && d.id !== detailNode.id) {
            setParentFromLink(d.id);
            return;
        }

        // Toggle selection for table sync
        const name = d.id;
        if (selectedModels.has(name)) selectedModels.delete(name);
        else selectedModels.add(name);
        syncSelectionUI();

        // Open detail panel
        if (d.type === 'model') openDetailPanel(d);
    });

    svg.on("click", (e) => {
        if (e.target === svgEl || e.target.tagName === 'svg') {
            tooltip.classed("show", false);
            if (!linkMode) closeDetailPanel();
        }
    });

    // Drag handlers
    function dragstarted(e) {
        if (!e.active) graphSimulation.alphaTarget(0.1).restart();
        e.subject.fx = e.subject.x;
        e.subject.fy = e.subject.y;
    }
    function dragged(e) {
        e.subject.fx = e.x;
        e.subject.fy = e.y;
    }
    function dragended(e) {
        if (!e.active) graphSimulation.alphaTarget(0);
    }
}

function getBenchmarkDots(d) {
    if (!GRAPH || !GRAPH.families || !GRAPH.families.length) return '';
    let html = '<div class="benchmark-dots">';
    for (const family of GRAPH.families) {
        const hasAny = Object.keys(d.metrics).some(k => k.startsWith(family + '/'));
        html += `<span class="dot-group"><span class="dot ${hasAny ? 'has' : 'no'}"></span>${esc(family)}</span>`;
    }
    html += '</div>';
    return html;
}

function showGraphTooltip(e, d, tooltip, container, locked) {
    const metricsHtml = getBenchmarkDots(d);

    const typeBadge = d.type === 'model'
        ? '<span class="gt-type gt-type-model">model</span>'
        : '<span class="gt-type gt-type-ckpt">checkpoint</span>';

    const closeBtn = locked
        ? `<button class="gt-close" onclick="event.stopPropagation(); document.getElementById('graphTooltip').classList.remove('show')">✕</button>`
        : '';

    tooltip.html(`
        ${closeBtn}
        <h3>${esc(d.id)}</h3>
        ${typeBadge}
        ${d.run ? `<span style="color:#6b7280;font-size:10px;margin-left:8px;">run: ${esc(d.run)}</span>` : ''}
        ${d.parent ? `<div style="color:#a78bfa;font-size:11px;margin-top:4px;">↳ parent: ${esc(d.parent)}</div>` : ''}
        ${d.notes ? `<div style="color:#fbbf24;font-size:11px;margin-top:4px;font-style:italic;">📝 ${esc(d.notes)}</div>` : ''}
        ${d.path ? `<div class="gt-path">${esc(d.path)}</div>` : ''}
        ${metricsHtml}
    `);

    const rect = container.getBoundingClientRect();
    const x = Math.min(e.clientX - rect.left + 15, rect.width - 350);
    const y = Math.min(e.clientY - rect.top - 10, rect.height - 300);
    tooltip.style("left", Math.max(0, x) + "px").style("top", Math.max(0, y) + "px");
    tooltip.classed("show", true);
}

/* ======================================================================
   GRAPH DETAIL PANEL
   ====================================================================== */
function openDetailPanel(d) {
    detailNode = d;
    linkMode = false;
    const panel = document.getElementById('graphDetailPanel');
    document.getElementById('gdpTitle').textContent = d.id;
    document.getElementById('gdpTypeRun').innerHTML =
        `<span class="gt-type gt-type-model">model</span> <span style="color:#6b7280;margin-left:6px;">run: ${esc(d.run || '—')}</span>`;
    document.getElementById('gdpPath').textContent = d.path || '—';
    document.getElementById('gdpBenchmarks').innerHTML = getBenchmarkDots(d);
    document.getElementById('gdpNotes').value = d.notes || '';

    // Build parent dropdown
    const select = document.getElementById('gdpParentSelect');
    let opts = '<option value="">— No parent —</option>';
    if (GRAPH) {
        const models = GRAPH.nodes
            .filter(n => n.type === 'model' && n.id !== d.id && n.run === d.run)
            .sort((a, b) => a.id.localeCompare(b.id));
        for (const m of models) {
            const sel = d.parent === m.id ? ' selected' : '';
            opts += `<option value="${esc(m.id)}"${sel}>${esc(m.id)}</option>`;
        }
    }
    select.innerHTML = opts;

    // Show/hide parent & notes & hide for checkpoints vs models
    const isModel = d.type === 'model';
    document.getElementById('gdpParentSection').style.display = isModel ? '' : 'none';
    document.getElementById('gdpNotesSection').style.display = isModel ? '' : 'none';
    document.getElementById('gdpHideSection').style.display = isModel ? '' : 'none';
    if (isModel) updateHideButton();

    // Reset UI
    document.getElementById('gdpLinkBtn').classList.remove('active');
    document.getElementById('gdpLinkBtn').textContent = '🔗 Click a node to set as parent';
    hideStatus('gdpParentStatus');
    hideStatus('gdpNotesStatus');

    // Highlight selected node
    d3.selectAll('#graphSvg .node-circle').attr('stroke-width', n => n.id === d.id ? 3.5 : 2)
        .attr('stroke', n => n.id === d.id ? '#818cf8' : '#fff');

    panel.classList.add('open');
}

function closeDetailPanel() {
    detailNode = null;
    linkMode = false;
    document.getElementById('graphDetailPanel').classList.remove('open');
    // Reset node highlights
    d3.selectAll('#graphSvg .node-circle').attr('stroke-width', 2).attr('stroke', '#fff');
}

function toggleLinkMode() {
    linkMode = !linkMode;
    const btn = document.getElementById('gdpLinkBtn');
    btn.classList.toggle('active', linkMode);
    btn.textContent = linkMode ? '🔗 Now click a node…' : '🔗 Click a node to set as parent';

    // Visual hint: highlight connectable nodes
    if (linkMode && detailNode) {
        d3.selectAll('#graphSvg .node-circle')
            .attr('stroke', n => {
                if (n.id === detailNode.id) return '#818cf8';
                if (n.type === 'model' && n.run === detailNode.run) return '#34d399';
                return '#fff';
            })
            .attr('stroke-width', n => {
                if (n.id === detailNode.id) return 3.5;
                if (n.type === 'model' && n.run === detailNode.run) return 3;
                return 2;
            });
    } else {
        d3.selectAll('#graphSvg .node-circle')
            .attr('stroke', n => n.id === detailNode?.id ? '#818cf8' : '#fff')
            .attr('stroke-width', n => n.id === detailNode?.id ? 3.5 : 2);
    }
}

async function setParentFromLink(parentName) {
    if (!detailNode) return;
    // Update the dropdown to match
    document.getElementById('gdpParentSelect').value = parentName;
    await saveParent(parentName);
    linkMode = false;
    document.getElementById('gdpLinkBtn').classList.remove('active');
    document.getElementById('gdpLinkBtn').textContent = '🔗 Click a node to set as parent';
    d3.selectAll('#graphSvg .node-circle')
        .attr('stroke', n => n.id === detailNode?.id ? '#818cf8' : '#fff')
        .attr('stroke-width', n => n.id === detailNode?.id ? 3.5 : 2);
}

async function onParentChange() {
    const val = document.getElementById('gdpParentSelect').value;
    await saveParent(val || null);
}

async function saveParent(parentName) {
    if (!detailNode || !detailNode.model_id) return;
    const statusEl = document.getElementById('gdpParentStatus');
    try {
        const resp = await fetch(`/api/model/${detailNode.model_id}/parent`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ parent_name: parentName })
        });
        if (!resp.ok) {
            const err = await resp.json();
            showStatus(statusEl, err.error || 'Error', true);
            return;
        }
        // Update local data
        detailNode.parent = parentName;
        showStatus(statusEl, parentName ? `Parent → ${parentName}` : 'Parent cleared', false);
        // Reload graph to reflect new links
        GRAPH = null;
        await loadGraph();
    } catch (e) {
        showStatus(statusEl, 'Network error', true);
    }
}

async function saveNotes() {
    if (!detailNode || !detailNode.model_id) return;
    const notes = document.getElementById('gdpNotes').value;
    if (notes === (detailNode.notes || '')) return; // no change
    const statusEl = document.getElementById('gdpNotesStatus');
    try {
        const resp = await fetch(`/api/model/${detailNode.model_id}/notes`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ notes })
        });
        if (!resp.ok) {
            showStatus(statusEl, 'Error saving', true);
            return;
        }
        detailNode.notes = notes;
        showStatus(statusEl, 'Saved', false);
    } catch (e) {
        showStatus(statusEl, 'Network error', true);
    }
}

async function toggleHideModel() {
    if (!detailNode || !detailNode.model_id) return;
    const newHidden = !detailNode.hidden;
    try {
        const resp = await fetch(`/api/model/${detailNode.model_id}/hidden`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ hidden: newHidden })
        });
        if (!resp.ok) return;
        detailNode.hidden = newHidden;
        updateHideButton();
        // Re-render graph to reflect visibility
        if (GRAPH) renderGraph(GRAPH);
        // If hiding and "Show hidden" is off, close the panel
        if (newHidden && !document.getElementById('graphShowHidden').checked) {
            closeDetailPanel();
        }
    } catch (e) { /* ignore */ }
}

function updateHideButton() {
    const btn = document.getElementById('gdpHideBtn');
    if (!btn || !detailNode) return;
    btn.textContent = detailNode.hidden ? '👁 Unhide this model' : '🙈 Hide this model';
}

function showStatus(el, msg, isError) {
    el.textContent = msg;
    el.className = 'gdp-status ' + (isError ? 'error' : 'success');
    setTimeout(() => { el.className = 'gdp-status'; }, 3000);
}
function hideStatus(id) {
    const el = document.getElementById(id);
    el.className = 'gdp-status';
    el.textContent = '';
}

/* ======================================================================
   GRAPH SEARCH / FILTER
   ====================================================================== */
function filterGraphNodes() {
    const term = document.getElementById('graphSearchBox').value.toLowerCase();
    d3.selectAll("#graphSvg .node-group").each(function(d) {
        const match = !term || d.id.toLowerCase().includes(term) ||
                      (d.path && d.path.toLowerCase().includes(term)) ||
                      (d.run && d.run.toLowerCase().includes(term));
        d3.select(this).style("opacity", match ? 1 : 0.12).style("pointer-events", match ? "all" : "none");
    });
    d3.selectAll("#graphSvg .link").style("opacity", term ? 0.15 : 0.6);
}

/* ======================================================================
   AUTO-REFRESH
   ====================================================================== */
let _autoRefreshId = null;
function toggleAutoRefresh(sec = 30) {
    if (_autoRefreshId) { clearInterval(_autoRefreshId); _autoRefreshId = null; return; }
    _autoRefreshId = setInterval(refresh, sec * 1000);
}

/* ======================================================================
   FILTER DROPDOWNS → CHIP BARS + BUILDER PANEL
   ====================================================================== */

function initFilters() {
    if (!DATA) return;

    // Build all metric keys from DATA.metrics
    const keySet = new Set();
    for (const m of DATA.metrics) keySet.add(`${m.family}/${m.metric_name}`);
    allMetricKeys = [...keySet].sort();

    visibleFamilies.clear();
    DATA.families.forEach(f => visibleFamilies.add(f));

    visibleMetrics.clear();
    allMetricKeys.forEach(k => visibleMetrics.add(k));
    columnOrder = [...allMetricKeys];
    renderMetricChips();

    visibleModels.clear();
    DATA.models.forEach(m => visibleModels.add(m.model_name));
    renderModelChips();
}

/* ── Metric Chip Bar ─────────────────────────────── */
function metricDisplayName(key) {
    const i = key.indexOf('/');
    return i >= 0 ? key.substring(i + 1) : key;
}
function metricFamily(key) {
    const i = key.indexOf('/');
    return i >= 0 ? key.substring(0, i) : '';
}

function renderMetricChips() {
    const bar = document.getElementById('metricChipBar');
    if (!bar) return;
    const total = allMetricKeys.length;
    if (columnOrder.length === total) {
        bar.innerHTML = `<div class="chip" style="cursor:default;"><span>All metrics (${total})</span></div>`;
        return;
    }
    if (columnOrder.length === 0) {
        bar.innerHTML = `<span style="font-size:11px;color:#6b7280;font-style:italic;">No metrics selected</span>`;
        return;
    }
    bar.innerHTML = columnOrder.map((key, i) =>
        `<div class="chip" draggable="true" data-type="metric" data-name="${esc(key)}" data-index="${i}"
              ondragstart="chipDragStart(event)" ondragover="chipDragOver(event)"
              ondragend="chipDragEnd(event)" ondrop="chipDrop(event)"
              title="${esc(key)}">
            <span class="chip-fam-tag">${esc(metricFamily(key))}/</span>${esc(metricDisplayName(key))}
            <button class="chip-remove" onclick="event.stopPropagation(); removeChip('metric','${esc(key)}')" title="Remove">&times;</button>
        </div>`
    ).join('');
}

/* ── Model Chip Bar ──────────────────────────────── */
function renderModelChips() {
    const bar = document.getElementById('modelChipBar');
    if (!bar || !DATA) return;
    const total = DATA.models.length;
    const visible = visibleModels.size;
    if (visible === total) {
        bar.innerHTML = `<div class="chip chip-model" style="cursor:default;">
            <span>All models (${total})</span>
        </div>`;
        return;
    }
    if (visible === 0) {
        bar.innerHTML = `<span style="font-size:11px;color:#6b7280;font-style:italic;">No models selected</span>`;
        return;
    }
    const models = [...visibleModels];
    const display = models.slice(0, 20);
    bar.innerHTML = display.map(m =>
        `<div class="chip chip-model" data-type="model" data-name="${esc(m)}">
            <span>${esc(m)}</span>
            <button class="chip-remove" onclick="event.stopPropagation(); removeChip('model','${esc(m)}')" title="Remove">&times;</button>
        </div>`
    ).join('') + (models.length > 20 ? `<span style="font-size:11px;color:#6b7280;">+${models.length - 20} more</span>` : '');
}

function removeChip(type, name) {
    if (type === 'metric') {
        columnOrder = columnOrder.filter(k => k !== name);
        visibleMetrics.delete(name);
        renderMetricChips();
        renderTable();
    } else {
        visibleModels.delete(name);
        renderModelChips();
        renderTable();
    }
}

/* ── Chip Drag & Drop (reorder metrics) ──────────── */
let chipDragIdx = null;

function chipDragStart(e) {
    const chip = e.target.closest('.chip');
    chipDragIdx = parseInt(chip.dataset.index);
    chip.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', String(chipDragIdx));
}

function chipDragOver(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
}

function chipDrop(e) {
    e.preventDefault();
    const targetChip = e.target.closest('.chip');
    if (!targetChip || chipDragIdx === null) return;
    const targetIdx = parseInt(targetChip.dataset.index);
    if (chipDragIdx === targetIdx) return;
    const [moved] = columnOrder.splice(chipDragIdx, 1);
    columnOrder.splice(targetIdx, 0, moved);
    chipDragIdx = null;
    renderMetricChips();
    renderTable();
}

function chipDragEnd() {
    chipDragIdx = null;
    document.querySelectorAll('.chip.dragging').forEach(c => c.classList.remove('dragging'));
}

/* ── Builder Panel ───────────────────────────────── */
function openBuilder(type) {
    builderType = type;
    document.getElementById('builderTitle').textContent = type === 'metric' ? 'Select Metrics' : 'Select Models';
    document.getElementById('builderSearch').value = '';
    document.getElementById('builderOverlay').classList.add('open');
    renderBuilderItems();
}

function closeBuilder() {
    document.getElementById('builderOverlay').classList.remove('open');
    builderType = null;
}

function renderBuilderItems(searchTerm) {
    const list = document.getElementById('builderList');
    if (!DATA) return;
    const term = (searchTerm || '').toLowerCase();
    if (builderType === 'metric') {
        // Group metrics by family
        const grouped = {};
        for (const key of allMetricKeys) {
            const fam = metricFamily(key);
            if (!grouped[fam]) grouped[fam] = [];
            grouped[fam].push(key);
        }
        let html = '';
        for (const fam of Object.keys(grouped).sort()) {
            const metrics = grouped[fam].filter(k => !term || k.toLowerCase().includes(term));
            if (!metrics.length) continue;
            const allOn = metrics.every(k => visibleMetrics.has(k));
            html += `<div style="padding:8px 12px 4px;font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#60a5fa;display:flex;align-items:center;gap:8px;">`;
            html += `<span>${esc(fam)}</span>`;
            html += `<button class="builder-act-btn" style="font-size:9px;padding:2px 8px;" onclick="toggleFamilyMetrics('${esc(fam)}')">${allOn ? 'None' : 'All'}</button>`;
            html += `</div>`;
            for (const key of metrics) {
                const active = visibleMetrics.has(key) ? 'active' : '';
                html += `<div class="builder-item ${active}" onclick="toggleBuilderItem('metric','${esc(key)}')">
                    <span class="bi-check">&#x2713;</span>
                    <span class="bi-name">${esc(metricDisplayName(key))}</span>
                </div>`;
            }
        }
        list.innerHTML = html;
    } else {
        const items = DATA.models.filter(m => !term || m.model_name.toLowerCase().includes(term));
        list.innerHTML = items.map(m => {
            const active = visibleModels.has(m.model_name) ? 'active' : '';
            return `<div class="builder-item ${active}" onclick="toggleBuilderItem('model','${esc(m.model_name)}')">
                <span class="bi-check">&#x2713;</span>
                <span class="bi-name">${esc(m.model_name)}</span>
            </div>`;
        }).join('');
    }
}

function filterBuilderItems() {
    renderBuilderItems(document.getElementById('builderSearch').value);
}

function toggleFamilyMetrics(fam) {
    const keys = allMetricKeys.filter(k => metricFamily(k) === fam);
    const allOn = keys.every(k => visibleMetrics.has(k));
    if (allOn) {
        keys.forEach(k => { visibleMetrics.delete(k); columnOrder = columnOrder.filter(c => c !== k); });
    } else {
        keys.forEach(k => { if (!visibleMetrics.has(k)) { visibleMetrics.add(k); columnOrder.push(k); } });
    }
    renderMetricChips();
    renderTable();
    renderBuilderItems(document.getElementById('builderSearch').value);
}

function toggleBuilderItem(type, name) {
    if (type === 'metric') {
        if (visibleMetrics.has(name)) {
            visibleMetrics.delete(name);
            columnOrder = columnOrder.filter(k => k !== name);
        } else {
            visibleMetrics.add(name);
            columnOrder.push(name);
        }
        renderMetricChips();
        renderTable();
    } else {
        if (visibleModels.has(name)) visibleModels.delete(name);
        else visibleModels.add(name);
        renderModelChips();
        renderTable();
    }
    renderBuilderItems(document.getElementById('builderSearch').value);
}

function builderSelectAll() {
    if (builderType === 'metric') {
        allMetricKeys.forEach(k => {
            if (!visibleMetrics.has(k)) {
                visibleMetrics.add(k);
                columnOrder.push(k);
            }
        });
        renderMetricChips();
        renderTable();
    } else {
        DATA.models.forEach(m => visibleModels.add(m.model_name));
        renderModelChips();
        renderTable();
    }
    renderBuilderItems(document.getElementById('builderSearch').value);
}

function builderSelectNone() {
    if (builderType === 'metric') {
        visibleMetrics.clear();
        columnOrder = [];
        renderMetricChips();
        renderTable();
    } else {
        visibleModels.clear();
        renderModelChips();
        renderTable();
    }
    renderBuilderItems(document.getElementById('builderSearch').value);
}

/* ======================================================================
   SAVED FILTERS (localStorage-backed)
   ====================================================================== */
let activeFilterName = null;

function filtersStorageKey() {
    return 'lightml_filters_' + (DATA ? DATA.db_name : 'default');
}

function loadSavedFilters() {
    try { return JSON.parse(localStorage.getItem(filtersStorageKey()) || '[]'); }
    catch { return []; }
}

function persistFilters(filters) {
    localStorage.setItem(filtersStorageKey(), JSON.stringify(filters));
}

function saveCurrentFilter() {
    const models = [...visibleModels];
    if (!models.length) { alert('No models selected to save.'); return; }
    if (models.length === DATA.models.length) { alert('All models are selected — filter would have no effect.'); return; }
    const name = prompt('Filter name:');
    if (!name || !name.trim()) return;
    const filters = loadSavedFilters();
    const existing = filters.findIndex(f => f.name === name.trim());
    const entry = { name: name.trim(), models };
    if (existing >= 0) filters[existing] = entry;
    else filters.push(entry);
    persistFilters(filters);
    activeFilterName = name.trim();
    renderSavedFilters();
}

function applySavedFilter(name) {
    const filters = loadSavedFilters();
    const f = filters.find(x => x.name === name);
    if (!f) return;
    if (activeFilterName === name) {
        // Toggle off: restore all models
        activeFilterName = null;
        visibleModels.clear();
        DATA.models.forEach(m => visibleModels.add(m.model_name));
    } else {
        activeFilterName = name;
        visibleModels.clear();
        f.models.forEach(m => visibleModels.add(m));
    }
    renderModelChips();
    renderTable();
    renderSavedFilters();
}

function deleteSavedFilter(name, e) {
    e.stopPropagation();
    const filters = loadSavedFilters().filter(f => f.name !== name);
    persistFilters(filters);
    if (activeFilterName === name) activeFilterName = null;
    renderSavedFilters();
}

function renderSavedFilters() {
    const bar = document.getElementById('savedFilterBar');
    const section = document.getElementById('savedFilterSection');
    if (!bar || !section) return;
    const filters = loadSavedFilters();
    if (!filters.length) { section.style.display = 'none'; return; }
    section.style.display = '';
    bar.innerHTML = filters.map(f => {
        const cls = f.name === activeFilterName ? 'saved-filter-chip active' : 'saved-filter-chip';
        return `<div class="${cls}" onclick="applySavedFilter('${esc(f.name)}')" title="${f.models.length} models">
            <span>🏷️ ${esc(f.name)}</span>
            <span style="font-size:9px;color:#9ca3af;">(${f.models.length})</span>
            <button class="sf-remove" onclick="deleteSavedFilter('${esc(f.name)}', event)" title="Delete filter">&times;</button>
        </div>`;
    }).join('');
}

/* ======================================================================
    ██████ ██   ██  █████  ██████  ████████ ███████
   ██      ██   ██ ██   ██ ██   ██    ██    ██
   ██      ███████ ███████ ██████     ██    ███████
   ██      ██   ██ ██   ██ ██   ██    ██         ██
    ██████ ██   ██ ██   ██ ██   ██    ██    ███████
   SAVED CHARTS SYSTEM (localStorage-backed)
   ====================================================================== */

const CHART_COLORS = [
    '#60a5fa','#f472b6','#34d399','#fbbf24','#a78bfa',
    '#fb923c','#22d3ee','#f87171','#a3e635','#e879f9',
    '#2dd4bf','#818cf8'
];
let cbSelectedType = 'radar';
let editingChartId = null;

function chartsStorageKey() {
    return 'lightml_charts_' + (DATA ? DATA.db_name : 'default');
}

function loadSavedCharts() {
    try { return JSON.parse(localStorage.getItem(chartsStorageKey()) || '[]'); }
    catch { return []; }
}

function persistCharts(charts) {
    localStorage.setItem(chartsStorageKey(), JSON.stringify(charts));
}

/* ── Builder ──────────────────────────────────────── */
function openChartBuilder(prefill) {
    if (!DATA) return;
    const pName = prefill ? prefill.name : '';
    const pType = prefill ? prefill.type : 'radar';
    const pModels = prefill ? new Set(prefill.models) : new Set();
    const pMetrics = prefill ? new Set(prefill.metrics) : new Set();
    document.getElementById('cbName').value = pName;
    cbSelectedType = pType;
    document.querySelectorAll('.cb-type-btn').forEach(b => b.classList.toggle('selected', b.dataset.type === pType));

    // Populate model picker
    const mp = document.getElementById('cbModelPicker');
    mp.innerHTML = DATA.models.map(m =>
        `<label class="cb-pick-item"><input type="checkbox" value="${esc(m.model_name)}"${pModels.has(m.model_name) ? ' checked' : ''}> ${esc(m.model_name)}</label>`
    ).join('');

    // Populate metric picker grouped by family
    const grouped = {};
    for (const key of allMetricKeys) {
        const fam = metricFamily(key);
        if (!grouped[fam]) grouped[fam] = [];
        grouped[fam].push(key);
    }
    const metp = document.getElementById('cbMetricPicker');
    let mhtml = '';
    for (const fam of Object.keys(grouped).sort()) {
        mhtml += `<div class="cb-pick-group-hdr">${esc(fam)}</div>`;
        for (const key of grouped[fam]) {
            mhtml += `<label class="cb-pick-item"><input type="checkbox" value="${esc(key)}"${pMetrics.has(key) ? ' checked' : ''}> ${esc(metricDisplayName(key))}</label>`;
        }
    }
    metp.innerHTML = mhtml;

    document.getElementById('cbOverlay').classList.add('open');
}

function closeChartBuilder() {
    editingChartId = null;
    document.getElementById('cbOverlay').classList.remove('open');
}

function selectChartType(type) {
    cbSelectedType = type;
    document.querySelectorAll('.cb-type-btn').forEach(b => b.classList.toggle('selected', b.dataset.type === type));
}

function saveChart() {
    const name = document.getElementById('cbName').value.trim() || ('Chart ' + (loadSavedCharts().length + 1));
    const models = [...document.querySelectorAll('#cbModelPicker input:checked')].map(i => i.value);
    const metrics = [...document.querySelectorAll('#cbMetricPicker input:checked')].map(i => i.value);

    if (models.length < 1) { alert('Select at least 1 model.'); return; }
    if (metrics.length < 1) { alert('Select at least 1 metric.'); return; }
    if (cbSelectedType === 'radar' && metrics.length < 3) { alert('Radar charts need at least 3 metrics.'); return; }
    if (cbSelectedType === 'pivot' && metrics.length < 1) { alert('Pivot needs at least 1 metric.'); return; }

    const charts = loadSavedCharts();
    if (editingChartId) {
        const idx = charts.findIndex(c => c.id === editingChartId);
        if (idx !== -1) {
            charts[idx] = { ...charts[idx], name, type: cbSelectedType, models, metrics };
        }
    } else {
        charts.push({
            id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
            name, type: cbSelectedType, models, metrics,
            created: new Date().toISOString()
        });
    }
    persistCharts(charts);
    closeChartBuilder();
    renderSavedCharts();
}

function deleteSavedChart(id) {
    if (!confirm('Delete this chart?')) return;
    const charts = loadSavedCharts().filter(c => c.id !== id);
    persistCharts(charts);
    renderSavedCharts();
}

function editChart(id) {
    const chart = loadSavedCharts().find(c => c.id === id);
    if (!chart) return;
    editingChartId = id;
    openChartBuilder(chart);
}

function duplicateChart(id) {
    const charts = loadSavedCharts();
    const src = charts.find(c => c.id === id);
    if (!src) return;
    const dup = { ...src, id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name: src.name + ' (copy)' };
    charts.push(dup);
    persistCharts(charts);
    renderSavedCharts();
}

/* ── Render saved charts grid ──────────────────── */
const CHART_SIZES = ['1x1','2x1','1x2','2x2'];
const CHART_SIZE_LABELS = { '1x1':'Small', '2x1':'Wide', '1x2':'Tall', '2x2':'Large' };
const GRIP_SVG = `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="10" r="1.2" fill="#6b7280"/><circle cx="10" cy="10" r="1.2" fill="#6b7280"/><circle cx="10" cy="6" r="1.2" fill="#6b7280"/></svg>`;

function renderSavedCharts() {
    const grid = document.getElementById('chartsGrid');
    if (!grid) return;
    const charts = loadSavedCharts();
    if (!charts.length) {
        grid.innerHTML = `<div class="charts-empty">
            <div class="icon">📈</div>
            <h3>No saved charts</h3>
            <p>Click <strong>＋ New Chart</strong> to create a radar or bar chart.</p>
        </div>`;
        return;
    }
    grid.innerHTML = charts.map(c => {
        const sz = c.size || '1x1';
        return `
        <div class="chart-card size-${sz}" id="cc-${c.id}" data-size="${sz}" data-chart-id="${c.id}">
            <div class="chart-drag-grip" data-chart-id="${c.id}" title="Drag to reorder"></div>
            <div class="chart-card-header">
                <h4><span class="chart-type-badge">${esc(c.type)}</span> ${esc(c.name)}</h4>
                <div class="chart-card-actions">
                    <button title="Edit" onclick="editChart('${c.id}')">✏️</button>
                    <button title="Duplicate" onclick="duplicateChart('${c.id}')">⧉</button>
                    <button class="danger" title="Delete" onclick="deleteSavedChart('${c.id}')">🗑</button>
                </div>
            </div>
            <div class="chart-card-body" id="ccb-${c.id}"></div>
            <div class="chart-resize-grip" data-chart-id="${c.id}">${GRIP_SVG}</div>
            <div class="chart-size-toast" id="cst-${c.id}"></div>
        </div>`;
    }).join('');

    for (const c of charts) renderChartD3(c);
    initResizeGrips();
    initDragGrips();
}

function initResizeGrips() {
    document.querySelectorAll('.chart-resize-grip').forEach(grip => {
        grip.addEventListener('mousedown', onGripDown);
        grip.addEventListener('touchstart', onGripDown, { passive: false });
    });
}

function onGripDown(e) {
    e.preventDefault();
    const grip = e.currentTarget;
    const chartId = grip.dataset.chartId;
    const card = document.getElementById('cc-' + chartId);
    if (!card) return;

    const toast = document.getElementById('cst-' + chartId);
    const startX = e.touches ? e.touches[0].clientX : e.clientX;
    const startY = e.touches ? e.touches[0].clientY : e.clientY;
    const startSize = card.dataset.size || '1x1';
    const [startCols, startRows] = startSize.split('x').map(Number);

    // Get one grid cell size
    const rect = card.getBoundingClientRect();
    const cellW = rect.width / startCols;
    const cellH = rect.height / startRows;

    let curCols = startCols, curRows = startRows;
    toast.textContent = CHART_SIZE_LABELS[startSize];
    toast.classList.add('visible');
    card.style.transition = 'none';

    function onMove(ev) {
        const cx = ev.touches ? ev.touches[0].clientX : ev.clientX;
        const cy = ev.touches ? ev.touches[0].clientY : ev.clientY;
        const dx = cx - startX;
        const dy = cy - startY;

        let newCols = Math.round(startCols + dx / cellW);
        let newRows = Math.round(startRows + dy / cellH);
        newCols = Math.max(1, Math.min(2, newCols));
        newRows = Math.max(1, Math.min(2, newRows));

        if (newCols !== curCols || newRows !== curRows) {
            curCols = newCols;
            curRows = newRows;
            const newSize = curCols + 'x' + curRows;
            card.className = card.className.replace(/size-\S+/, 'size-' + newSize);
            card.dataset.size = newSize;
            toast.textContent = CHART_SIZE_LABELS[newSize];
            renderChartD3({ ...loadSavedCharts().find(c => c.id === chartId), _container: true });
        }
    }

    function onUp() {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        document.removeEventListener('touchmove', onMove);
        document.removeEventListener('touchend', onUp);
        card.style.transition = '';
        toast.classList.remove('visible');

        const finalSize = curCols + 'x' + curRows;
        const charts = loadSavedCharts();
        const idx = charts.findIndex(c => c.id === chartId);
        if (idx !== -1) {
            charts[idx].size = finalSize;
            delete charts[idx].width;
            delete charts[idx].height;
            persistCharts(charts);
        }
        // final re-render at correct size
        setTimeout(() => renderChartD3(charts[idx] || charts.find(c => c.id === chartId)), 50);
    }

    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('touchend', onUp);
}

/* ── Get metric data for chart ─────────────────── */
function getChartData(chart) {
    if (!DATA) return [];
    // Build model_name → { metric_key → value }
    const modelIds = {};
    for (const m of DATA.models) {
        if (chart.models.includes(m.model_name)) modelIds[m.id] = m.model_name;
    }
    const result = {};
    for (const name of chart.models) result[name] = {};
    for (const m of DATA.metrics) {
        const mName = modelIds[m.model_id];
        if (!mName) continue;
        if (m.checkpoint_id) continue;
        const key = `${m.family}/${m.metric_name}`;
        if (chart.metrics.includes(key)) result[mName][key] = m.value;
    }
    return result;
}

/* ── D3 Chart Renderers ─────────────────────────── */
function renderChartD3(chart) {
    const container = document.getElementById('ccb-' + chart.id);
    if (!container) return;
    const data = getChartData(chart);
    container.innerHTML = '';

    if (chart.type === 'radar') renderRadar(container, chart, data);
    else if (chart.type === 'bar') renderBarChart(container, chart, data, false);
    else if (chart.type === 'hbar') renderBarChart(container, chart, data, true);
    else if (chart.type === 'table') renderTableChart(container, chart, data);
    else if (chart.type === 'pivot') renderPivotChart(container, chart, data);
}

/* ── RADAR CHART ───────────────────────────────── */
function renderRadar(container, chart, data) {
    const W = container.clientWidth || 460;
    const H = container.clientHeight || 340;
    const margin = 50;
    const R = Math.min(W, H) / 2 - margin;
    const cx = W / 2, cy = H / 2;
    const metrics = chart.metrics;
    const n = metrics.length;
    const angleSlice = (2 * Math.PI) / n;

    const svg = d3.select(container).append('svg').attr('width', W).attr('height', H);
    const g = svg.append('g').attr('transform', `translate(${cx},${cy})`);

    // Normalize: find max per metric across models to scale 0-1
    const maxVals = {};
    for (const mk of metrics) {
        let mx = 0;
        for (const mName of chart.models) {
            const v = data[mName]?.[mk];
            if (v != null && v > mx) mx = v;
        }
        maxVals[mk] = mx || 1;
    }

    // Grid circles
    const levels = 5;
    for (let l = 1; l <= levels; l++) {
        const r = (R / levels) * l;
        g.append('circle').attr('r', r)
            .attr('fill', 'none').attr('stroke', '#334155').attr('stroke-dasharray', '3,3');
        g.append('text').attr('x', 4).attr('y', -r - 2)
            .attr('font-size', '9px').attr('fill', '#4b5563')
            .text(Math.round((l / levels) * 100) + '%');
    }

    // Axes
    for (let i = 0; i < n; i++) {
        const angle = angleSlice * i - Math.PI / 2;
        const x = Math.cos(angle) * R;
        const y = Math.sin(angle) * R;
        g.append('line').attr('x1', 0).attr('y1', 0).attr('x2', x).attr('y2', y)
            .attr('stroke', '#334155');
        const lx = Math.cos(angle) * (R + 18);
        const ly = Math.sin(angle) * (R + 18);
        const label = metricDisplayName(metrics[i]);
        const shortLabel = label.length > 18 ? label.slice(0, 15) + '…' : label;
        g.append('text').attr('x', lx).attr('y', ly)
            .attr('text-anchor', Math.abs(lx) < 5 ? 'middle' : lx > 0 ? 'start' : 'end')
            .attr('dominant-baseline', 'central')
            .attr('font-size', '10px').attr('fill', '#9ca3af')
            .text(shortLabel);
    }

    // Data polygons
    chart.models.forEach((modelName, mi) => {
        const points = metrics.map((mk, i) => {
            const v = data[modelName]?.[mk] ?? 0;
            const norm = v / maxVals[mk];
            const angle = angleSlice * i - Math.PI / 2;
            return [Math.cos(angle) * R * norm, Math.sin(angle) * R * norm];
        });
        const color = CHART_COLORS[mi % CHART_COLORS.length];
        const path = points.map((p, i) => (i === 0 ? 'M' : 'L') + p[0] + ',' + p[1]).join(' ') + 'Z';
        g.append('path').attr('d', path)
            .attr('fill', color).attr('fill-opacity', 0.12)
            .attr('stroke', color).attr('stroke-width', 2);
        // Dots
        points.forEach((p, i) => {
            const v = data[modelName]?.[metrics[i]] ?? 0;
            const dot = g.append('circle').attr('cx', p[0]).attr('cy', p[1]).attr('r', 4)
                .attr('fill', color).attr('stroke', '#1e293b').attr('stroke-width', 1.5)
                .style('cursor', 'default');
            dot.append('title').text(`${modelName}\\n${metricDisplayName(metrics[i])}: ${formatVal(v)}`);
        });
    });

    // Legend
    const lgH = chart.models.length * 20 + 12;
    const legend = svg.append('g').attr('transform', `translate(8, ${H - lgH})`);
    legend.append('rect').attr('width', 200).attr('height', lgH).attr('rx', 6)
        .attr('fill', 'rgba(15,23,42,0.85)').attr('stroke', '#334155').attr('stroke-width', 1);
    chart.models.forEach((name, i) => {
        const lg = legend.append('g').attr('transform', `translate(8, ${i * 20 + 8})`);
        lg.append('rect').attr('width', 12).attr('height', 12).attr('rx', 3).attr('fill', CHART_COLORS[i % CHART_COLORS.length]);
        lg.append('text').attr('x', 18).attr('y', 10).attr('font-size', '11px').attr('fill', '#e2e8f0').attr('font-weight', '500')
            .text(name.length > 25 ? name.slice(0, 22) + '…' : name);
    });
}

/* ── BAR CHART (vertical & horizontal) ─────────── */
function renderBarChart(container, chart, data, horizontal) {
    const W = container.clientWidth || 460;
    const H = container.clientHeight || Math.max(340, horizontal ? chart.metrics.length * chart.models.length * 22 + 80 : 340);
    const margin = horizontal ? { top: 30, right: 20, bottom: 30, left: 180 } : { top: 30, right: 20, bottom: 80, left: 50 };

    const svg = d3.select(container).append('svg').attr('width', W).attr('height', H);
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
    const iw = W - margin.left - margin.right;
    const ih = H - margin.top - margin.bottom;

    const metricLabels = chart.metrics.map(metricDisplayName);
    const groupScale = horizontal
        ? d3.scaleBand().domain(metricLabels).range([0, ih]).padding(0.2)
        : d3.scaleBand().domain(metricLabels).range([0, iw]).padding(0.2);
    const modelScale = d3.scaleBand().domain(chart.models).range([0, groupScale.bandwidth()]).padding(0.05);

    let maxVal = 0;
    for (const mk of chart.metrics) {
        for (const mName of chart.models) {
            const v = data[mName]?.[mk] ?? 0;
            if (v > maxVal) maxVal = v;
        }
    }
    maxVal = maxVal <= 1 ? 1 : maxVal * 1.1;

    const valScale = horizontal
        ? d3.scaleLinear().domain([0, maxVal]).range([0, iw]).nice()
        : d3.scaleLinear().domain([0, maxVal]).range([ih, 0]).nice();

    // Axes
    if (horizontal) {
        g.append('g').call(d3.axisLeft(groupScale)).selectAll('text').attr('fill', '#9ca3af').attr('font-size', '10px');
        g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(valScale).ticks(6).tickFormat(v => v <= 1 ? (v * 100).toFixed(0) + '%' : v))
            .selectAll('text').attr('fill', '#6b7280').attr('font-size', '9px');
    } else {
        g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(groupScale))
            .selectAll('text').attr('fill', '#9ca3af').attr('font-size', '10px')
            .attr('transform', 'rotate(-35)').attr('text-anchor', 'end');
        g.append('g').call(d3.axisLeft(valScale).ticks(6).tickFormat(v => v <= 1 ? (v * 100).toFixed(0) + '%' : v))
            .selectAll('text').attr('fill', '#6b7280').attr('font-size', '9px');
    }
    g.selectAll('.domain, .tick line').attr('stroke', '#334155');

    // Grid
    if (horizontal) {
        g.selectAll('.grid').data(valScale.ticks(6)).join('line')
            .attr('x1', d => valScale(d)).attr('x2', d => valScale(d))
            .attr('y1', 0).attr('y2', ih).attr('stroke', '#1e293b');
    } else {
        g.selectAll('.grid').data(valScale.ticks(6)).join('line')
            .attr('x1', 0).attr('x2', iw)
            .attr('y1', d => valScale(d)).attr('y2', d => valScale(d)).attr('stroke', '#1e293b');
    }

    // Bars
    chart.metrics.forEach((mk, mi) => {
        const label = metricDisplayName(mk);
        chart.models.forEach((model, mdi) => {
            const v = data[model]?.[mk] ?? 0;
            const color = CHART_COLORS[mdi % CHART_COLORS.length];
            if (horizontal) {
                const bar = g.append('rect')
                    .attr('y', groupScale(label) + modelScale(model))
                    .attr('x', 0)
                    .attr('height', modelScale.bandwidth())
                    .attr('width', valScale(v))
                    .attr('fill', color).attr('rx', 3);
                bar.append('title').text(`${model}: ${formatVal(v)}`);
            } else {
                const bar = g.append('rect')
                    .attr('x', groupScale(label) + modelScale(model))
                    .attr('y', valScale(v))
                    .attr('width', modelScale.bandwidth())
                    .attr('height', ih - valScale(v))
                    .attr('fill', color).attr('rx', 3);
                bar.append('title').text(`${model}: ${formatVal(v)}`);
            }
        });
    });

    // Legend
    const lgW = chart.models.length * 160;
    const legend = svg.append('g').attr('transform', `translate(${margin.left + 4}, 2)`);
    legend.append('rect').attr('width', Math.min(lgW + 10, W - margin.left)).attr('height', 22).attr('rx', 6)
        .attr('fill', 'rgba(15,23,42,0.85)').attr('stroke', '#334155').attr('stroke-width', 1);
    chart.models.forEach((name, i) => {
        const lg = legend.append('g').attr('transform', `translate(${i * 160 + 6}, 4)`);
        lg.append('rect').attr('width', 12).attr('height', 12).attr('rx', 3).attr('fill', CHART_COLORS[i % CHART_COLORS.length]);
        lg.append('text').attr('x', 18).attr('y', 10).attr('font-size', '11px').attr('fill', '#e2e8f0').attr('font-weight', '500')
            .text(name.length > 18 ? name.slice(0, 15) + '…' : name);
    });
}

/* ── TABLE CHART ───────────────────────────────── */
function renderTableChart(container, chart, data) {
    // Models as rows, metrics as columns (like main table view)
    let html = '<div class="chart-table-wrap"><table><thead><tr><th>Model</th>';
    for (const mk of chart.metrics) {
        html += `<th>${esc(metricDisplayName(mk))}</th>`;
    }
    html += '</tr></thead><tbody>';

    // Find best per metric for highlighting
    const best = {};
    for (const mk of chart.metrics) {
        let mx = -Infinity;
        for (const m of chart.models) { const v = data[m]?.[mk]; if (v != null && v > mx) mx = v; }
        if (mx > -Infinity) best[mk] = mx;
    }

    for (const model of chart.models) {
        html += `<tr><td class="ct-model">${esc(model)}</td>`;
        for (const mk of chart.metrics) {
            const v = data[model]?.[mk];
            if (v == null) { html += '<td style="color:#4b5563;">—</td>'; }
            else {
                const cls = best[mk] === v ? 'ct-best' : '';
                html += `<td class="${cls}">${formatVal(v)}</td>`;
            }
        }
        html += '</tr>';
    }
    html += '</tbody></table></div>';
    container.innerHTML = html;
}

/* ── PIVOT CHART ───────────────────────────────── */
function renderPivotChart(container, chart, data) {
    // Metrics as rows, models as columns (transposed)
    let html = '<div class="chart-table-wrap"><table><thead><tr><th>Metric</th>';
    for (const model of chart.models) {
        html += `<th>${esc(model)}</th>`;
    }
    html += '</tr></thead><tbody>';

    // Group metrics by family
    const grouped = {};
    for (const mk of chart.metrics) {
        const fam = metricFamily(mk);
        if (!grouped[fam]) grouped[fam] = [];
        grouped[fam].push(mk);
    }

    for (const fam of Object.keys(grouped).sort()) {
        if (Object.keys(grouped).length > 1) {
            html += `<tr><td class="ct-pivot-hdr" colspan="${chart.models.length + 1}">${esc(fam)}</td></tr>`;
        }
        for (const mk of grouped[fam]) {
            // Find best in this row
            let mx = -Infinity;
            for (const m of chart.models) { const v = data[m]?.[mk]; if (v != null && v > mx) mx = v; }

            html += `<tr><td class="ct-model">${esc(metricDisplayName(mk))}</td>`;
            for (const model of chart.models) {
                const v = data[model]?.[mk];
                if (v == null) { html += '<td style="color:#4b5563;">—</td>'; }
                else {
                    const cls = mx === v ? 'ct-best' : '';
                    html += `<td class="${cls}">${formatVal(v)}</td>`;
                }
            }
            html += '</tr>';
        }
    }
    html += '</tbody></table></div>';
    container.innerHTML = html;
}

/* ── DRAG TO REORDER ───────────────────────────── */
let dragChartId = null;

function initDragGrips() {
    document.querySelectorAll('.chart-drag-grip').forEach(grip => {
        grip.addEventListener('mousedown', onDragStart);
        grip.addEventListener('touchstart', onDragStart, { passive: false });
    });
}

function onDragStart(e) {
    e.preventDefault();
    const chartId = e.currentTarget.dataset.chartId;
    const card = document.getElementById('cc-' + chartId);
    if (!card) return;
    dragChartId = chartId;
    card.classList.add('dragging-card');

    function onMove(ev) {
        const cx = ev.touches ? ev.touches[0].clientX : ev.clientX;
        const cy = ev.touches ? ev.touches[0].clientY : ev.clientY;
        // Find card under cursor
        const el = document.elementFromPoint(cx, cy);
        const targetCard = el ? el.closest('.chart-card') : null;
        document.querySelectorAll('.chart-card.drop-target').forEach(c => c.classList.remove('drop-target'));
        if (targetCard && targetCard.dataset.chartId && targetCard.dataset.chartId !== chartId) {
            targetCard.classList.add('drop-target');
        }
    }

    function onUp(ev) {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        document.removeEventListener('touchmove', onMove);
        document.removeEventListener('touchend', onUp);
        card.classList.remove('dragging-card');

        // Find drop target
        const cx = ev.changedTouches ? ev.changedTouches[0].clientX : ev.clientX;
        const cy = ev.changedTouches ? ev.changedTouches[0].clientY : ev.clientY;
        const el = document.elementFromPoint(cx, cy);
        const targetCard = el ? el.closest('.chart-card') : null;
        document.querySelectorAll('.chart-card.drop-target').forEach(c => c.classList.remove('drop-target'));

        if (targetCard && targetCard.dataset.chartId && targetCard.dataset.chartId !== chartId) {
            // Swap positions in saved charts array
            const charts = loadSavedCharts();
            const srcIdx = charts.findIndex(c => c.id === chartId);
            const dstIdx = charts.findIndex(c => c.id === targetCard.dataset.chartId);
            if (srcIdx !== -1 && dstIdx !== -1) {
                const [moved] = charts.splice(srcIdx, 1);
                charts.splice(dstIdx, 0, moved);
                persistCharts(charts);
                renderSavedCharts();
            }
        }
        dragChartId = null;
    }

    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('touchend', onUp);
}
</script>
</body>
</html>
