"""
Cascade Live Server v2 - The Bridge.

Omnidirectional causation visualization.
Bridges: data ↔ human, past ↔ future, signal ↔ meaning.

This is not a dashboard. It's a nervous system made visible.
Multi-dimensional topology - events exist in N-space, rendered in 3D.
"""

import json
import webbrowser
import http.server
import socketserver
import threading
from queue import Queue
import time

event_queue = Queue()

HTML_TEMPLATE = '''<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CASCADE // THE BRIDGE</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
        
        :root {
            --bg: #000;
            --panel: #0a0a0a;
            --surface: #111;
            --border: #1a1a1a;
            --text: #e8e8e8;
            --muted: #555;
            --dim: #333;
            
            --healthy: #00ff88;
            --warning: #ffaa00;
            --critical: #ff3366;
            --info: #00aaff;
            --unknown: #666;
        }
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'JetBrains Mono', monospace;
            background: var(--bg);
            color: var(--text);
            height: 100vh;
            overflow: hidden;
        }
        
        /* === LAYOUT === */
        .container {
            display: grid;
            grid-template-rows: auto 1fr;
            height: 100vh;
        }
        
        /* === TRIAGE HEADER === */
        .triage-bar {
            background: var(--panel);
            border-bottom: 1px solid var(--border);
            padding: 12px 20px;
            display: flex;
            align-items: center;
            gap: 32px;
        }
        
        .triage-status {
            display: flex;
            align-items: center;
            gap: 12px;
        }
        
        .status-badge {
            padding: 6px 16px;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 1px;
            border-radius: 2px;
        }
        
        .status-badge.HEALTHY { background: var(--healthy); color: #000; }
        .status-badge.WARNING { background: var(--warning); color: #000; }
        .status-badge.CRITICAL { background: var(--critical); color: #fff; animation: pulse-critical 1s infinite; }
        .status-badge.UNKNOWN { background: var(--unknown); color: #fff; }
        .status-badge.AWAITING { background: #1a1a1a; color: #666; border: 1px solid #333; }
        .status-badge.LISTENING { background: #1a1a2a; color: #4466aa; border: 1px solid #334; animation: pulse-listen 2s infinite; }
        
        @keyframes pulse-listen { 0%, 100% { border-color: #334; } 50% { border-color: #557; } }
        
        @keyframes pulse-critical { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
        
        .triage-action {
            font-size: 12px;
            color: var(--muted);
        }
        
        .triage-checks {
            display: flex;
            gap: 16px;
            margin-left: auto;
        }
        
        .check-item {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 10px;
            color: var(--muted);
        }
        
        .check-icon { font-size: 12px; }
        .check-icon.ok { color: var(--healthy); }
        .check-icon.fail { color: var(--critical); }
        .check-icon.waiting { color: var(--dim); }
        
        .confidence {
            font-size: 11px;
            color: var(--dim);
            padding-left: 16px;
            border-left: 1px solid var(--border);
        }
        
        /* === MAIN WORKSPACE === */
        .workspace {
            display: grid;
            grid-template-columns: 280px 1fr 320px;
            overflow: hidden;
        }
        
        /* === LEFT: CATEGORIES === */
        .categories-panel {
            background: var(--panel);
            border-right: 1px solid var(--border);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        
        #categories-list {
            flex: 1;
            overflow-y: auto;
        }
        
        .panel-title {
            padding: 12px 16px;
            font-size: 9px;
            font-weight: 700;
            letter-spacing: 2px;
            color: var(--muted);
            border-bottom: 1px solid var(--border);
            position: sticky;
            top: 0;
            background: var(--panel);
        }
        
        .category-group {
            border-bottom: 1px solid var(--border);
        }
        
        .category-header {
            padding: 10px 16px;
            font-size: 10px;
            font-weight: 600;
            color: var(--text);
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: pointer;
            transition: background 0.2s;
        }
        
        .category-header:hover { background: var(--surface); }
        
        .category-badge {
            font-size: 9px;
            padding: 2px 6px;
            border-radius: 2px;
            font-weight: 700;
        }
        
        .category-badge.healthy { background: rgba(0, 255, 136, 0.2); color: var(--healthy); }
        .category-badge.warning { background: rgba(255, 170, 0, 0.2); color: var(--warning); }
        .category-badge.critical { background: rgba(255, 51, 102, 0.2); color: var(--critical); }
        
        .metric-list {
            padding: 0 16px 10px;
        }
        
        .metric-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 0;
            font-size: 10px;
            border-bottom: 1px solid var(--border);
        }
        
        .metric-item:last-child { border-bottom: none; }
        
        .metric-name { color: var(--muted); }
        .metric-value { color: var(--text); font-weight: 500; }
        .metric-trend { font-size: 9px; margin-left: 8px; }
        .metric-trend.rising { color: var(--critical); }
        .metric-trend.falling { color: var(--healthy); }
        .metric-trend.stable { color: var(--muted); }
        
        /* === CENTER: FLOW === */
        .flow-panel {
            background: var(--bg);
            position: relative;
            overflow: hidden;
        }
        
        .flow-canvas {
            width: 100%;
            height: 100%;
            position: relative;
        }
        
        .flow-overlay {
            position: absolute;
            bottom: 20px;
            left: 20px;
            font-size: 10px;
            color: var(--dim);
        }
        
        /* === STATE FEED (Left Panel - TOP) === */
        .state-feed {
            flex: 1;
            background: #050505;
            overflow-y: auto;
            min-height: 150px;
        }
        
        .metrics-section {
            flex: 1;
            border-top: 1px solid var(--border);
            overflow-y: auto;
            min-height: 150px;
        }
        
        .state-entry {
            padding: 10px 12px;
            border-bottom: 1px solid var(--border);
            font-size: 10px;
            cursor: pointer;
            transition: background 0.2s;
        }
        
        .state-entry:hover {
            background: var(--surface);
        }
        
        .state-entry.expanded {
            background: rgba(0, 170, 255, 0.08);
        }
        
        .state-entry:first-child {
            background: rgba(0, 170, 255, 0.05);
        }
        
        @keyframes state-fade-in {
            from { opacity: 0; transform: translateY(-5px); }
            to { opacity: 1; transform: translateY(0); }
        }
        
        .state-time {
            font-size: 8px;
            color: var(--dim);
            margin-bottom: 4px;
        }
        
        .state-equation {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            color: var(--text);
            display: flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
        }
        
        .state-var {
            color: var(--info);
            font-weight: 600;
        }
        
        .state-value {
            color: var(--text);
            background: rgba(255, 255, 255, 0.05);
            padding: 1px 6px;
            border-radius: 2px;
        }
        
        .state-sep {
            color: var(--dim);
        }
        
        .state-phase {
            font-size: 9px;
            color: var(--warning);
            margin-top: 4px;
        }
        
        .state-phase.loading { color: var(--info); }
        .state-phase.training { color: var(--healthy); }
        .state-phase.warning { color: var(--warning); }
        .state-phase.error { color: var(--critical); }
        
        /* Drill-down raw data */
        .state-raw {
            display: none;
            margin-top: 8px;
            padding: 8px;
            background: #000;
            border: 1px solid var(--border);
            font-size: 9px;
            color: var(--muted);
            white-space: pre-wrap;
            word-break: break-all;
            max-height: 100px;
            overflow-y: auto;
        }
        
        .state-entry.expanded .state-raw {
            display: block;
        }
        
        .state-expand-hint {
            float: right;
            font-size: 8px;
            color: var(--dim);
        }
        
        .idle-indicator {
            position: absolute;
            top: 20px;
            right: 20px;
            font-size: 9px;
            color: var(--dim);
            padding: 4px 10px;
            border: 1px solid var(--border);
            border-radius: 2px;
            opacity: 0;
            transition: opacity 0.5s, color 0.3s, border-color 0.3s;
        }
        
        .idle-indicator.active {
            opacity: 1;
            color: #00ff88;
            border-color: #00ff88;
            animation: pulse-activity 4s infinite;
            background: rgba(0, 255, 136, 0.1);
        }
        
        @keyframes pulse-activity {
            0%, 100% { opacity: 0.6; transform: scale(1); }
            50% { opacity: 1; transform: scale(1.02); }
        }
        
        .camera-toggle {
            position: absolute;
            top: 50px;
            right: 20px;
            font-size: 9px;
            color: var(--dim);
            padding: 4px 10px;
            border: 1px solid var(--border);
            border-radius: 2px;
            cursor: pointer;
            transition: all 0.3s;
            user-select: none;
        }
        
        .camera-toggle:hover {
            border-color: var(--muted);
            color: var(--muted);
        }
        
        .camera-toggle.auto-enabled {
            color: #00aaff;
            border-color: #00aaff;
            background: rgba(0, 170, 255, 0.1);
        }
        
        .camera-toggle.manual-mode {
            color: #ff8800;
            border-color: #ff8800;
            background: rgba(255, 136, 0, 0.1);
        }
        
        /* === VIEW TOGGLE (3D/2D) === */
        .view-toggle {
            position: absolute;
            top: 20px;
            left: 20px;
            display: flex;
            gap: 0;
            z-index: 100;
        }
        
        .view-toggle-btn {
            padding: 6px 14px;
            font-size: 10px;
            font-weight: 600;
            font-family: 'JetBrains Mono', monospace;
            color: var(--muted);
            background: var(--panel);
            border: 1px solid var(--border);
            cursor: pointer;
            transition: all 0.2s;
        }
        
        .view-toggle-btn:first-child {
            border-radius: 3px 0 0 3px;
        }
        
        .view-toggle-btn:last-child {
            border-radius: 0 3px 3px 0;
            border-left: none;
        }
        
        .view-toggle-btn:hover {
            border-color: var(--muted);
        }
        
        .view-toggle-btn.active {
            color: #00aaff;
            border-color: #00aaff;
            background: rgba(0, 170, 255, 0.15);
        }
        
        /* === 2D METRICS VIEW (ENGINE DISSECTION) === */
        .metrics-view {
            display: none;
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: var(--bg);
            overflow-y: auto;
            padding: 60px 20px 20px;
        }
        
        .metrics-view.active {
            display: block;
        }
        
        .circuit-section {
            margin-bottom: 24px;
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 4px;
            overflow: hidden;
        }
        
        .circuit-header {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 14px;
            background: var(--surface);
            border-bottom: 1px solid var(--border);
        }
        
        .circuit-color {
            width: 12px;
            height: 12px;
            border-radius: 2px;
        }
        
        .circuit-title {
            font-size: 11px;
            font-weight: 600;
            color: var(--text);
            flex: 1;
        }
        
        .circuit-count {
            font-size: 9px;
            color: var(--muted);
        }
        
        .circuit-chart-container {
            position: relative;
            height: 120px;
            padding: 10px;
        }
        
        .circuit-chart-container canvas {
            width: 100% !important;
            height: 100% !important;
        }
        
        /* Circuit colors */
        .circuit-color.attention { background: #ff66aa; }
        .circuit-color.mlp { background: #66ff88; }
        .circuit-color.norm { background: #ffaa44; }
        .circuit-color.embedding { background: #4488ff; }
        .circuit-color.output { background: #ff4444; }
        .circuit-color.other { background: #888888; }
        
        /* Node drill-down panel */
        .node-drill {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(10, 10, 10, 0.95);
            border: 1px solid var(--border);
            border-radius: 6px;
            padding: 16px 20px;
            min-width: 350px;
            max-width: 500px;
            z-index: 200;
            display: none;
        }
        
        .node-drill.active {
            display: block;
        }
        
        .node-drill-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
        }
        
        .node-drill-title {
            font-size: 11px;
            font-weight: 600;
            color: var(--info);
        }
        
        .node-drill-close {
            cursor: pointer;
            color: var(--muted);
            font-size: 14px;
        }
        
        .node-drill-close:hover {
            color: var(--text);
        }
        
        .node-drill-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 12px;
        }
        
        .node-drill-stat {
            text-align: center;
        }
        
        .node-drill-stat-value {
            font-size: 14px;
            font-weight: 700;
            color: var(--text);
        }
        
        .node-drill-stat-label {
            font-size: 8px;
            color: var(--muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        
        .node-drill-hash {
            margin-top: 12px;
            padding-top: 12px;
            border-top: 1px solid var(--border);
            font-size: 9px;
            color: var(--dim);
        }
        
        .node-drill-hash span {
            color: var(--muted);
        }

        /* === RIGHT: DETAILS === */
        .detail-panel {
            background: var(--panel);
            border-left: 1px solid var(--border);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        
        .detail-scroll {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
        }
        
        .detail-section {
            flex: 1;
            margin-bottom: 12px;
            min-height: 80px;
        }
        
        .detail-label {
            font-size: 9px;
            font-weight: 700;
            letter-spacing: 1.5px;
            color: var(--muted);
            margin-bottom: 8px;
        }
        
        .detail-value {
            font-size: 12px;
            color: var(--text);
        }
        
        .detail-code {
            background: #000;
            border: 1px solid var(--border);
            padding: 10px;
            font-size: 10px;
            color: var(--info);
            white-space: pre-wrap;
            overflow-x: auto;
            max-height: 150px;
        }
        
        /* Correlations */
        .correlation-item {
            display: flex;
            align-items: center;
            padding: 8px 0;
            border-bottom: 1px solid var(--border);
            font-size: 10px;
            transition: all 0.3s ease;
        }
        
        .correlation-item.highlight {
            background: rgba(0, 255, 255, 0.15);
            border-left: 2px solid var(--info);
            padding-left: 8px;
        }
        
        .correlation-pair { flex: 1; color: var(--text); }
        .correlation-coef { 
            font-weight: 700;
            padding: 2px 8px;
            border-radius: 2px;
        }
        .correlation-coef.strong { background: rgba(0, 255, 136, 0.2); color: var(--healthy); }
        .correlation-coef.moderate { background: rgba(0, 170, 255, 0.2); color: var(--info); }
        
        /* === IMMERSIVE CORRELATION - Metric Highlights === */
        .metric-item {
            transition: all 0.3s ease;
            padding: 2px 4px;
            margin: 1px 0;
            border-radius: 2px;
        }
        
        .metric-item.highlight {
            background: rgba(0, 255, 255, 0.2);
            box-shadow: 0 0 8px rgba(0, 255, 255, 0.4);
        }
        
        .metric-item.trace-active {
            animation: metric-pulse 0.8s ease-in-out infinite;
        }
        
        @keyframes metric-pulse {
            0%, 100% { background: rgba(0, 255, 255, 0.1); }
            50% { background: rgba(0, 255, 255, 0.35); box-shadow: 0 0 12px rgba(0, 255, 255, 0.5); }
        }
        
        .category-group {
            transition: all 0.3s ease;
            padding: 4px;
            margin: 2px 0;
            border-radius: 4px;
        }
        
        .category-group.highlight {
            background: rgba(0, 255, 255, 0.08);
            border-left: 2px solid var(--info);
        }
        
        .category-header.pulse {
            animation: header-pulse 1s ease-in-out infinite;
        }
        
        @keyframes header-pulse {
            0%, 100% { color: var(--text); }
            50% { color: var(--info); text-shadow: 0 0 8px rgba(0, 255, 255, 0.6); }
        }
        
        /* Events */
        .event-feed {
            flex: 1;
            border-top: 1px solid var(--border);
            min-height: 150px;
            max-height: 300px;
            overflow-y: auto;
        }
        
        .event-item {
            padding: 8px 16px;
            border-bottom: 1px solid var(--border);
            font-size: 10px;
            cursor: pointer;
            transition: background 0.2s;
        }
        
        .event-item:hover { background: var(--surface); }
        .event-item.selected { background: var(--surface); border-left: 2px solid var(--info); }
        
        .event-type { font-weight: 600; color: var(--text); }
        .event-time { color: var(--dim); float: right; }
        .event-component { color: var(--muted); font-size: 9px; }
        
        /* === CAUSATION CHAIN DRILL-DOWN === */
        .causation-chain {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        
        .causation-node {
            padding: 6px 10px;
            border: 1px solid var(--border);
            border-radius: 3px;
            cursor: pointer;
            transition: all 0.2s;
            display: flex;
            flex-direction: column;
            gap: 2px;
        }
        
        .causation-node:hover {
            background: var(--surface);
            border-color: var(--muted);
        }
        
        .causation-node.past {
            border-left: 2px solid #666;
            margin-left: 12px;
        }
        
        .causation-node.selected {
            border-color: #0ff;
            background: rgba(0, 255, 255, 0.1);
            box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
        }
        
        .causation-node.future {
            border-left: 2px solid #666;
            margin-left: 12px;
        }
        
        .causation-type {
            font-weight: 600;
            font-size: 10px;
            color: var(--text);
        }
        
        .causation-component {
            font-size: 9px;
            color: var(--dim);
        }
        
        .causation-past, .causation-future {
            padding: 8px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 4px;
        }
        
        .causation-center {
            padding: 8px;
            background: rgba(0, 255, 255, 0.05);
            border-radius: 4px;
        }
        
        /* Metric rows in detail */
        .detail-metrics {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }
        
        .metric-row {
            display: flex;
            align-items: center;
            font-size: 10px;
            position: relative;
            padding: 4px 8px;
            background: var(--surface);
            border-radius: 2px;
            overflow: hidden;
        }
        
        .metric-key {
            color: var(--muted);
            min-width: 60px;
        }
        
        .metric-val {
            color: var(--text);
            font-weight: 600;
            flex: 1;
            text-align: right;
        }
        
        .metric-bar {
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            background: linear-gradient(90deg, rgba(0, 255, 136, 0.2), rgba(0, 170, 255, 0.2));
            z-index: -1;
        }
        
        /* 3D hover tooltip */
        .node-tooltip {
            position: fixed;
            background: rgba(0, 0, 0, 0.95);
            border: 1px solid var(--border);
            padding: 8px 12px;
            border-radius: 4px;
            font-size: 10px;
            pointer-events: none;
            z-index: 1000;
            max-width: 200px;
        }
        
        .node-tooltip .tt-type {
            font-weight: 700;
            color: #0ff;
        }
        
        .node-tooltip .tt-component {
            color: var(--muted);
            font-size: 9px;
        }
        
        /* === MOBILE RESPONSIVE === */
        /* Phone-first: visualization dominates, panels collapse */
        
        @media (max-width: 768px) {
            .container {
                grid-template-rows: auto 1fr auto;
            }
            
            .triage-bar {
                padding: 8px 12px;
                gap: 12px;
                flex-wrap: wrap;
            }
            
            .triage-checks {
                display: none;  /* Hide checks on mobile */
            }
            
            .confidence {
                display: none;  /* Hide on mobile */
            }
            
            .status-badge {
                padding: 4px 10px;
                font-size: 10px;
            }
            
            .triage-action {
                font-size: 10px;
                flex: 1;
            }
            
            /* Stack layout on mobile */
            .workspace {
                grid-template-columns: 1fr;
                grid-template-rows: 1fr;
                position: relative;
            }
            
            /* HIDE side panels by default on mobile */
            .categories-panel,
            .detail-panel {
                display: none;
            }
            
            /* Visualization takes full screen */
            .flow-panel {
                grid-column: 1;
                grid-row: 1;
            }
            
            .flow-canvas {
                height: 100% !important;
            }
            
            .flow-overlay {
                bottom: 60px;
                font-size: 10px;
            }
            
            /* Mobile nav tabs at bottom */
            .mobile-nav {
                display: flex !important;
                position: fixed;
                bottom: 0;
                left: 0;
                right: 0;
                background: var(--panel);
                border-top: 1px solid var(--border);
                z-index: 100;
                padding: 8px;
                gap: 4px;
            }
            
            .mobile-nav-btn {
                flex: 1;
                padding: 10px 8px;
                background: var(--surface);
                border: 1px solid var(--border);
                border-radius: 4px;
                color: var(--muted);
                font-size: 9px;
                font-family: inherit;
                text-transform: uppercase;
                letter-spacing: 1px;
                cursor: pointer;
                transition: all 0.2s;
            }
            
            .mobile-nav-btn.active {
                background: var(--info);
                color: #000;
                border-color: var(--info);
            }
            
            .mobile-nav-btn:hover {
                border-color: var(--info);
            }
            
            /* When panel is shown on mobile */
            .categories-panel.mobile-show,
            .detail-panel.mobile-show {
                display: flex;
                position: fixed;
                top: 50px;
                left: 0;
                right: 0;
                bottom: 50px;
                z-index: 50;
                width: 100%;
                border: none;
            }
            
            .categories-panel.mobile-show {
                background: rgba(10, 10, 10, 0.98);
            }
            
            .detail-panel.mobile-show {
                background: rgba(10, 10, 10, 0.98);
            }
            
            /* Event feed on mobile */
            .event-feed {
                max-height: 150px;
            }
            
            .event-item {
                padding: 6px 12px;
            }
            
            /* Simplify state feed on mobile */
            .state-feed {
                max-height: 200px;
            }
            
            .state-entry {
                padding: 8px 12px;
            }
        }
        
        /* Hide mobile nav on desktop */
        .mobile-nav {
            display: none;
        }
        
        /* Tablet tweaks */
        @media (min-width: 769px) and (max-width: 1024px) {
            .workspace {
                grid-template-columns: 220px 1fr 260px;
            }
            
            .triage-checks {
                gap: 10px;
            }
            
            .check-item {
                font-size: 9px;
            }
        }

        /* === MOBILE ONLY (≤768px) - Desktop unchanged === */
        .mobile-nav { display: none; }
        
        @media (max-width: 768px) {
            .triage-bar { padding: 8px 12px; gap: 12px; flex-wrap: wrap; }
            .triage-checks, .confidence { display: none; }
            .status-badge { padding: 4px 10px; font-size: 10px; }
            .triage-action { font-size: 10px; flex: 1; }
            
            .workspace {
                grid-template-columns: 1fr;
                grid-template-rows: 1fr;
                position: relative;
            }
            
            .categories-panel, .detail-panel { display: none; }
            .flow-panel { grid-column: 1; grid-row: 1; }
            .flow-canvas { height: 100% !important; }
            .flow-overlay { bottom: 60px; font-size: 10px; }
            
            .mobile-nav {
                display: flex !important;
                position: fixed;
                bottom: 0; left: 0; right: 0;
                background: var(--panel);
                border-top: 1px solid var(--border);
                z-index: 100;
                padding: 8px;
                gap: 4px;
            }
            
            .mobile-nav-btn {
                flex: 1;
                padding: 10px 8px;
                background: var(--surface);
                border: 1px solid var(--border);
                border-radius: 4px;
                color: var(--muted);
                font-size: 9px;
                font-family: inherit;
                text-transform: uppercase;
                letter-spacing: 1px;
                cursor: pointer;
            }
            
            .mobile-nav-btn.active {
                background: var(--info);
                color: #000;
                border-color: var(--info);
            }
            
            .categories-panel.mobile-show,
            .detail-panel.mobile-show {
                display: flex;
                position: fixed;
                top: 50px; left: 0; right: 0; bottom: 50px;
                z-index: 50;
                width: 100%;
                border: none;
                background: rgba(10, 10, 10, 0.98);
            }
        }

    </style>
</head>
<body>
    <div class="container">
        <!-- TRIAGE BAR -->
        <div class="triage-bar">
            <div class="triage-status">
                <div class="status-badge AWAITING" id="triage-badge">AWAITING</div>
                <div class="triage-action" id="triage-action">Connecting to stream...</div>
            </div>
            
            <div class="triage-checks" id="triage-checks">
                <div class="check-item"><span class="check-icon">○</span> loss</div>
                <div class="check-item"><span class="check-icon">○</span> gradient</div>
                <div class="check-item"><span class="check-icon">○</span> efficiency</div>
                <div class="check-item"><span class="check-icon">○</span> overfit</div>
                <div class="check-item"><span class="check-icon">○</span> anomalies</div>
            </div>
            
            <div class="confidence" id="confidence">--</div>
        </div>
        
        <!-- MAIN WORKSPACE -->
        <div class="workspace">
            <!-- LEFT: State & Categories -->
            <div class="categories-panel">
                <!-- STATE FEED (TOP) -->
                <div class="state-feed" id="state-feed">
                    <div class="panel-title">STATE =</div>
                    <div id="state-entries">
                        <div class="state-entry">
                            <div class="state-time">--:--:--</div>
                            <div class="state-equation"><span class="state-value">awaiting signal...</span></div>
                        </div>
                    </div>
                </div>
                
                <!-- METRICS (BOTTOM) -->
                <div class="metrics-section">
                    <div class="panel-title">METRIC CATEGORIES</div>
                    <div id="categories-list"></div>
                </div>
            </div>
            
            <!-- CENTER: Flow Visualization - 3D -->
            <div class="flow-panel">
                <!-- View Toggle -->
                <div class="view-toggle">
                    <button class="view-toggle-btn active" data-view="3d" onclick="toggleView('3d')">◉ 3D</button>
                    <button class="view-toggle-btn" data-view="2d" onclick="toggleView('2d')">≡ 2D</button>
                </div>
                
                <!-- 3D Canvas -->
                <div class="flow-canvas" id="flow-canvas"></div>
                
                <!-- 2D Metrics View (Engine Dissection) -->
                <div class="metrics-view" id="metrics-view">
                    <div id="circuit-traces">
                        <!-- Dynamic circuit trace charts will be inserted here -->
                        <div style="color: var(--muted); text-align: center; padding: 40px;">
                            Run a model to see circuit traces...
                        </div>
                    </div>
                </div>
                
                <div class="flow-overlay">
                    <div>Events: <span id="event-count">0</span></div>
                    <div>Metrics: <span id="metric-count">0</span></div>
                    <div style="margin-top: 4px; color: #444;">drag to orbit • scroll to zoom</div>
                </div>
                <div class="idle-indicator" id="idle-indicator">◎ OBSERVING</div>
                <div class="camera-toggle auto-enabled" id="camera-toggle" title="Toggle auto-camera">⟐ AUTO</div>
            </div>
            
            <!-- Node Drill-Down Panel -->
            <div class="node-drill" id="node-drill">
                <div class="node-drill-header">
                    <div class="node-drill-title" id="drill-title">Layer Details</div>
                    <span class="node-drill-close" onclick="closeNodeDrill()">✕</span>
                </div>
                <div class="node-drill-grid" id="drill-grid"></div>
                <div class="node-drill-hash" id="drill-hash"></div>
            </div>
            
            <!-- RIGHT: Details -->
            <div class="detail-panel">
                <div class="panel-title">CORRELATIONS & DETAIL</div>
                <div class="detail-scroll" id="detail-scroll">
                    <div class="detail-section">
                        <div class="detail-label">CORRELATIONS</div>
                        <div id="correlations-list">
                            <div style="color: var(--dim); font-size: 10px;">Awaiting data...</div>
                        </div>
                    </div>
                    
                    <div class="detail-section">
                        <div class="detail-label">SELECTED EVENT</div>
                        <div id="event-detail">
                            <div style="color: var(--dim); font-size: 10px;">Select an event</div>
                        </div>
                    </div>
                    
                    <div class="detail-section">
                        <div class="detail-label">PROVENANCE</div>
                        <div id="provenance-info">
                            <div style="color: var(--dim); font-size: 10px;">Awaiting data...</div>
                        </div>
                    </div>
                    
                    <div class="detail-section">
                        <div class="detail-label">ARCHITECTURE</div>
                        <div id="architecture-info">
                            <div style="color: var(--dim); font-size: 10px;">Loading model...</div>
                        </div>
                    </div>
                    
                    <div class="detail-section">
                        <div class="detail-label">HEALTH ISSUES</div>
                        <div id="health-issues">
                            <div style="color: var(--dim); font-size: 10px;">None detected</div>
                        </div>
                    </div>
                </div>
                
                <div class="event-feed" id="event-feed"></div>
            </div>
        </div>
        
        <!-- MOBILE NAVIGATION -->
        <div class="mobile-nav" id="mobile-nav">
            <button class="mobile-nav-btn active" data-panel="viz" onclick="showMobilePanel('viz')">◉ Visual</button>
            <button class="mobile-nav-btn" data-panel="metrics" onclick="showMobilePanel('metrics')">≡ Metrics</button>
            <button class="mobile-nav-btn" data-panel="detail" onclick="showMobilePanel('detail')">⋮ Detail</button>
        </div>
    </div>

<script>
// === MOBILE PANEL TOGGLE ===
function showMobilePanel(panel) {
    const categoriesPanel = document.querySelector('.categories-panel');
    const detailPanel = document.querySelector('.detail-panel');
    const flowPanel = document.querySelector('.flow-panel');
    
    // Update nav buttons
    document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.panel === panel);
    });
    
    // Hide all panels first
    categoriesPanel.classList.remove('mobile-show');
    detailPanel.classList.remove('mobile-show');
    
    // Show selected panel
    if (panel === 'metrics') {
        categoriesPanel.classList.add('mobile-show');
    } else if (panel === 'detail') {
        detailPanel.classList.add('mobile-show');
    }
    // 'viz' just hides the overlays, visualization is always there
}

// === STATE ===
const events = new Map();
let metrics = {};
let triage = null;
let selectedEventId = null;

// === CIRCUIT TRACE STATE (2D Metrics View) ===
let currentView = '3d';  // '3d' or '2d'
const circuitData = {
    attention: { name: 'Attention (Q/K/V/O)', color: '#ff66aa', layers: [] },
    mlp: { name: 'MLP (Gate/Up/Down)', color: '#66ff88', layers: [] },
    norm: { name: 'Layer Norm', color: '#ffaa44', layers: [] },
    embedding: { name: 'Embeddings', color: '#4488ff', layers: [] },
    output: { name: 'Output/LM Head', color: '#ff4444', layers: [] },
    other: { name: 'Other', color: '#888888', layers: [] }
};
let circuitCharts = {};  // Chart.js instances

// Classify a layer name into circuit type
function classifyCircuit(name) {
    const lower = name.toLowerCase();
    if (lower.includes('q_proj') || lower.includes('k_proj') || lower.includes('v_proj') || 
        lower.includes('o_proj') || lower.includes('self_attn') || lower.includes('attention')) {
        return 'attention';
    }
    if (lower.includes('mlp') || lower.includes('gate_proj') || lower.includes('up_proj') || 
        lower.includes('down_proj') || lower.includes('fc1') || lower.includes('fc2')) {
        return 'mlp';
    }
    if (lower.includes('norm') || lower.includes('layernorm') || lower.includes('rmsnorm')) {
        return 'norm';
    }
    if (lower.includes('embed') || lower.includes('wte') || lower.includes('wpe')) {
        return 'embedding';
    }
    if (lower.includes('lm_head') || lower.includes('output') || lower.includes('classifier')) {
        return 'output';
    }
    return 'other';
}

// === VIEW TOGGLE ===
function toggleView(view) {
    currentView = view;
    
    // Update buttons
    document.querySelectorAll('.view-toggle-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.view === view);
    });
    
    // Toggle visibility
    const flowCanvas = document.getElementById('flow-canvas');
    const metricsView = document.getElementById('metrics-view');
    
    if (view === '3d') {
        flowCanvas.style.display = 'block';
        metricsView.classList.remove('active');
    } else {
        flowCanvas.style.display = 'none';
        metricsView.classList.add('active');
        renderCircuitTraces();
    }
}

// === CIRCUIT TRACE RENDERING ===
function renderCircuitTraces() {
    const container = document.getElementById('circuit-traces');
    
    // Check if we have any data
    const hasData = Object.values(circuitData).some(c => c.layers.length > 0);
    if (!hasData) {
        container.innerHTML = `
            <div style="color: var(--muted); text-align: center; padding: 40px;">
                <div style="font-size: 24px; margin-bottom: 12px;">⚡</div>
                <div>Run a model to see circuit traces</div>
                <div style="font-size: 10px; margin-top: 8px; color: var(--dim);">
                    Each circuit type (attention, MLP, etc.) will be traced separately
                </div>
            </div>
        `;
        return;
    }
    
    // Build chart sections for each circuit type with data
    let html = '';
    for (const [type, data] of Object.entries(circuitData)) {
        if (data.layers.length === 0) continue;
        
        html += `
            <div class="circuit-section">
                <div class="circuit-header">
                    <div class="circuit-color ${type}"></div>
                    <div class="circuit-title">${data.name}</div>
                    <div class="circuit-count">${data.layers.length} layers</div>
                </div>
                <div class="circuit-chart-container">
                    <canvas id="chart-${type}"></canvas>
                </div>
            </div>
        `;
    }
    
    container.innerHTML = html;
    
    // Create Chart.js instances
    setTimeout(() => {
        for (const [type, data] of Object.entries(circuitData)) {
            if (data.layers.length === 0) continue;
            
            const canvas = document.getElementById(`chart-${type}`);
            if (!canvas) continue;
            
            // Destroy existing chart if any
            if (circuitCharts[type]) {
                circuitCharts[type].destroy();
            }
            
            const ctx = canvas.getContext('2d');
            
            // Prepare data - mean and std traces
            const labels = data.layers.map((_, i) => i);
            const meanData = data.layers.map(l => l.mean);
            const stdData = data.layers.map(l => l.std);
            
            circuitCharts[type] = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [
                        {
                            label: 'Mean',
                            data: meanData,
                            borderColor: data.color,
                            backgroundColor: data.color + '33',
                            fill: true,
                            tension: 0.3,
                            pointRadius: 0,
                            borderWidth: 2
                        },
                        {
                            label: 'Std',
                            data: stdData,
                            borderColor: data.color + '88',
                            backgroundColor: 'transparent',
                            fill: false,
                            tension: 0.3,
                            pointRadius: 0,
                            borderWidth: 1,
                            borderDash: [4, 2]
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: {
                        intersect: false,
                        mode: 'index'
                    },
                    plugins: {
                        legend: {
                            display: true,
                            position: 'top',
                            labels: {
                                color: '#888',
                                font: { size: 9, family: 'JetBrains Mono' },
                                boxWidth: 20,
                                padding: 8
                            }
                        },
                        tooltip: {
                            backgroundColor: 'rgba(0,0,0,0.9)',
                            titleFont: { size: 10, family: 'JetBrains Mono' },
                            bodyFont: { size: 10, family: 'JetBrains Mono' },
                            callbacks: {
                                title: (items) => {
                                    const idx = items[0].dataIndex;
                                    const layer = data.layers[idx];
                                    return layer ? layer.name : `Layer ${idx}`;
                                },
                                afterBody: (items) => {
                                    const idx = items[0].dataIndex;
                                    const layer = data.layers[idx];
                                    if (!layer) return '';
                                    return [
                                        `min: ${layer.min?.toFixed(4) || 'N/A'}`,
                                        `max: ${layer.max?.toFixed(4) || 'N/A'}`,
                                        `hash: ${layer.hash?.slice(0, 8) || 'N/A'}`
                                    ];
                                }
                            }
                        }
                    },
                    scales: {
                        x: {
                            display: true,
                            title: {
                                display: true,
                                text: 'Layer Index',
                                color: '#555',
                                font: { size: 9 }
                            },
                            ticks: { color: '#444', font: { size: 8 } },
                            grid: { color: '#1a1a1a' }
                        },
                        y: {
                            display: true,
                            title: {
                                display: true,
                                text: 'Activation',
                                color: '#555',
                                font: { size: 9 }
                            },
                            ticks: { color: '#444', font: { size: 8 } },
                            grid: { color: '#1a1a1a' }
                        }
                    },
                    onClick: (evt, elements) => {
                        if (elements.length > 0) {
                            const idx = elements[0].index;
                            const layer = data.layers[idx];
                            if (layer) showNodeDrill(layer);
                        }
                    }
                }
            });
        }
    }, 50);
}

// === NODE DRILL-DOWN ===
function showNodeDrill(layer) {
    const drill = document.getElementById('node-drill');
    const title = document.getElementById('drill-title');
    const grid = document.getElementById('drill-grid');
    const hash = document.getElementById('drill-hash');
    
    title.textContent = layer.name || 'Unknown Layer';
    
    grid.innerHTML = `
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${layer.mean?.toFixed(4) || 'N/A'}</div>
            <div class="node-drill-stat-label">Mean</div>
        </div>
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${layer.std?.toFixed(4) || 'N/A'}</div>
            <div class="node-drill-stat-label">Std Dev</div>
        </div>
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${(layer.sparsity * 100)?.toFixed(1) || '0'}%</div>
            <div class="node-drill-stat-label">Sparsity</div>
        </div>
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${layer.min?.toFixed(4) || 'N/A'}</div>
            <div class="node-drill-stat-label">Min</div>
        </div>
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${layer.max?.toFixed(4) || 'N/A'}</div>
            <div class="node-drill-stat-label">Max</div>
        </div>
        <div class="node-drill-stat">
            <div class="node-drill-stat-value">${layer.idx || '?'}</div>
            <div class="node-drill-stat-label">Position</div>
        </div>
    `;
    
    hash.innerHTML = `
        <span>Hash:</span> ${layer.hash || 'N/A'}<br>
        <span>Parent:</span> ${layer.parent_hash || 'N/A'}
    `;
    
    drill.classList.add('active');
}

function closeNodeDrill() {
    document.getElementById('node-drill').classList.remove('active');
}

// === SSE CONNECTION ===
// On HuggingFace Spaces, events are injected via script (no SSE endpoint)
// On standalone server, SSE is used for real-time updates
let eventSource = null;

// Detect embedded mode BEFORE trying SSE
// In iframe = HF Space = no SSE endpoint exists
const isEmbedded = (window.parent !== window) || window.frameElement !== null;

function connect() {
    // In embedded mode (HF Space iframe), don't use SSE at all
    // Events come via inject_events script from Python
    if (isEmbedded) {
        document.getElementById('triage-badge').className = 'status-badge LISTENING';
        document.getElementById('triage-badge').textContent = 'READY';
        document.getElementById('triage-action').textContent = 'select scenario → run';
        return;  // No SSE - events injected by Gradio
    }
    
    // Standalone mode - use SSE
    if (eventSource) {
        eventSource.close();
    }
    
    eventSource = new EventSource('/events');
    
    eventSource.onopen = () => {
        document.getElementById('triage-badge').className = 'status-badge LISTENING';
        document.getElementById('triage-badge').textContent = 'LISTENING';
        document.getElementById('triage-action').textContent = 'Stream connected. Waiting for metrics...';
    };
    
    eventSource.onerror = (e) => {
        document.getElementById('triage-badge').className = 'status-badge AWAITING';
        document.getElementById('triage-badge').textContent = 'OFFLINE';
        document.getElementById('triage-action').textContent = 'Stream lost. Reconnecting...';
        
        // EventSource auto-reconnects, no manual intervention needed
    };
    
    eventSource.onmessage = (e) => {
        if (e.data.startsWith(':')) return;
        try {
            const data = JSON.parse(e.data);
            handleData(data);
        } catch(err) {
            console.error('Parse error:', err);
        }
    };
}

// === DATA HANDLER ===
function handleData(data) {
    // Update status when receiving data
    if (isEmbedded) {
        document.getElementById('triage-badge').className = 'status-badge LISTENING';
        document.getElementById('triage-badge').textContent = 'STREAMING';
        document.getElementById('triage-action').textContent = 'Receiving events from model...';
    }
    
    // Store event
    if (data.event) {
        events.set(data.event.event_id, data.event);
        addEventToFeed(data.event);
        
        // Check for provenance data in event
        const evtData = data.event.data || {};
        if (evtData.merkle_root) {
            provenanceData.merkle_root = evtData.merkle_root;
            provenanceData.chain_length = evtData.chain_length || 0;
            provenanceData.model_hash = evtData.model_hash || provenanceData.model_hash;
            provenanceData.input_hash = evtData.input_hash || provenanceData.input_hash;
            renderProvenance();
        }
        if (evtData.model_hash && !provenanceData.model_hash) {
            provenanceData.model_hash = evtData.model_hash;
            renderProvenance();
        }
        if (evtData.input_hash && !provenanceData.input_hash) {
            provenanceData.input_hash = evtData.input_hash;
            renderProvenance();
        }
        
        // === COLLECT CIRCUIT DATA FOR 2D VIEW ===
        // Activation events have mean, std, min, max, hash, etc.
        if (data.event.event_type === 'activation' && evtData.mean !== undefined) {
            const layerName = data.event.component || '';
            const circuitType = classifyCircuit(layerName);
            
            const layerData = {
                name: layerName,
                mean: evtData.mean,
                std: evtData.std,
                min: evtData.min,
                max: evtData.max,
                sparsity: evtData.sparsity || 0,
                hash: evtData.hash,
                parent_hash: evtData.parent_hash,
                idx: evtData.layer_idx || circuitData[circuitType].layers.length
            };
            
            circuitData[circuitType].layers.push(layerData);
            
            // If in 2D view, update charts periodically
            if (currentView === '2d' && circuitData[circuitType].layers.length % 20 === 0) {
                renderCircuitTraces();
            }
        }
        
        // Clear circuit data on new inference (detect via system state change to observing)
        if (data.event.event_type === 'state' && evtData.status === 'observing') {
            // Reset circuit data for fresh run
            for (const type of Object.keys(circuitData)) {
                circuitData[type].layers = [];
            }
            // Clear ALL traces to prevent stacking on new runs
            activeTraces = [];
            console.log('[CASCADE] Cleared all traces for new observation');
        }
        
        // Capture model architecture from config events
        if (evtData.hidden_size) modelArchitecture.hidden_size = evtData.hidden_size;
        if (evtData.num_layers) modelArchitecture.num_layers = evtData.num_layers;
        if (evtData.num_heads) modelArchitecture.num_heads = evtData.num_heads;
        if (evtData.model_type) modelArchitecture.model_type = evtData.model_type;
        if (evtData.architectures) modelArchitecture.architectures = evtData.architectures;
        // Render architecture panel if we got any arch data
        if (evtData.hidden_size || evtData.num_layers) renderArchitecture();
    }
    
    // Update metrics
    if (data.metrics) {
        metrics = data.metrics;
        renderCategories();
        renderCorrelations();
        renderHealthIssues();
    }
    
    // Update triage
    if (data.triage) {
        triage = data.triage;
        renderTriage();
    }
    
    // Update counts
    document.getElementById('event-count').textContent = events.size;
    document.getElementById('metric-count').textContent = metrics.metric_count || 0;
    
    scheduleRender();  // Use throttled render
    renderStateSnapshot(data);
}

// === RENDER: STATE FEED ===
let lastStateKey = '';  // Avoid duplicate consecutive states

function renderStateSnapshot(data) {
    const entriesEl = document.getElementById('state-entries');
    
    // Get the latest event
    const latestEvent = data.event;
    const m = metrics.metrics || {};
    
    // Build state equation parts
    const parts = [];
    
    // Current phase from latest event
    let phase = 'observing';
    let phaseIcon = '◉';
    let phaseClass = '';
    let activity = '';
    
    if (latestEvent) {
        const evtType = latestEvent.event_type || '';
        const component = latestEvent.component || '';
        const raw = latestEvent.raw || '';
        
        // Extract meaningful activity from raw text
        if (raw.includes('AutoencoderKL') || raw.includes('VAE')) {
            activity = 'loading VAE';
            phase = 'loading';
            phaseIcon = '⟳';
            phaseClass = 'loading';
        } else if (raw.includes('UNet') || raw.includes('unet')) {
            activity = 'loading UNet';
            phase = 'loading';
            phaseIcon = '⟳';
            phaseClass = 'loading';
        } else if (raw.includes('CLIPTextModel') || raw.includes('text_encoder')) {
            activity = 'loading CLIP';
            phase = 'loading';
            phaseIcon = '⟳';
            phaseClass = 'loading';
        } else if (raw.includes('tokenizer')) {
            activity = 'loading tokenizer';
            phase = 'loading';
            phaseIcon = '⟳';
            phaseClass = 'loading';
        } else if (raw.includes('scheduler') || raw.includes('DDPM')) {
            activity = 'loading scheduler';
            phase = 'loading';
            phaseIcon = '⟳';
            phaseClass = 'loading';
        } else if (raw.includes('checkpoint') || raw.includes('Checkpoint')) {
            activity = 'checkpoint';
            phase = 'checkpoint';
            phaseIcon = '💾';
        } else if (raw.includes('Device:') || raw.includes('cuda')) {
            activity = 'GPU: cuda';
            phase = 'device init';
            phaseIcon = '⚡';
        } else if (raw.includes('Mixed precision')) {
            const match = raw.match(/Mixed precision[^:]*:\\s*(\\w+)/i);
            activity = match ? `precision: ${match[1]}` : 'mixed precision';
            phase = 'config';
            phaseIcon = '⚙';
        } else if (raw.includes('Distributed')) {
            activity = 'distributed setup';
            phase = 'config';
            phaseIcon = '⚙';
        } else if (raw.includes('Step') || raw.includes('step')) {
            const match = raw.match(/[Ss]tep[s]?\\s*[=:]?\\s*(\\d+)/);
            activity = match ? `step ${match[1]}` : 'training';
            phase = 'training';
            phaseIcon = '▶';
            phaseClass = 'training';
        } else if (raw.includes('loss') || raw.includes('Loss')) {
            const match = raw.match(/[Ll]oss[^0-9]*([0-9.]+)/);
            activity = match ? `loss=${match[1]}` : 'training';
            phase = 'training';
            phaseIcon = '▶';
            phaseClass = 'training';
        } else if (evtType === 'warning') {
            const lines = raw.split('\\n');
            activity = lines[0].substring(0, 35) || 'warning';
            phase = 'warning';
            phaseIcon = '⚠';
            phaseClass = 'warning';
        } else if (evtType === 'inference') {
            activity = 'inference';
            phase = 'inference';
            phaseIcon = '◆';
        } else if (evtType === 'config') {
            activity = 'configuring';
            phase = 'config';
            phaseIcon = '⚙';
        } else if (evtType.includes('state_change')) {
            phase = 'init';
            phaseIcon = '◈';
        }
        
        // Add component to parts
        if (component && component !== 'unknown') {
            parts.push({ type: 'var', value: component });
        }
        
        // Add activity if we have one
        if (activity) {
            parts.push({ type: 'activity', value: activity });
        }
    }
    
    // Add key metrics if available
    if (m.loss?.current !== undefined) {
        parts.push({ type: 'metric', name: 'loss', value: m.loss.current.toFixed(4) });
    }
    if (m.iter?.current !== undefined || m.step?.current !== undefined) {
        const step = m.iter?.current ?? m.step?.current;
        parts.push({ type: 'metric', name: 'step', value: Math.round(step) });
    }
    if (m.lr?.current !== undefined) {
        parts.push({ type: 'metric', name: 'lr', value: m.lr.current.toExponential(1) });
    }
    if (m.mfu?.current !== undefined) {
        parts.push({ type: 'metric', name: 'mfu', value: (m.mfu.current * 100).toFixed(1) + '%' });
    }
    
    // Build equation HTML
    let eqHtml;
    if (parts.length === 0) {
        eqHtml = '<span class="state-value">...</span>';
    } else {
        eqHtml = parts.map((p, i) => {
            const sep = i > 0 ? '<span class="state-sep"> | </span>' : '';
            if (p.type === 'var') {
                return sep + `<span class="state-var">${p.value}</span>`;
            } else if (p.type === 'activity') {
                return sep + `<span class="state-value">${p.value}</span>`;
            } else {
                return sep + `<span class="state-value">${p.name}=${p.value}</span>`;
            }
        }).join('');
    }
    
    // Create state key to avoid duplicates
    const stateKey = `${phase}:${eqHtml}`;
    if (stateKey === lastStateKey) return;
    lastStateKey = stateKey;
    
    // Get timestamp
    const now = new Date();
    const timeStr = now.toLocaleTimeString('en-US', { hour12: false });
    
    // Get raw data for drill-down (escape HTML)
    const rawText = latestEvent?.raw || '';
    const escapedRaw = rawText
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
    
    // Create new entry with drill-down
    const entry = document.createElement('div');
    entry.className = 'state-entry';
    entry.innerHTML = `
        <div class="state-time">${timeStr} <span class="state-expand-hint">▼ click to expand</span></div>
        <div class="state-equation">${eqHtml}</div>
        <div class="state-phase ${phaseClass}">${phaseIcon} ${phase}</div>
        <div class="state-raw">${escapedRaw || '(no raw data)'}</div>
    `;
    
    // Click to expand/collapse
    entry.addEventListener('click', () => {
        entry.classList.toggle('expanded');
    });
    
    // Insert at top
    entriesEl.insertBefore(entry, entriesEl.firstChild);
    
    // Limit entries to 50
    while (entriesEl.children.length > 10000) {
        entriesEl.removeChild(entriesEl.lastChild);
    }
}

// === RENDER: TRIAGE BAR ===
function renderTriage() {
    if (!triage) return;
    
    const badge = document.getElementById('triage-badge');
    badge.className = `status-badge ${triage.status}`;
    badge.textContent = triage.status;
    
    document.getElementById('triage-action').textContent = triage.action || '';
    document.getElementById('confidence').textContent = 
        `${Math.round((triage.confidence || 0) * 100)}% confidence`;
    
    // Update checks
    const checksEl = document.getElementById('triage-checks');
    const checkNames = ['loss_progress', 'gradient_health', 'efficiency', 'overfitting', 'anomalies'];
    const checkLabels = ['loss', 'gradient', 'efficiency', 'overfit', 'anomalies'];
    
    checksEl.innerHTML = checkNames.map((name, i) => {
        const check = triage.checks?.[name];
        const status = check?.status || 'waiting';
        const ok = check?.ok !== false;
        
        // Different icons for different states
        let icon, iconClass;
        if (status === 'waiting') {
            icon = '○';  // Hollow circle = waiting
            iconClass = 'waiting';
        } else if (ok) {
            icon = '✓';
            iconClass = 'ok';
        } else {
            icon = '✗';
            iconClass = 'fail';
        }
        
        return `
            <div class="check-item" title="${check?.detail || ''}">
                <span class="check-icon ${iconClass}">${icon}</span>
                ${checkLabels[i]}
            </div>
        `;
    }).join('');
}

// === RENDER: CATEGORIES ===
function renderCategories() {
    const container = document.getElementById('categories-list');
    const byCategory = metrics.metrics_by_category || {};
    
    container.innerHTML = Object.entries(byCategory).map(([catName, info]) => {
        // Determine category health
        const healths = info.health || [];
        const hasCritical = healths.includes('critical');
        const hasWarning = healths.includes('warning');
        const healthClass = hasCritical ? 'critical' : hasWarning ? 'warning' : 'healthy';
        
        // Get metric details
        const metricItems = (info.metrics || []).map(name => {
            const m = metrics.metrics?.[name];
            if (!m) return '';
            
            const trendClass = m.trend === 'rising' ? 'rising' : 
                              m.trend === 'falling' ? 'falling' : 'stable';
            const trendArrow = m.trend === 'rising' ? '↑' : 
                              m.trend === 'falling' ? '↓' : '→';
            
            return `
                <div class="metric-item" data-metric="${name}" data-category="${catName}">
                    <span class="metric-name">${name}</span>
                    <span>
                        <span class="metric-value">${m.current?.toFixed(4) ?? '--'}</span>
                        <span class="metric-trend ${trendClass}">${trendArrow}</span>
                    </span>
                </div>
            `;
        }).join('');
        
        return `
            <div class="category-group" data-category="${catName}">
                <div class="category-header">
                    <span>${catName}</span>
                    <span class="category-badge ${healthClass}">${info.count}</span>
                </div>
                <div class="metric-list">${metricItems}</div>
            </div>
        `;
    }).join('');
}

// === RENDER: CORRELATIONS ===
function renderCorrelations() {
    const container = document.getElementById('correlations-list');
    const corrs = metrics.correlations || [];
    
    if (corrs.length === 0) {
        container.innerHTML = '<div style="color: var(--dim); font-size: 10px;">Awaiting sufficient data...</div>';
        return;
    }
    
    container.innerHTML = corrs.slice(0, 8).map(c => {
        const strength = Math.abs(c.r) > 0.8 ? 'strong' : 'moderate';
        const sign = c.r > 0 ? '+' : '';
        return `
            <div class="correlation-item" data-metrics="${c.a},${c.b}">
                <span class="correlation-pair">${c.a} ↔ ${c.b}</span>
                <span class="correlation-coef ${strength}">${sign}${c.r.toFixed(2)}</span>
            </div>
        `;
    }).join('');
}

// === RENDER: PROVENANCE INFO ===
let provenanceData = {
    merkle_root: null,
    model_hash: null,
    input_hash: null,
    chain_length: 0
};

function renderProvenance() {
    const container = document.getElementById('provenance-info');
    
    if (!provenanceData.merkle_root) {
        container.innerHTML = '<div style="color: var(--dim); font-size: 10px;">Awaiting data...</div>';
        return;
    }
    
    container.innerHTML = `
        <div style="font-family: monospace; font-size: 9px;">
            <div style="margin-bottom: 6px;">
                <span style="color: var(--healthy);">◉</span>
                <span style="color: var(--muted);">MERKLE ROOT</span>
            </div>
            <div style="color: #0ff; letter-spacing: 1px; margin-bottom: 8px;">${provenanceData.merkle_root}</div>
            
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; font-size: 8px;">
                <div>
                    <div style="color: var(--dim);">MODEL</div>
                    <div style="color: var(--muted);">${provenanceData.model_hash || '---'}</div>
                </div>
                <div>
                    <div style="color: var(--dim);">INPUT</div>
                    <div style="color: var(--muted);">${provenanceData.input_hash || '---'}</div>
                </div>
            </div>
            
            <div style="margin-top: 8px; color: var(--dim); font-size: 8px;">
                ${provenanceData.chain_length} layers in chain
            </div>
        </div>
    `;
}

// === RENDER: ARCHITECTURE INFO ===
function renderArchitecture() {
    const container = document.getElementById('architecture-info');
    const arch = modelArchitecture;
    
    if (!arch.num_layers && !arch.hidden_size) {
        container.innerHTML = '<div style="color: var(--dim); font-size: 10px;">Loading model...</div>';
        return;
    }
    
    container.innerHTML = `
        <div style="font-family: monospace; font-size: 9px;">
            <div style="margin-bottom: 6px;">
                <span style="color: var(--info);">◈</span>
                <span style="color: var(--muted);">${arch.model_type.toUpperCase()}</span>
            </div>
            
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 10px;">
                <div>
                    <div style="color: var(--dim); font-size: 8px;">LAYERS</div>
                    <div style="color: #0ff; font-size: 14px; font-weight: bold;">${arch.num_layers || '?'}</div>
                </div>
                <div>
                    <div style="color: var(--dim); font-size: 8px;">HIDDEN</div>
                    <div style="color: #0ff; font-size: 14px; font-weight: bold;">${arch.hidden_size || '?'}</div>
                </div>
                <div>
                    <div style="color: var(--dim); font-size: 8px;">HEADS</div>
                    <div style="color: var(--muted);">${arch.num_heads || '?'}</div>
                </div>
                <div>
                    <div style="color: var(--dim); font-size: 8px;">PARAMS</div>
                    <div style="color: var(--muted);">${arch.hidden_size && arch.num_layers ? formatParams(arch.hidden_size, arch.num_layers, arch.num_heads) : '?'}</div>
                </div>
            </div>
        </div>
    `;
}

function formatParams(hidden, layers, heads) {
    // Rough estimate: each layer ≈ 12 * hidden^2 params (attention + mlp)
    const paramsM = (12 * hidden * hidden * layers) / 1e6;
    if (paramsM > 1000) return (paramsM / 1000).toFixed(1) + 'B';
    return paramsM.toFixed(0) + 'M';
}

// === RENDER: HEALTH ISSUES ===
function renderHealthIssues() {
    const container = document.getElementById('health-issues');
    const health = metrics.health_status;
    
    if (!health || !health.issues || health.issues.length === 0) {
        container.innerHTML = '<div style="color: var(--healthy); font-size: 10px;">✓ All metrics healthy</div>';
        return;
    }
    
    container.innerHTML = health.issues.map(issue => `
        <div style="padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 10px;">
            <span style="color: var(--${issue.status === 'critical' ? 'critical' : 'warning'});">●</span>
            <strong>${issue.metric}</strong> 
            <span style="color: var(--dim);">(${issue.category})</span>
            <div style="color: var(--muted); font-size: 9px;">
                ${issue.value?.toFixed(4)} • ${issue.trend}
            </div>
        </div>
    `).join('');
}

// === IMMERSIVE CORRELATION SYSTEM ===
// Connects 3D visualization flow to panel highlights in real-time

let activeHighlights = new Set();  // Currently highlighted metric names
let highlightTimeout = null;

function highlightMetricsForEvent(evt) {
    // Extract all numeric data keys from the event
    const data = evt.data || {};
    const metricNames = Object.keys(data).filter(k => typeof data[k] === 'number');
    
    // Also get the category for this event type
    const categories = new Set();
    metricNames.forEach(name => {
        const m = metrics.metrics?.[name];
        if (m && m.category) {
            categories.add(m.category);
        }
    });
    
    // Highlight matching metrics in the panels
    highlightMetrics(metricNames, categories);
}

function highlightMetrics(metricNames, categories = new Set()) {
    // Clear previous highlights
    clearHighlights();
    
    // Highlight individual metrics
    metricNames.forEach(name => {
        const el = document.querySelector(`.metric-item[data-metric="${name}"]`);
        if (el) {
            el.classList.add('highlight', 'trace-active');
            activeHighlights.add(name);
        }
    });
    
    // Highlight categories
    categories.forEach(cat => {
        const catEl = document.querySelector(`.category-group[data-category="${cat}"]`);
        if (catEl) {
            catEl.classList.add('highlight');
            const header = catEl.querySelector('.category-header');
            if (header) header.classList.add('pulse');
        }
    });
    
    // Highlight correlations that involve these metrics
    highlightCorrelationsFor(metricNames);
}

function highlightCorrelationsFor(metricNames) {
    const nameSet = new Set(metricNames);
    
    document.querySelectorAll('.correlation-item').forEach(el => {
        const metricsAttr = el.dataset.metrics;
        if (metricsAttr) {
            const [a, b] = metricsAttr.split(',');
            if (nameSet.has(a) || nameSet.has(b)) {
                el.classList.add('highlight');
            }
        }
    });
}

function clearHighlights() {
    document.querySelectorAll('.metric-item.highlight, .metric-item.trace-active').forEach(el => {
        el.classList.remove('highlight', 'trace-active');
    });
    document.querySelectorAll('.category-group.highlight').forEach(el => {
        el.classList.remove('highlight');
        const header = el.querySelector('.category-header');
        if (header) header.classList.remove('pulse');
    });
    document.querySelectorAll('.correlation-item.highlight').forEach(el => {
        el.classList.remove('highlight');
    });
    activeHighlights.clear();
}

// Called from trace animation to highlight metrics along the trace path
function highlightTraceMetrics(trace, positions) {
    if (!trace || !trace.nodes || trace.nodes.length === 0) return;
    
    // Get the current head node of the trace
    const headIdx = trace.nodes[trace.nodes.length - 1];
    if (headIdx >= positions.length) return;
    
    const pos = positions[headIdx];
    if (!pos || !pos.evt) return;
    
    // Highlight metrics for this event
    highlightMetricsForEvent(pos.evt);
}

// Auto-clear highlights after trace completes
function scheduleHighlightClear(delay = 500) {
    if (highlightTimeout) clearTimeout(highlightTimeout);
    highlightTimeout = setTimeout(clearHighlights, delay);
}

// === RENDER: EVENT FEED ===
function addEventToFeed(evt) {
    const feed = document.getElementById('event-feed');
    
    const el = document.createElement('div');
    el.className = 'event-item';
    el.dataset.id = evt.event_id;
    el.innerHTML = `
        <span class="event-type">${evt.event_type}</span>
        <span class="event-time">${new Date(evt.timestamp * 1000).toLocaleTimeString()}</span>
        <div class="event-component">${evt.component}</div>
    `;
    el.onclick = () => selectEvent(evt.event_id);
    
    feed.insertBefore(el, feed.firstChild);
    // No cap - unlimited events
}

function selectEvent(id) {
    try {
        selectedEventId = id;
        registerInteraction();  // Clicking exits idle mode
        
        document.querySelectorAll('.event-item').forEach(el => {
            el.classList.toggle('selected', el.dataset.id === id);
        });
        
        const evt = events.get(id);
        if (!evt) return;
        
        // === MOBILE: Defer heavy work to prevent UI freeze ===
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        if (isMobile) {
            // On mobile, do minimal update immediately, heavy work deferred
            const detailEl = document.getElementById('event-detail');
            if (detailEl) {
                detailEl.innerHTML = `
                    <div class="detail-header">
                        <div class="detail-value" style="font-weight: 700; font-size: 14px; color: #0ff;">${evt.event_type || 'Event'}</div>
                        <div style="color: var(--muted); font-size: 10px;">${evt.component || ''} • ${(id || '').slice(0, 12)}...</div>
                    </div>
                    <div style="color: #666; font-size: 10px; padding: 8px;">Loading details...</div>
                `;
            }
            // Defer heavy causation analysis
            setTimeout(() => selectEventFull(id, evt), 50);
            return;
        }
        
        selectEventFull(id, evt);
    } catch (e) {
        console.error('CASCADE selectEvent error:', e);
    }
}

function selectEventFull(id, evt) {
    try {
        // === IMMERSIVE: Highlight metrics for selected event ===
        highlightMetricsForEvent(evt);
    
    // === DRILL-DOWN: FIND CAUSATION SEQUENCE ===
    const allEvents = Array.from(events.values()).sort((a, b) => a.timestamp - b.timestamp);
    const idx = allEvents.findIndex(e => e.event_id === id);
    
    // Past events (causes)
    const pastEvents = [];
    for (let i = 1; i <= 5 && idx - i >= 0; i++) {
        pastEvents.push(allEvents[idx - i]);
    }
    
    // Future events (effects)
    const futureEvents = [];
    for (let i = 1; i <= 5 && idx + i < allEvents.length; i++) {
        futureEvents.push(allEvents[idx + i]);
    }
    
    // Build causation chain HTML
    const causationHTML = `
        <div class="causation-chain">
            ${pastEvents.length > 0 ? `
                <div class="causation-past">
                    <div style="color: #666; font-size: 9px; margin-bottom: 4px;">⟵ CAUSES (past)</div>
                    ${pastEvents.map((e, i) => `
                        <div class="causation-node past" onclick="selectEvent('${e.event_id}')" style="opacity: ${1 - i * 0.15}">
                            <span class="causation-type">${e.event_type}</span>
                            <span class="causation-component">${e.component}</span>
                        </div>
                    `).join('')}
                </div>
            ` : ''}
            
            <div class="causation-center">
                <div style="color: #0ff; font-size: 9px;">◉ SELECTED</div>
                <div class="causation-node selected">
                    <span class="causation-type">${evt.event_type}</span>
                    <span class="causation-component">${evt.component}</span>
                </div>
            </div>
            
            ${futureEvents.length > 0 ? `
                <div class="causation-future">
                    <div style="color: #666; font-size: 9px; margin-bottom: 4px;">EFFECTS (future) ⟶</div>
                    ${futureEvents.map((e, i) => `
                        <div class="causation-node future" onclick="selectEvent('${e.event_id}')" style="opacity: ${1 - i * 0.15}">
                            <span class="causation-type">${e.event_type}</span>
                            <span class="causation-component">${e.component}</span>
                        </div>
                    `).join('')}
                </div>
            ` : ''}
        </div>
    `;
    
    // Build metrics analysis
    const data = evt.data || {};
    const metricsHTML = Object.keys(data).length > 0 ? `
        <div class="detail-metrics">
            ${Object.entries(data).map(([k, v]) => {
                const numVal = typeof v === 'number' ? v : null;
                const barWidth = numVal !== null ? Math.min(100, numVal * (k === 'loss' ? 20 : k === 'acc' ? 1 : k === 'prob' ? 100 : 10)) : 0;
                return `
                    <div class="metric-row">
                        <span class="metric-key">${k}</span>
                        <span class="metric-val">${typeof v === 'number' ? v.toFixed(4) : v}</span>
                        ${numVal !== null ? `<div class="metric-bar" style="width: ${barWidth}%"></div>` : ''}
                    </div>
                `;
            }).join('')}
        </div>
    ` : '<div style="color: var(--dim); font-size: 10px;">No numeric data</div>';
    
    document.getElementById('event-detail').innerHTML = `
        <div class="detail-header">
            <div class="detail-value" style="font-weight: 700; font-size: 14px; color: #0ff;">${evt.event_type}</div>
            <div style="color: var(--muted); font-size: 10px;">${evt.component} • ${evt.event_id.slice(0, 12)}...</div>
            <div style="color: var(--dim); font-size: 9px; margin-top: 4px;">${new Date(evt.timestamp * 1000).toLocaleString()}</div>
        </div>
        
        <div class="detail-section" style="margin-top: 12px;">
            <div class="detail-label">CAUSATION SEQUENCE</div>
            ${causationHTML}
        </div>
        
        <div class="detail-section" style="margin-top: 12px;">
            <div class="detail-label">METRICS</div>
            ${metricsHTML}
        </div>
        
        <div class="detail-section" style="margin-top: 12px;">
            <div class="detail-label">RAW DATA</div>
            <div class="detail-code">${JSON.stringify(evt.data, null, 2)}</div>
        </div>
    `;
    
    scheduleRender();  // Use throttled render
    } catch (e) {
        console.error('CASCADE selectEventFull error:', e);
    }
}

// === RENDER: FLOW VISUALIZATION - 3D with Three.js ===
// Multi-dimensional space - events exist in N dimensions, rendered in 3D
// Topology emerges from data, not imposed

let scene, camera, renderer, controls;
let nodeObjects = new Map();  // event_id -> THREE.Mesh
let edgeObjects = [];
let labelSprites = [];  // Text labels that face camera
let traceLines = [];    // Causation trace lines
let initialized3D = false;

// MODEL ARCHITECTURE - captured from config event, drives topology scaling
let modelArchitecture = {
    hidden_size: 0,
    num_layers: 0,
    num_heads: 0,
    model_type: 'unknown',
    architectures: []
};

// Raycasting for click detection (initialized in init3D after THREE loads)
let raycaster = null;
let mouse = null;  // Will be THREE.Vector2
let hoveredObject = null;
let sphericalRadius = 12;  // Camera distance - closer for better visibility
let sphericalTheta = 0;
let sphericalPhi = Math.PI / 2;
// Camera target - tracks the centroid of the data (not always origin!)
let cameraTarget = { x: 0, y: 0, z: 0 };

// Golden ratio for trace hue sequencing
const goldenRatio = 0.618033988749895;
let hueOffset = Math.random();

function getNextHue() {
    hueOffset = (hueOffset + goldenRatio) % 1;
    return hueOffset;
}

// === IDLE MODE: CAUSATION TRACES ===
// Define these BEFORE they're used in renderFlow/animate3D
let activeTraces = [];
let idleMode = true;  // Start in idle mode immediately
let lastInteraction = 0;  // Never interacted yet
const IDLE_TIMEOUT = 2000;  // 2 seconds to enter idle
const TRACE_INTERVAL = 800;  // Spawn trace every 0.8s (was 1.5s) for more fluid animation
const MAX_TRACES = 50;  // Allow many simultaneous traces for complex flows

// === SMOOTH TRACE CAMERA ===
// Follows trace CLUSTERS, not individual nodes
// Cinematic, smooth, no jarring jumps

let traceRider = {
    // Current position (lerps toward target)
    position: { x: 0, y: 0, z: 0 },
    lookAt: { x: 0, y: 0, z: 0 },
    
    // Target (smoothed cluster center)
    targetPos: { x: 0, y: 0, z: 0 },
    targetLookAt: { x: 0, y: 0, z: 0 },
    
    // Movement speeds - VERY SLOW for smoothness
    lerpFactor: 0.08,  // Increased from 0.05 for smoother tracking
    lookAtLerp: 0.12,  // Faster look-at tracking
    
    // Distance from cluster - OBSERVATORY VIEW (no node penetration)
    distance: 40,          // Closer for better visibility
    height: 25,            // Lower height for closer view
    
    // Overview config
    overviewAngle: 0,      // Slowly rotates in overview
    
    // State
    mode: 'overview',
    lastTraceTime: 0,
    
    // Auto camera toggle
    autoEnabled: true
};

// Toggle handler for auto-camera
document.getElementById('camera-toggle')?.addEventListener('click', function() {
    traceRider.autoEnabled = !traceRider.autoEnabled;
    const btn = this;
    if (traceRider.autoEnabled) {
        btn.textContent = '⟐ AUTO';
        btn.classList.remove('manual-mode');
        btn.classList.add('auto-enabled');
    } else {
        btn.textContent = '⟡ MANUAL';
        btn.classList.remove('auto-enabled');
        btn.classList.add('manual-mode');
    }
});

// Store positions globally for trace rider access
let globalPositions = [];

function updateTraceRider(positions) {
    if (!positions || positions.length < 2) return;
    globalPositions = positions;
    
    // Calculate full network centroid
    const networkCentroid = positions.reduce((acc, p) => ({
        x: acc.x + p.x / positions.length,
        y: acc.y + p.y / positions.length,
        z: acc.z + p.z / positions.length
    }), { x: 0, y: 0, z: 0 });
    
    // Calculate bounding box to find principal spread
    let minX = Infinity, maxX = -Infinity;
    let minY = Infinity, maxY = -Infinity;
    let minZ = Infinity, maxZ = -Infinity;
    
    positions.forEach(p => {
        minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
        minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
        minZ = Math.min(minZ, p.z); maxZ = Math.max(maxZ, p.z);
    });
    
    const spreadX = maxX - minX;
    const spreadY = maxY - minY;
    const spreadZ = maxZ - minZ;
    const boundingRadius = Math.max(spreadX, spreadY, spreadZ, 10) / 2;
    
    // Determine which axis has SMALLEST spread - camera goes there
    // We want to look AT the widest face, so position along narrowest axis
    let cameraAxis = 'x';  // Default: position along X, look at Y-Z plane
    if (spreadX >= spreadY && spreadX >= spreadZ) {
        // X is widest - position along Y or Z
        cameraAxis = spreadY < spreadZ ? 'y' : 'z';
    } else if (spreadY >= spreadX && spreadY >= spreadZ) {
        // Y is widest - position along X or Z  
        cameraAxis = spreadX < spreadZ ? 'x' : 'z';
    } else {
        // Z is widest - position along X or Y
        cameraAxis = spreadX < spreadY ? 'x' : 'y';
    }
    
    // Check if we have active traces
    const hasActiveTrace = activeTraces.length > 0 && 
        activeTraces.some(t => t.nodes && t.nodes.length >= 3 && t.age < t.maxAge * 0.7);
    
    if (hasActiveTrace) {
        // === RIDING MODE: CIRCUIT BOARD INSPECTION ===
        traceRider.mode = 'riding';
        traceRider.lastTraceTime = Date.now();
        
        const trace = activeTraces[activeTraces.length - 1];
        if (!trace || !trace.nodes || trace.nodes.length < 3) return;
        
        const headIdx = trace.nodes[trace.nodes.length - 1];
        if (headIdx >= positions.length) return;
        const headPos = positions[headIdx];
        
        // Position camera along narrowest axis, looking at widest spread
        if (cameraAxis === 'x') {
            // Position along X axis, look at Y-Z plane
            traceRider.targetPos = {
                x: networkCentroid.x + traceRider.distance,
                y: headPos.y + 5,
                z: headPos.z
            };
        } else if (cameraAxis === 'y') {
            // Position along Y axis (above), look at X-Z plane
            traceRider.targetPos = {
                x: headPos.x,
                y: networkCentroid.y + traceRider.distance,
                z: headPos.z
            };
        } else {
            // Position along Z axis, look at X-Y plane
            traceRider.targetPos = {
                x: headPos.x,
                y: headPos.y + 5,
                z: networkCentroid.z + traceRider.distance
            };
        }
        
        traceRider.targetLookAt = {
            x: headPos.x,
            y: headPos.y,
            z: headPos.z
        };
        
    } else {
        // === OVERVIEW MODE: Face the widest spread ===
        traceRider.mode = 'overview';
        
        const overviewDist = boundingRadius * 2.5 + 40;
        
        // Position camera along narrowest axis to see maximum nodes
        if (cameraAxis === 'x') {
            traceRider.targetPos = {
                x: networkCentroid.x + overviewDist,
                y: networkCentroid.y,
                z: networkCentroid.z
            };
        } else if (cameraAxis === 'y') {
            traceRider.targetPos = {
                x: networkCentroid.x,
                y: networkCentroid.y + overviewDist,
                z: networkCentroid.z
            };
        } else {
            traceRider.targetPos = {
                x: networkCentroid.x,
                y: networkCentroid.y,
                z: networkCentroid.z + overviewDist
            };
        }
        
        traceRider.targetLookAt = networkCentroid;
    }
}

function applyTraceRider() {
    // Skip if auto-camera is disabled or idle mode is off
    if (!idleMode || !camera || !traceRider.autoEnabled) return;
    
    // Unified smooth speed - both modes are gentle
    const speed = traceRider.mode === 'riding' ? traceRider.lerpFactor : traceRider.lerpFactor * 0.5;
    
    // Smooth lerp position toward target
    traceRider.position.x += (traceRider.targetPos.x - traceRider.position.x) * speed;
    traceRider.position.y += (traceRider.targetPos.y - traceRider.position.y) * speed;
    traceRider.position.z += (traceRider.targetPos.z - traceRider.position.z) * speed;
    
    // Look target lerp (same speed for stability)
    traceRider.lookAt.x += (traceRider.targetLookAt.x - traceRider.lookAt.x) * speed;
    traceRider.lookAt.y += (traceRider.targetLookAt.y - traceRider.lookAt.y) * speed;
    traceRider.lookAt.z += (traceRider.targetLookAt.z - traceRider.lookAt.z) * speed;
    
    // Apply to camera directly
    camera.position.set(
        traceRider.position.x,
        traceRider.position.y,
        traceRider.position.z
    );
    camera.lookAt(
        traceRider.lookAt.x,
        traceRider.lookAt.y,
        traceRider.lookAt.z
    );
    
    // Update spherical coords for manual control continuity
    const dx = traceRider.position.x - traceRider.lookAt.x;
    const dy = traceRider.position.y - traceRider.lookAt.y;
    const dz = traceRider.position.z - traceRider.lookAt.z;
    sphericalRadius = Math.sqrt(dx*dx + dy*dy + dz*dz);
    cameraTarget = { ...traceRider.lookAt };
}

// === HSV TO RGB === (define early so renderFlow can use it)
function hsvToRgb(h, s, v) {
    let r, g, b;
    const i = Math.floor(h * 6);
    const f = h * 6 - i;
    const p = v * (1 - s);
    const q = v * (1 - f * s);
    const t = v * (1 - (1 - f) * s);
    
    switch (i % 6) {
        case 0: r = v; g = t; b = p; break;
        case 1: r = q; g = v; b = p; break;
        case 2: r = p; g = v; b = t; break;
        case 3: r = p; g = q; b = v; break;
        case 4: r = t; g = p; b = v; break;
        case 5: r = v; g = p; b = q; break;
    }
    
    return {
        r: Math.round(r * 255),
        g: Math.round(g * 255),
        b: Math.round(b * 255)
    };
}

// === EMERGENT COMPONENT CATEGORIZATION ===
// Discover component types from their names - no hardcoded model assumptions
function categorizeComponent(name) {
    const n = name.toLowerCase();
    if (n.includes('attention') || n.includes('attn')) return 'attention';
    if (n.includes('mlp') || n.includes('feedforward') || n.includes('ff')) return 'mlp';
    if (n.includes('norm') || n.includes('ln') || n.includes('layernorm')) return 'norm';
    if (n.includes('embed')) return 'embedding';
    if (n.includes('head') || n.includes('lm_head') || n.includes('output')) return 'output';
    if (n.includes('linear') || n.includes('dense')) return 'linear';
    if (n.includes('conv')) return 'conv';
    if (n.includes('pool')) return 'pool';
    if (n.includes('encoder')) return 'encoder';
    if (n.includes('decoder')) return 'decoder';
    if (n.includes('generation') || n.includes('token')) return 'generation';
    if (n.includes('tokenizer')) return 'tokenizer';
    if (n.includes('system') || n.includes('model') || n.includes('config')) return 'system';
    return 'other';
}

// Spatial offsets by component type - creates visual clustering
function getTypeOffset(type) {
    const offsets = {
        'embedding': { x: -8, z: 0 },
        'attention': { x: 0, z: -5 },
        'mlp': { x: 0, z: 5 },
        'norm': { x: 4, z: 0 },
        'linear': { x: 2, z: 2 },
        'output': { x: 8, z: 0 },
        'generation': { x: 10, z: 0 },
        'encoder': { x: -3, z: -3 },
        'decoder': { x: 3, z: 3 },
        'tokenizer': { x: -10, z: 0 },
        'system': { x: -12, z: 0 },
        'conv': { x: 0, z: -8 },
        'pool': { x: 0, z: 8 },
        'other': { x: 0, z: 0 }
    };
    return offsets[type] || { x: 0, z: 0 };
}

// Get color for component type
function getComponentColor(type, data) {
    const typeColors = {
        'embedding': { h: 0.6, s: 0.7, v: 0.9 },   // Blue
        'attention': { h: 0.85, s: 0.8, v: 0.95 }, // Magenta/Pink
        'mlp': { h: 0.3, s: 0.7, v: 0.9 },         // Green
        'norm': { h: 0.15, s: 0.6, v: 0.85 },      // Orange/Yellow
        'linear': { h: 0.5, s: 0.6, v: 0.8 },      // Cyan
        'output': { h: 0.0, s: 0.8, v: 0.95 },     // Red
        'generation': { h: 0.45, s: 0.9, v: 1.0 }, // Teal/Cyan bright
        'tokenizer': { h: 0.7, s: 0.5, v: 0.7 },   // Purple muted
        'system': { h: 0.0, s: 0.0, v: 0.5 },      // Gray
        'encoder': { h: 0.55, s: 0.7, v: 0.85 },   // Light blue
        'decoder': { h: 0.08, s: 0.7, v: 0.9 },    // Orange
        'conv': { h: 0.75, s: 0.6, v: 0.8 },       // Purple
        'pool': { h: 0.4, s: 0.5, v: 0.7 },        // Teal muted
        'other': { h: 0.0, s: 0.0, v: 0.6 }        // Gray
    };
    
    const base = typeColors[type] || typeColors['other'];
    
    // Modulate by data values if present
    let h = base.h, s = base.s, v = base.v;
    if (data) {
        if ('mean' in data) {
            // Shift hue slightly based on activation mean
            h = (h + Math.tanh(data.mean) * 0.05) % 1;
        }
        if ('std' in data) {
            // Higher variance = more saturated
            s = Math.min(1, s + data.std * 0.1);
        }
        if ('sparsity' in data) {
            // Higher sparsity = dimmer
            v = v * (1 - data.sparsity * 0.3);
        }
    }
    
    return { h, s, v };
}

// Camera position update - must be global for renderFlow to call it
function updateCameraPosition() {
    if (!camera) return;
    // Orbit around cameraTarget (the centroid), not origin!
    camera.position.x = cameraTarget.x + sphericalRadius * Math.sin(sphericalPhi) * Math.cos(sphericalTheta);
    camera.position.y = cameraTarget.y + sphericalRadius * Math.cos(sphericalPhi);
    camera.position.z = cameraTarget.z + sphericalRadius * Math.sin(sphericalPhi) * Math.sin(sphericalTheta);
    camera.lookAt(cameraTarget.x, cameraTarget.y, cameraTarget.z);
}

function init3D() {
    try {
        // Check if THREE.js is loaded
        if (typeof THREE === 'undefined') {
            console.error('CASCADE: THREE.js not loaded');
            const container = document.getElementById('flow-canvas');
            if (container) {
                container.innerHTML = '<div style="color:#ff6666;padding:20px;text-align:center;">Loading 3D library...</div>';
            }
            // Retry after 500ms
            setTimeout(init3D, 500);
            return;
        }
        
        const container = document.getElementById('flow-canvas');
        if (!container) {
            console.error('CASCADE: flow-canvas container not found');
            return;
        }
        
        // Clear any existing content
        container.innerHTML = '';
        
        const rect = container.getBoundingClientRect();
    
    // Scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);
    
    // Camera
    camera = new THREE.PerspectiveCamera(60, rect.width / rect.height, 0.1, 1000);
    camera.position.set(0, 0, 30);
    
    // Renderer - with mobile-friendly settings
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    
    try {
        renderer = new THREE.WebGLRenderer({ 
            antialias: !isMobile,  // Disable antialiasing on mobile for performance
            powerPreference: isMobile ? 'low-power' : 'high-performance',
            failIfMajorPerformanceCaveat: false,
            preserveDrawingBuffer: false
        });
    } catch (e) {
        console.error('CASCADE: WebGL renderer creation failed:', e);
        container.innerHTML = '<div style="color:#ff6666;padding:20px;text-align:center;">WebGL not supported. Try a different browser.</div>';
        return;
    }
    
    renderer.setSize(rect.width, rect.height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 2 : 3));  // Limit pixel ratio on mobile
    container.appendChild(renderer.domElement);
    
    // Simple orbit controls (manual implementation)
    let isDragging = false;
    let hasDragged = false;  // Track if actual drag happened (for click detection)
    let previousMousePosition = { x: 0, y: 0 };
    
    container.addEventListener('mousedown', (e) => {
        isDragging = true;
        hasDragged = false;
        previousMousePosition = { x: e.clientX, y: e.clientY };
        registerInteraction();
    });
    
    container.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        
        const deltaX = e.clientX - previousMousePosition.x;
        const deltaY = e.clientY - previousMousePosition.y;
        
        // Check if this is actually a drag (moved more than 3px)
        if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
            hasDragged = true;
        }
        
        sphericalTheta -= deltaX * 0.005;
        sphericalPhi -= deltaY * 0.005;
        sphericalPhi = Math.max(0.1, Math.min(Math.PI - 0.1, sphericalPhi));
        
        updateCameraPosition();
        previousMousePosition = { x: e.clientX, y: e.clientY };
    });
    
    container.addEventListener('mouseup', () => isDragging = false);
    container.addEventListener('mouseleave', () => isDragging = false);
    
    // === TOUCH SUPPORT FOR MOBILE ===
    let lastTouchDistance = 0;
    
    container.addEventListener('touchstart', (e) => {
        if (e.touches.length === 1) {
            isDragging = true;
            hasDragged = false;
            previousMousePosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
            registerInteraction();
        } else if (e.touches.length === 2) {
            // Pinch zoom - store initial distance
            const dx = e.touches[0].clientX - e.touches[1].clientX;
            const dy = e.touches[0].clientY - e.touches[1].clientY;
            lastTouchDistance = Math.sqrt(dx * dx + dy * dy);
        }
        e.preventDefault();
    }, { passive: false });
    
    container.addEventListener('touchmove', (e) => {
        if (e.touches.length === 1 && isDragging) {
            const touch = e.touches[0];
            const deltaX = touch.clientX - previousMousePosition.x;
            const deltaY = touch.clientY - previousMousePosition.y;
            
            if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
                hasDragged = true;
            }
            
            sphericalTheta -= deltaX * 0.005;
            sphericalPhi -= deltaY * 0.005;
            sphericalPhi = Math.max(0.1, Math.min(Math.PI - 0.1, sphericalPhi));
            
            updateCameraPosition();
            previousMousePosition = { x: touch.clientX, y: touch.clientY };
        } else if (e.touches.length === 2) {
            // Pinch zoom
            const dx = e.touches[0].clientX - e.touches[1].clientX;
            const dy = e.touches[0].clientY - e.touches[1].clientY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            if (lastTouchDistance > 0) {
                const delta = lastTouchDistance - distance;
                sphericalRadius += delta * 0.2;
                sphericalRadius = Math.max(10, Math.min(200, sphericalRadius));
                updateCameraPosition();
            }
            lastTouchDistance = distance;
        }
        e.preventDefault();
    }, { passive: false });
    
    container.addEventListener('touchend', (e) => {
        isDragging = false;
        lastTouchDistance = 0;
        
        // Tap to select (like click)
        if (!hasDragged && e.changedTouches.length === 1) {
            const touch = e.changedTouches[0];
            const rect = container.getBoundingClientRect();
            mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
            mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
            
            raycaster.setFromCamera(mouse, camera);
            const meshes = Array.from(nodeObjects.values());
            const intersects = raycaster.intersectObjects(meshes);
            
            if (intersects.length > 0) {
                const hit = intersects[0].object;
                if (hit.userData && hit.userData.eventId) {
                    selectEvent(hit.userData.eventId);
                }
            }
        }
    });
    
    container.addEventListener('wheel', (e) => {
        e.preventDefault();
        sphericalRadius += e.deltaY * 0.05;
        sphericalRadius = Math.max(10, Math.min(200, sphericalRadius));
        updateCameraPosition();
        registerInteraction();
    });
    
    // === RAYCASTING: Click on 3D nodes ===
    raycaster = new THREE.Raycaster();
    mouse = new THREE.Vector2();
    
    // Create tooltip element
    const tooltip = document.createElement('div');
    tooltip.className = 'node-tooltip';
    tooltip.style.display = 'none';
    document.body.appendChild(tooltip);
    
    // Click handler - select event
    container.addEventListener('click', (e) => {
        if (hasDragged) return;  // Don't select if user was dragging
        
        const rect = container.getBoundingClientRect();
        mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
        mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
        
        raycaster.setFromCamera(mouse, camera);
        const meshes = Array.from(nodeObjects.values());
        const intersects = raycaster.intersectObjects(meshes);
        
        if (intersects.length > 0) {
            const hit = intersects[0].object;
            if (hit.userData && hit.userData.eventId) {
                selectEvent(hit.userData.eventId);
            }
        }
    });
    
    // Hover handler - show tooltip
    container.addEventListener('mousemove', (e) => {
        if (isDragging) {
            tooltip.style.display = 'none';
            return;
        }
        
        const rect = container.getBoundingClientRect();
        mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
        mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
        
        raycaster.setFromCamera(mouse, camera);
        const meshes = Array.from(nodeObjects.values());
        const intersects = raycaster.intersectObjects(meshes);
        
        if (intersects.length > 0) {
            const hit = intersects[0].object;
            if (hit.userData && hit.userData.eventId) {
                const evt = events.get(hit.userData.eventId);
                if (evt) {
                    // Highlight hovered node
                    if (hoveredObject && hoveredObject !== hit) {
                        // Reset previous hover
                    }
                    hoveredObject = hit;
                    container.style.cursor = 'pointer';
                    
                    // Show tooltip
                    tooltip.innerHTML = `
                        <div class="tt-type">${evt.event_type}</div>
                        <div class="tt-component">${evt.component}</div>
                        ${Object.keys(evt.data || {}).slice(0, 3).map(k => 
                            `<div style="color: #888; font-size: 9px;">${k}: ${JSON.stringify(evt.data[k]).slice(0, 20)}</div>`
                        ).join('')}
                        <div style="color: #444; font-size: 8px; margin-top: 4px;">click to drill down</div>
                    `;
                    tooltip.style.display = 'block';
                    tooltip.style.left = (e.clientX + 10) + 'px';
                    tooltip.style.top = (e.clientY + 10) + 'px';
                }
            }
        } else {
            hoveredObject = null;
            container.style.cursor = 'grab';
            tooltip.style.display = 'none';
        }
    });
    
    container.addEventListener('mouseleave', () => {
        tooltip.style.display = 'none';
    });
    
    // Ambient light
    scene.add(new THREE.AmbientLight(0x404040));
    
    // Point light at camera
    const pointLight = new THREE.PointLight(0xffffff, 0.8);
    camera.add(pointLight);
    scene.add(camera);
    
    // Handle resize
    window.addEventListener('resize', () => {
        const rect = container.getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) {
            camera.aspect = rect.width / rect.height;
            camera.updateProjectionMatrix();
            renderer.setSize(rect.width, rect.height);
        }
    });
    
    initialized3D = true;
    animate3D();
    } catch (e) {
        console.error('CASCADE init3D error:', e);
        // Show fallback message
        const container = document.getElementById('flow-canvas');
        if (container) {
            container.innerHTML = '<div style="color:#ff6666;padding:20px;text-align:center;">3D initialization failed. Try refreshing.</div>';
        }
    }
}

function animate3D() {
    requestAnimationFrame(animate3D);
    
    if (renderer && scene && camera) {
        // TRACE RIDER CAMERA: GoPro-on-dog when tracing, overview between
        if (idleMode) {
            applyTraceRider();
            
            // Update mode indicator
            const indicator = document.getElementById('idle-indicator');
            if (indicator) {
                if (traceRider.mode === 'riding') {
                    indicator.textContent = '◉ TRACING';
                    indicator.style.color = '#ff8800';
                    indicator.style.borderColor = '#ff8800';
                } else {
                    indicator.textContent = '◎ OBSERVING';
                    indicator.style.color = '#00ff88';
                    indicator.style.borderColor = '#00ff88';
                }
            }
        }
        
        renderer.render(scene, camera);
        
        // Update sprite labels to face camera
        labelSprites.forEach(sprite => {
            sprite.lookAt(camera.position);
        });
    }
}

function renderFlow() {
    try {
        if (!initialized3D) {
            init3D();
            return;
        }
        
        // === DYNAMIC MEMORY MANAGEMENT ===
        // No hard caps - use Level of Detail (LOD) for performance
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        // Configurable via window.CASCADE_MAX_EVENTS (default: unlimited on desktop, 10k mobile)
        const maxEvents = window.CASCADE_MAX_EVENTS || (isMobile ? 10000 : Infinity);
    
    let recentEvents = Array.from(events.values());
    
    // Only trim if we have a limit AND exceed it
    if (maxEvents !== Infinity && recentEvents.length > maxEvents) {
        // Clean up old 3D objects to free GPU memory
        const oldEventIds = recentEvents.slice(0, recentEvents.length - maxEvents).map(e => e.event_id);
        oldEventIds.forEach(id => {
            const mesh = nodeObjects.get(id);
            if (mesh) {
                mesh.geometry.dispose();
                mesh.material.dispose();
                scene.remove(mesh);
                nodeObjects.delete(id);
            }
        });
        recentEvents = recentEvents.slice(-maxEvents);
        console.log('CASCADE: Dynamic trim to', maxEvents, 'events (configurable via window.CASCADE_MAX_EVENTS)');
    }
    
    if (recentEvents.length === 0) return;
    
    // === PURELY EMERGENT TOPOLOGY ===
    // NO hardcoded categories - positions derived ONLY from actual data
    // The network structure reveals itself through the data it produces
    
    // 1. DISCOVER unique components from the event stream
    const componentMap = new Map();  // component name -> { events, layer_idx, metrics }
    const componentOrder = [];  // Discovery order = execution order
    
    recentEvents.forEach((evt, i) => {
        const component = evt.component || 'unknown';
        const data = evt.data || {};
        
        if (!componentMap.has(component)) {
            componentMap.set(component, {
                events: [],
                layer_idx: data.layer_idx ?? componentOrder.length,
                // Accumulate metrics for this component
                meanSum: 0,
                stdSum: 0,
                metricCount: 0,
                firstSeen: i
            });
            componentOrder.push(component);
        }
        
        const info = componentMap.get(component);
        info.events.push({ evt, globalIdx: i });
        
        // Accumulate actual metrics from this component
        if (typeof data.mean === 'number') {
            info.meanSum += data.mean;
            info.metricCount++;
        }
        if (typeof data.std === 'number') {
            info.stdSum += data.std;
        }
        // Capture structural info
        if (data.layer_type) info.layer_type = data.layer_type;
        if (typeof data.layer_num === 'number' && data.layer_num >= 0) {
            info.layer_num = data.layer_num;
        }
        if (data.total_layers) info.total_layers = data.total_layers;
        if (typeof data.attn_entropy === 'number') {
            info.attn_entropy = (info.attn_entropy || 0) + data.attn_entropy;
            info.attn_count = (info.attn_count || 0) + 1;
        }
        
        // Capture tensor shape for sizing
        if (data.shape && Array.isArray(data.shape)) {
            info.shape = data.shape;
            // Track max hidden dim and sequence length seen
            if (data.shape.length >= 3) {
                info.maxHidden = Math.max(info.maxHidden || 0, data.shape[2]);
                info.maxSeqLen = Math.max(info.maxSeqLen || 0, data.shape[1]);
            } else if (data.shape.length >= 2) {
                info.maxHidden = Math.max(info.maxHidden || 0, data.shape[1]);
            }
        }
        
        // Track provenance hashes for this component
        if (data.hash) {
            if (!info.hashes) info.hashes = [];
            info.hashes.push(data.hash);
        }
    });
    
    // 2. EMERGENT TOPOLOGY - Structure EMERGES from actual data
    // NO hardcoded offsets - positions derive from:
    //   - Actual layer numbers (not normalized)
    //   - Tensor shapes (hidden dim, seq length)
    //   - Module execution order (discovery order)
    //   - Provenance chain position (hash lineage)
    
    const numComponents = componentOrder.length;
    const maxLayerNum = Math.max(...Array.from(componentMap.values()).map(c => c.layer_num ?? -1), 0);
    const actualNumLayers = modelArchitecture.num_layers || maxLayerNum || 12;
    const actualHiddenSize = modelArchitecture.hidden_size || 768;
    
    // Y: Layer DEPTH - ACTUAL layer number, scales with model depth
    // A 96-layer model should be 8x taller than a 12-layer model
    const yScale = Math.max(actualNumLayers / 12, 1);  // Base scale on 12-layer model
    const componentY = new Map();
    componentOrder.forEach((comp, idx) => {
        const info = componentMap.get(comp);
        // Use actual layer number, not normalized
        const layerNum = (info.layer_num >= 0) ? info.layer_num : (info.layer_idx ?? idx);
        // Each layer gets a distinct Y position
        const y = layerNum * 1.5;  // 1.5 units per layer
        componentY.set(comp, y);
    });
    
    // Z: Module TYPE clustering - but spread varies by hidden_size
    // Larger hidden dims = more Z spread (the model's "width")
    const zScale = actualHiddenSize / 768;  // Base scale on 768 hidden
    const typeOrder = ['embed', 'attention', 'norm', 'mlp', 'conv', 'dropout', 'head', 'other'];
    const componentZ = new Map();
    const typeCount = new Map();  // Track how many of each type we've seen per layer
    
    componentOrder.forEach(comp => {
        const info = componentMap.get(comp);
        const layerNum = info.layer_num ?? 0;
        const layerType = info.layer_type || 'other';
        
        // Key for this layer+type combo
        const key = `${layerNum}_${layerType}`;
        const count = typeCount.get(key) || 0;
        typeCount.set(key, count + 1);
        
        // Z based on type order and count within type
        const typeIdx = typeOrder.indexOf(layerType);
        const baseZ = (typeIdx - 3) * 4 * zScale;  // Center around type index 3
        const variation = count * 2 * zScale;  // Spread within same type
        
        componentZ.set(comp, baseZ + variation);
    });
    
    // X: Activation dynamics + TENSOR SHAPE spread
    // Use actual sequence length and hidden dim from shape
    const componentX = new Map();
    componentOrder.forEach(comp => {
        const info = componentMap.get(comp);
        
        // Base X on tensor shape if available
        let x = 0;
        if (info.maxSeqLen > 0) {
            // Longer sequences spread wider
            x = (info.maxSeqLen / 512 - 1) * 5;  // Center around seq=512
        }
        
        // Add activation dynamics
        if (info.attn_count > 0) {
            const avgEntropy = info.attn_entropy / info.attn_count;
            x += (avgEntropy - 2) * 2;  // Attention entropy adds spread
        } else if (info.metricCount > 0) {
            const avgMean = info.meanSum / info.metricCount;
            x += Math.tanh(avgMean) * 3;  // Activation mean adds dynamics
        }
        
        componentX.set(comp, x);
    });
    
    // Color: Attention entropy OR activation variance
    const componentHue = new Map();
    componentOrder.forEach(comp => {
        const info = componentMap.get(comp);
        if (info.attn_count > 0) {
            // Attention: entropy → hue (low entropy = focused = warm, high = diffuse = cool)
            const avgEntropy = info.attn_entropy / info.attn_count;
            componentHue.set(comp, Math.min(avgEntropy / 5, 1) * 0.6);  // 0=red, 0.6=blue
        } else {
            const avgStd = info.metricCount > 0 ? info.stdSum / info.metricCount : 0;
            componentHue.set(comp, 0.6 - Math.min(avgStd, 1) * 0.6);
        }
    });
    
    // 3. BUILD positions - structure + dynamics
    const positions = recentEvents.map((evt, i) => {
        const data = evt.data || {};
        const component = evt.component || 'unknown';
        const compInfo = componentMap.get(component);
        
        // X: Base position + sequence spread within component
        const localIdx = compInfo.events.findIndex(e => e.globalIdx === i);
        const localCount = compInfo.events.length;
        const seqSpread = localCount > 1 ? (localIdx / (localCount - 1)) * 6 - 3 : 0;
        const x = (componentX.get(component) || 0) + seqSpread;
        
        // Y: Layer depth (structural)
        const y = componentY.get(component) || 0;
        
        // Z: Layer type cluster (structural) + per-event dynamics
        let z = componentZ.get(component) || 0;
        // Add per-event variation from attention entropy if present
        if (typeof data.attn_entropy === 'number') {
            z += (data.attn_entropy - 2) * 2;  // Spread by entropy
        }
        
        return { 
            x, 
            y, 
            z, 
            evt, 
            component,
            hue: componentHue.get(component) || 0.5,
            layerType: compInfo.layer_type || 'other'
        };
    });
    
    // === AUTO-FIT: Calculate bounding sphere and adjust camera ===
    if (positions.length > 0) {
        // === TRACE RIDER CAMERA ===
        // Feed positions to GoPro-on-dog camera system
        updateTraceRider(positions);
        
        // Find centroid
        const centroid = { x: 0, y: 0, z: 0 };
        positions.forEach(p => {
            centroid.x += p.x;
            centroid.y += p.y;
            centroid.z += p.z;
        });
        centroid.x /= positions.length;
        centroid.y /= positions.length;
        centroid.z /= positions.length;
        
        // Find max distance from centroid (bounding sphere radius)
        let maxDist = 0;
        positions.forEach(p => {
            const dist = Math.sqrt(
                Math.pow(p.x - centroid.x, 2) +
                Math.pow(p.y - centroid.y, 2) +
                Math.pow(p.z - centroid.z, 2)
            );
            if (dist > maxDist) maxDist = dist;
        });
        
        // Ensure minimum size but cap maximum
        maxDist = Math.max(maxDist, 5);
        maxDist = Math.min(maxDist, 40);  // Don't let it get too big
        
        // Calculate ideal camera distance - CLOSER view
        const fov = camera.fov * Math.PI / 180;
        const idealDist = (maxDist * 1.2) / Math.tan(fov / 2);  // Reduced padding from 1.8 to 1.2
        
        // Cap the ideal distance - don't zoom out too far
        const cappedDist = Math.min(idealDist, 80);
        
        // Trace rider handles all camera in idle mode
        // Manual mode: basic auto-fit when user is controlling
        if (!idleMode && typeof sphericalRadius !== 'undefined') {
            const shouldZoomOut = cappedDist > sphericalRadius * 1.3;
            const shouldZoomIn = cappedDist < sphericalRadius * 0.7;
            
            if (shouldZoomOut || shouldZoomIn) {
                sphericalRadius = sphericalRadius * 0.95 + cappedDist * 0.05;
            }
            cameraTarget.x = cameraTarget.x * 0.95 + centroid.x * 0.05;
            cameraTarget.y = cameraTarget.y * 0.95 + centroid.y * 0.05;
            cameraTarget.z = cameraTarget.z * 0.95 + centroid.z * 0.05;
            updateCameraPosition();
        }
    }
    
    // === UPDATE 3D SCENE ===
    // Remove old objects
    nodeObjects.forEach((mesh, id) => {
        if (!recentEvents.find(e => e.event_id === id)) {
            scene.remove(mesh);
            nodeObjects.delete(id);
        }
    });
    
    // Add/update nodes - STRUCTURAL topology + DYNAMIC coloring
    positions.forEach((pos, i) => {
        const evt = pos.evt;
        let mesh = nodeObjects.get(evt.event_id);
        
        const data = evt.data || {};
        const layerType = pos.layerType || 'other';
        let color = 0x666666;
        let size = 0.6;
        let emissive = 0x000000;
        
        // Size based on layer type - attention is bigger/more important
        const typeSizes = {
            'attention': 0.7,
            'mlp': 0.55,
            'norm': 0.35,
            'embed': 0.8,
            'head': 0.8,
            'dropout': 0.25,
            'conv': 0.6,
            'other': 0.4
        };
        size = typeSizes[layerType] || 0.4;
        
        // EMERGENT SIZE: Scale by tensor hidden dimension if available
        // Bigger hidden dims = bigger nodes, reveals model architecture
        if (data.shape && Array.isArray(data.shape)) {
            const hiddenDim = data.shape.length >= 3 ? data.shape[2] : (data.shape.length >= 2 ? data.shape[1] : 768);
            // Scale relative to typical hidden size (768) - moderate scaling
            const dimScale = Math.sqrt(hiddenDim / 768) * 0.75;  // Increased from 0.5 to 0.75
            size = size * dimScale;
        }
        
        // Check if this node is part of an active trace
        const traceInfo = activeTraces.find(t => t.nodes.includes(i));
        
        // Calculate position in causality chain if part of trace
        let causalityProgress = 0;
        if (traceInfo) {
            const tracePos = traceInfo.nodes.indexOf(i);
            causalityProgress = tracePos / (traceInfo.nodes.length - 1);
        }
        
        if (evt.event_id === selectedEventId) {
            color = 0x00ffff;
            emissive = 0x00aaff;
            size = size * (1.5 + causalityProgress * 0.5);  // Bigger for later in chain
        } else if (traceInfo) {
            // Node is being traced - show causality progression
            const hue = 0.6 - causalityProgress * 0.6;  // Blue to red based on position
            const rgb = hsvToRgb(hue, 1.0, 1.0);
            color = (rgb.r << 16) | (rgb.g << 8) | rgb.b;
            const glowRgb = hsvToRgb(hue, 0.8, 0.6);
            emissive = (glowRgb.r << 16) | (glowRgb.g << 8) | glowRgb.b;
            size = size * (1.0 + causalityProgress * 1.0);  // Progressive sizing
        } else if (evt.event_type.includes('error')) {
            color = 0xff3366;
            emissive = 0x661122;
            size = size * 1.2;
        } else {
            // === STRUCTURAL + DYNAMIC COLORING ===
            // Hue: attention entropy (focused=warm) or activation variance
            // Saturation: layer type emphasis
            // Value: activation magnitude
            
            const h = pos.hue;  // Computed from attention entropy or std
            let s = 0.7;
            let v = 0.85;
            
            // Layer type affects saturation (attention = vibrant, norm = muted)
            const typeSaturation = {
                'attention': 0.9,
                'mlp': 0.75,
                'norm': 0.4,
                'embed': 0.85,
                'head': 0.95,
                'dropout': 0.3,
                'conv': 0.8,
                'other': 0.5
            };
            s = typeSaturation[layerType] || 0.6;
            
            // Attention entropy affects value (low entropy = focused = brighter)
            if ('attn_entropy' in data) {
                v = 0.95 - Math.min(data.attn_entropy / 6, 0.4);
            } else if ('std' in data) {
                s = Math.max(s, 0.5 + Math.min(data.std, 1) * 0.4);
            }
            
            if ('mean' in data) {
                v = 0.6 + Math.min(Math.abs(data.mean), 1) * 0.35;
            }
            if ('sparsity' in data) {
                v = v * (1 - data.sparsity * 0.2);
            }
            
            const rgb = hsvToRgb(h, s, v);
            color = (rgb.r << 16) | (rgb.g << 8) | rgb.b;
            
            const glowRgb = hsvToRgb(h, s * 0.5, v * 0.25);
            emissive = (glowRgb.r << 16) | (glowRgb.g << 8) | glowRgb.b;
            
            // Attention layers get extra glow
            if (layerType === 'attention' && 'attn_entropy' in data) {
                const focus = Math.max(0, 1 - data.attn_entropy / 4);
                const focusRgb = hsvToRgb(h, 0.8, focus * 0.5);
                emissive = (focusRgb.r << 16) | (focusRgb.g << 8) | focusRgb.b;
            }
            
            // Size boost from attention focus or other metrics
            if ('attn_entropy' in data) {
                size = size * (1.2 - Math.min(data.attn_entropy / 8, 0.3));
            } else if ('prob' in data) {
                size = 0.5 + data.prob * 0.5;
            } else if ('score' in data) {
                size = 0.5 + data.score * 0.5;
            }
        }
        
        if (!mesh) {
            const geometry = new THREE.SphereGeometry(0.4, 16, 16);  // Increased from 0.3 to 0.4
            const material = new THREE.MeshPhongMaterial({ 
                color, 
                emissive,
                transparent: true, 
                opacity: 0.95,
                shininess: 50
            });
            mesh = new THREE.Mesh(geometry, material);
            mesh.userData = { eventId: evt.event_id, index: i, component: pos.component, layerType: layerType };
            scene.add(mesh);
            nodeObjects.set(evt.event_id, mesh);
        } else {
            mesh.material.color.setHex(color);
            mesh.material.emissive.setHex(emissive);
        }
        
        mesh.scale.setScalar(size);
        mesh.position.lerp(new THREE.Vector3(pos.x, pos.y, pos.z), 0.15);  // Increased from 0.3 for smoother movement
    });
    
    // === CLEAR old edges and traces ===
    edgeObjects.forEach(line => scene.remove(line));
    edgeObjects = [];
    labelSprites.forEach(sprite => scene.remove(sprite));
    labelSprites = [];
    
    // === BUILD list of actual mesh positions (after lerp) ===
    // This ensures edges connect to where nodes actually ARE, not where they're going
    const actualPositions = positions.map((pos, i) => {
        const mesh = nodeObjects.get(pos.evt.event_id);
        if (mesh) {
            return { 
                x: mesh.position.x, 
                y: mesh.position.y, 
                z: mesh.position.z, 
                evt: pos.evt,
                component: pos.component,
                hash: pos.evt.data?.hash,
                parent_hash: pos.evt.data?.parent_hash
            };
        }
        return pos;
    });
    
    // === PROVENANCE-BASED CONNECTIONS ===
    // Edges drawn based on hash lineage, not temporal sequence
    // If no hash data, fall back to temporal order
    const hashToPosition = new Map();
    actualPositions.forEach((pos, i) => {
        if (pos.hash) {
            hashToPosition.set(pos.hash, pos);
        }
    });
    
    actualPositions.forEach((curr, i) => {
        if (i === 0) return;
        
        let prev = null;
        
        // Try to find parent by hash (provenance)
        if (curr.parent_hash && hashToPosition.has(curr.parent_hash)) {
            prev = hashToPosition.get(curr.parent_hash);
        } else {
            // Fall back to temporal order
            prev = actualPositions[i - 1];
        }
        
        if (!prev) return;
        
        const points = [
            new THREE.Vector3(prev.x, prev.y, prev.z),
            new THREE.Vector3(curr.x, curr.y, curr.z)
        ];
        
        // Color based on whether this is a verified provenance edge
        const isProvenanceEdge = curr.parent_hash && hashToPosition.has(curr.parent_hash);
        const edgeColor = isProvenanceEdge ? 0x00aa44 : 0x333333;  // Green = verified lineage
        const edgeOpacity = isProvenanceEdge ? 0.4 : 0.15;
        
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        const material = new THREE.LineBasicMaterial({ color: edgeColor, transparent: true, opacity: edgeOpacity });
        const line = new THREE.Line(geometry, material);
        scene.add(line);
        edgeObjects.push(line);
    });
    
    // === CAUSATION TRACES - VIBRANT with Golden Ratio HSV ===
    activeTraces.forEach((trace, traceIdx) => {
        if (trace.nodes.length < 2) return;
        
        const rgb = hsvToRgb(trace.hue, 1.0, 1.0);  // Full saturation, full brightness
        const traceColor = new THREE.Color(`rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`);
        
        // Draw thick glowing trace line - use ACTUAL mesh positions
        const tracePoints = [];
        const traceColors = [];  // For gradient effect
        const traceSizes = [];   // For progression effect
        
        trace.nodes.forEach((nodeIdx, i) => {
            if (nodeIdx < actualPositions.length) {
                const p = actualPositions[nodeIdx];
                tracePoints.push(new THREE.Vector3(p.x, p.y, p.z));
                
                // Color gradient from start to end (blue -> red for causality flow)
                const progress = i / (trace.nodes.length - 1);
                const hue = (1 - progress) * 0.6;  // Blue (0.6) to Red (0.0)
                const gradRgb = hsvToRgb(hue, 1.0, 1.0);
                traceColors.push(new THREE.Color(`rgb(${gradRgb.r}, ${gradRgb.g}, ${gradRgb.b})`));
                
                // Size progression - nodes get bigger as causality flows
                const sizeProgress = 0.5 + progress * 1.5;  // 0.5x to 2x size
                traceSizes.push(sizeProgress);
            }
        });
        
        if (tracePoints.length > 1) {
            // Main trace line with gradient
            const geometry = new THREE.BufferGeometry().setFromPoints(tracePoints);
            
            // Add color attribute for gradient
            const colors = new Float32Array(tracePoints.length * 3);
            traceColors.forEach((color, i) => {
                colors[i * 3] = color.r;
                colors[i * 3 + 1] = color.g;
                colors[i * 3 + 2] = color.b;
            });
            geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
            
            const material = new THREE.LineBasicMaterial({ 
                vertexColors: true,  // Use gradient colors
                transparent: true, 
                opacity: 0.9
            });
            const line = new THREE.Line(geometry, material);
            scene.add(line);
            edgeObjects.push(line);
            
            // Glow effect
            const glowMaterial = new THREE.LineBasicMaterial({ 
                color: traceColor,
                transparent: true, 
                opacity: 0.4
            });
            const glowLine = new THREE.Line(geometry.clone(), glowMaterial);
            scene.add(glowLine);
            edgeObjects.push(glowLine);
            
            // === CAUSALITY FLOW INDICATORS ===
            // Add pulsing spheres at key points to show direction
            const keyPoints = [0, Math.floor(tracePoints.length / 3), Math.floor(2 * tracePoints.length / 3), tracePoints.length - 1];
            keyPoints.forEach((pointIdx, i) => {
                if (pointIdx < tracePoints.length) {
                    const pos = tracePoints[pointIdx];
                    const size = 0.2 + (i / keyPoints.length) * 0.3;  // Increasing size
                    const markerGeom = new THREE.SphereGeometry(size, 8, 8);
                    const markerMat = new THREE.MeshBasicMaterial({
                        color: traceColors[pointIdx],
                        transparent: true,
                        opacity: 0.8
                    });
                    const marker = new THREE.Mesh(markerGeom, markerMat);
                    marker.position.copy(pos);
                    scene.add(marker);
                    edgeObjects.push(marker);
                    
                    // Add arrow for direction (except last point)
                    if (i < keyPoints.length - 1 && pointIdx < tracePoints.length - 1) {
                        const nextPos = tracePoints[Math.min(pointIdx + 5, tracePoints.length - 1)];
                        const direction = new THREE.Vector3().subVectors(nextPos, pos).normalize();
                        
                        // Create arrow cone
                        const arrowGeom = new THREE.ConeGeometry(0.1, 0.3, 8);
                        const arrowMat = new THREE.MeshBasicMaterial({
                            color: traceColors[pointIdx],
                            transparent: true,
                            opacity: 0.9
                        });
                        const arrow = new THREE.Mesh(arrowGeom, arrowMat);
                        
                        // Position arrow between points and orient along direction
                        const midPoint = new THREE.Vector3().addVectors(pos, nextPos).multiplyScalar(0.5);
                        arrow.position.copy(midPoint);
                        arrow.lookAt(nextPos);
                        arrow.rotateX(Math.PI / 2);  // Adjust for cone orientation
                        
                        scene.add(arrow);
                        edgeObjects.push(arrow);
                    }
                }
            });
        }
        
        // === TEXT LABELS on key trace nodes ===
        const keyIndices = [
            trace.nodes[0],  // Start
            trace.nodes[Math.floor(trace.nodes.length / 2)],  // Middle
            trace.nodes[trace.nodes.length - 1]  // Head
        ];
        
        keyIndices.forEach((nodeIdx, ki) => {
            if (nodeIdx >= actualPositions.length) return;
            const pos = actualPositions[nodeIdx];
            const evt = pos.evt;
            const data = evt.data || {};
            
            // Build label text from metrics
            const parts = [];
            if ('step' in data) parts.push(`s${data.step}`);
            else if ('iter' in data) parts.push(`#${data.iter}`);
            if ('loss' in data) parts.push(`L:${data.loss.toFixed(2)}`);
            if ('acc' in data) parts.push(`${data.acc.toFixed(0)}%`);
            if ('prob' in data) parts.push(`p:${data.prob.toFixed(2)}`);
            
            const labelText = parts.length > 0 ? parts.join(' ') : evt.event_type.slice(0, 8);
            
            // Create canvas texture for label
            const canvas = document.createElement('canvas');
            canvas.width = 256;
            canvas.height = 64;
            const ctx = canvas.getContext('2d');
            
            // Background pill
            ctx.fillStyle = `rgba(0, 0, 0, 0.85)`;
            ctx.roundRect(0, 0, canvas.width, canvas.height, 8);
            ctx.fill();
            
            // Border in trace color
            ctx.strokeStyle = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
            ctx.lineWidth = 3;
            ctx.roundRect(2, 2, canvas.width - 4, canvas.height - 4, 6);
            ctx.stroke();
            
            // Text
            ctx.fillStyle = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
            ctx.font = 'bold 28px JetBrains Mono, monospace';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(labelText, canvas.width / 2, canvas.height / 2);
            
            const texture = new THREE.CanvasTexture(canvas);
            const spriteMaterial = new THREE.SpriteMaterial({ 
                map: texture, 
                transparent: true,
                depthTest: false
            });
            const sprite = new THREE.Sprite(spriteMaterial);
            
            // Position above node - use actual position
            sprite.position.set(pos.x, pos.y + 2, pos.z);
            sprite.scale.set(4, 1, 1);
            
            scene.add(sprite);
            labelSprites.push(sprite);
        });
        
        // Bright sphere at trace head - use actual position
        const headIdx = trace.nodes[trace.nodes.length - 1];
        if (headIdx < actualPositions.length) {
            const headPos = actualPositions[headIdx];
            const headGeom = new THREE.SphereGeometry(0.3, 16, 16);
            const headMat = new THREE.MeshBasicMaterial({ 
                color: traceColor, 
                transparent: true, 
                opacity: 1.0 
            });
            const headSphere = new THREE.Mesh(headGeom, headMat);
            headSphere.position.set(headPos.x, headPos.y, headPos.z);
            scene.add(headSphere);
            edgeObjects.push(headSphere);
            
            // === IMMERSIVE: Highlight metrics for trace head event ===
            highlightTraceMetrics(trace, actualPositions);
        }
    });
    
    // If no active traces, clear highlights
    if (activeTraces.length === 0) {
        scheduleHighlightClear(300);
    }
    } catch (e) {
        console.error('CASCADE renderFlow error:', e);
    }
}

// === RENDER OPTIMIZATION ===
let renderScheduled = false;
let lastRenderTime = 0;
const MIN_RENDER_INTERVAL = 16; // Cap at ~60fps for smooth animation (was 50ms)

function scheduleRender() {
    if (renderScheduled) return;
    renderScheduled = true;
    
    requestAnimationFrame(() => {
        renderFlow();
        lastRenderTime = performance.now();
        renderScheduled = false;
    });
}

// Trace class - follows TRUE temporal order
class CausationTrace {
    constructor(startNode, endNode, hue) {
        // Trace walks from start to end along TRUE timeline
        this.startNode = Math.min(startNode, endNode);
        this.endNode = Math.max(startNode, endNode);
        this.currentPos = this.startNode;
        this.nodes = [this.startNode];
        this.hue = hue;
        this.age = 0;
        this.maxAge = (this.endNode - this.startNode) * 3 + 20;  // Proportional to length
        this.complete = false;
    }
    
    grow(totalNodes) {
        this.age++;
        
        // Walk forward through TRUE temporal sequence
        if (!this.complete && this.currentPos < this.endNode && this.currentPos < totalNodes - 1) {
            this.currentPos++;
            this.nodes.push(this.currentPos);
            
            if (this.currentPos >= this.endNode) {
                this.complete = true;
            }
        }
        
        // Fade out after completion
        if (this.complete && this.age > this.maxAge * 0.7) {
            // Start fading - remove from head
            if (this.nodes.length > 1 && this.age % 2 === 0) {
                this.nodes.shift();
            }
        }
        
        return this.age < this.maxAge && this.nodes.length > 0;
    }
}

function spawnTrace() {
    const eventCount = events.size;
    if (eventCount < 3) return;  // Lower threshold - only need 3 events
    
    // Clear OLD traces first to prevent stacking
    if (activeTraces.length > MAX_TRACES / 2) {
        activeTraces = activeTraces.slice(-Math.floor(MAX_TRACES / 2));
    }
    
    // Pick TRUE temporal segment to trace
    const maxLen = Math.min(eventCount - 1, 20);  // Cap segment length
    const segmentLength = Math.floor(Math.random() * maxLen) + 3;  // 3-23 nodes
    const startNode = Math.floor(Math.random() * Math.max(1, eventCount - segmentLength));
    const endNode = Math.min(startNode + segmentLength, eventCount - 1);
    
    const trace = new CausationTrace(startNode, endNode, getNextHue());
    activeTraces.push(trace);
    
    // Strict limit to prevent infinite stacking
    while (activeTraces.length > MAX_TRACES) {
        activeTraces.shift();
    }
}

function tickTraces() {
    const eventCount = events.size;
    
    // Filter out dead traces AND limit total number to prevent stacking
    activeTraces = activeTraces.filter(trace => trace.grow(eventCount));
    
    // Aggressive cleanup - keep only fresh traces
    if (activeTraces.length > 10) {
        // Keep only the 10 most recent traces
        activeTraces = activeTraces.slice(-10);
    }
    
    scheduleRender();  // Use throttled render
}

function checkIdleMode() {
    const now = Date.now();
    const wasIdle = idleMode;
    idleMode = (now - lastInteraction) > IDLE_TIMEOUT;
    
    const indicator = document.getElementById('idle-indicator');
    if (indicator) {
        indicator.classList.toggle('active', idleMode);
    }
    
    if (idleMode && !wasIdle) {
        console.log('[CASCADE] Entering ambient mode...');
    }
}

function registerInteraction() {
    lastInteraction = Date.now();
    idleMode = false;
}

// Tick at smooth rate - increased interval for smoother animation
setInterval(() => {
    checkIdleMode();
    if (idleMode && events.size >= 3) {
        tickTraces();
    }
}, 100);  // Tick faster, but render is throttled

// Spawn new traces
setInterval(() => {
    if (idleMode && events.size >= 3) {
        spawnTrace();
    }
}, TRACE_INTERVAL);

// Track interactions - only intentional actions reset idle
document.addEventListener('click', registerInteraction);
document.addEventListener('keydown', registerInteraction);
// Mousemove does NOT reset idle - only drag does (handled in init3D)

// === MOBILE NAV ===
function showMobilePanel(panelName) {
    const panels = ['left-panel', 'canvas-container', 'right-panel'];
    const buttons = document.querySelectorAll('.mobile-nav button');
    
    panels.forEach(id => {
        const el = document.getElementById(id);
        if (el) el.classList.remove('mobile-active');
    });
    buttons.forEach(btn => btn.classList.remove('active'));
    
    const targetPanel = document.getElementById(panelName);
    if (targetPanel) targetPanel.classList.add('mobile-active');
    
    // Highlight active button
    const activeBtn = document.querySelector(`.mobile-nav button[onclick*="${panelName}"]`);
    if (activeBtn) activeBtn.classList.add('active');
    
    // Trigger resize for canvas
    if (panelName === 'canvas-container') {
        cachedDimensions = null;
        scheduleRender();
    }
}

// === INIT ===
connect();
window.addEventListener('resize', () => {
    cachedDimensions = null;  // Force dimension recalc
    scheduleRender();
});
</script>

<!-- Mobile Navigation - Hidden on desktop -->
<div class="mobile-nav">
    <button onclick="showMobilePanel('left-panel')">📊 Flow</button>
    <button class="active" onclick="showMobilePanel('canvas-container')">🌐 3D</button>
    <button onclick="showMobilePanel('right-panel')">📈 Stats</button>
</div>

</body>
</html>
'''


class CascadeLiveServerV2:
    """The Bridge - Live visualization server."""
    
    def __init__(self, port=8888):
        self.port = port
    
    def start(self, open_browser=True):
        socketserver.TCPServer.allow_reuse_address = True
        
        class Handler(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                if self.path == '/events':
                    self.send_response(200)
                    self.send_header('Content-type', 'text/event-stream')
                    self.send_header('Cache-Control', 'no-cache')
                    self.send_header('Access-Control-Allow-Origin', '*')
                    self.end_headers()
                    
                    while True:
                        try:
                            while not event_queue.empty():
                                data = event_queue.get_nowait()
                                self.wfile.write(f"data: {json.dumps(data)}\n\n".encode())
                                self.wfile.flush()
                            self.wfile.write(b": keepalive\n\n")
                            self.wfile.flush()
                            time.sleep(0.3)
                        except:
                            return
                else:
                    self.send_response(200)
                    self.send_header('Content-type', 'text/html')
                    self.end_headers()
                    self.wfile.write(HTML_TEMPLATE.encode())
            
            def log_message(self, *args): pass
        
        print(f"[CASCADE] The Bridge → http://localhost:{self.port}")
        threading.Thread(
            target=lambda: socketserver.TCPServer(("", self.port), Handler).serve_forever(),
            daemon=True
        ).start()
        
        if open_browser:
            webbrowser.open(f"http://localhost:{self.port}")
