<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Everysight Maverick AI Simulator</title>
    <link rel="icon" type="image/png" href="/favicon.png">
    <link rel="shortcut icon" type="image/png" href="/favicon.png">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link
        href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;400;600&family=JetBrains+Mono:wght@400;600&display=swap"
        rel="stylesheet">
    <!--__SIM_RELEASE_PLACEHOLDER__-->
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        'sim-bg': '#111723',
                        'sim-accent': '#EBEB70',
                        'sim-surface': '#1C2233',
                        'sim-border': '#1C2233',
                        'sim-text-secondary': '#8B9DAE',
                    },
                    fontFamily: {
                        display: ['Plus Jakarta Sans', 'ui-sans-serif', 'system-ui'],
                        mono: ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular']
                    },
                    borderRadius: {
                        'card': '32px',
                    }
                }
            }
        }
    </script>
    <script defer src="https://unpkg.com/lucide@latest"></script>
    <style>
        :root {
            --es-yellow: #EBEB70;
            --es-dark-blue: #111723;
            --es-dark-blue-2: #1C2233;
            --es-light-grey: #E7E7E7;
            --es-dark-grey: #D7D7D7;
            --es-blue-grey: #8B9DAE;
            --es-light-blue: #A6BBCA;
            --es-yellow-2: #D7D161;
            --accent-blue: #A6BBCA;
            --accent-purple: #a78bfa;
            --accent-yellow: #EBEB70;
            --border: rgba(255, 255, 255, 0.12);
            --text-secondary: rgba(139, 157, 174, 0.9);
        }

        body {
            font-size: 15px;
            font-family: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, Segoe UI, Arial, sans-serif;
        }

        button,
        input,
        textarea,
        select {
            font-family: inherit;
        }

        body .text-zinc-500 {
            color: rgba(229, 231, 235, 0.78) !important;
        }

        body .text-zinc-400 {
            color: rgba(229, 231, 235, 0.88) !important;
        }

        .accordion-chevron {
            width: 18px;
            height: 18px;
            transition: transform 160ms ease;
            opacity: 0.9;
        }

        .accordion-open .accordion-chevron {
            transform: rotate(180deg);
        }

        .ios-switch {
            position: relative;
            display: inline-block;
            width: 44px;
            height: 26px;
        }

        .ios-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .ios-slider {
            position: absolute;
            cursor: pointer;
            inset: 0;
            background: rgba(255, 255, 255, 0.12);
            border: 1px solid rgba(255, 255, 255, 0.16);
            border-radius: 999px;
            transition: background 160ms ease, border-color 160ms ease;
        }

        .ios-slider:before {
            position: absolute;
            content: "";
            height: 22px;
            width: 22px;
            left: 2px;
            top: 1px;
            background: #fff;
            border-radius: 999px;
            transition: transform 160ms ease;
        }

        .ios-switch input:checked+.ios-slider {
            background: rgba(235, 235, 112, 0.95);
            border-color: rgba(235, 235, 112, 0.75);
        }

        .ios-switch input:checked+.ios-slider:before {
            transform: translateX(18px);
            background: #0b0b0b;
        }

        input[type="checkbox"] {
            accent-color: #EBEB70;
            width: 16px;
            height: 16px;
        }

        .replay-row {
            opacity: 0.5;
            transition: opacity 0.2s, background 0.2s, border 0.2s;
        }

        .replay-row.is-completed,
        .replay-row.is-current-step {
            opacity: 1 !important;
        }

        .replay-row.is-current-step {
            background: rgba(235, 235, 112, 0.25) !important;
            border-color: rgba(235, 235, 112, 0.6) !important;
            box-shadow: 0 0 0 1px rgba(235, 235, 112, 0.4);
        }

        /* Replay Panel Minimization */
        #replay-panel.minimized {
            transform: translateX(calc(100% - 48px)) !important;
            cursor: pointer;
        }

        #replay-panel.minimized:hover {
            transform: translateX(calc(100% - 56px)) !important;
        }

        #replay-panel.minimized>*:not(.replay-panel-header) {
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s ease;
        }

        .replay-panel-header {
            transition: all 0.3s ease;
        }

        #replay-panel.minimized .replay-panel-header {
            padding: 1.5rem 0.75rem !important;
        }

        #replay-panel.minimized #replay-filename,
        #replay-panel.minimized h2 {
            display: none;
        }

        :root {
            --nd2-bg: #070b12;
            --nd2-surface: #101720;
            --nd2-surface-2: #151d28;
            --nd2-text: #f4f7fb;
            --nd2-text-2: #a9b5c4;
            --nd2-muted: rgba(169, 181, 196, 0.62);
            --nd2-line: rgba(255, 255, 255, 0.1);
            --nd2-yellow: #f3ef5e;
            --nd2-purple: #6f42e8;
            --nd2-red: #cf2f2f;
            --nd2-green: #34f27a;
            --nd2-blue: #4f8dff;
            --nd2-glass-blur: 18px;
            --nd2-glass-saturate: 1.18;
            --nd2-panel-radius: 20px;
            --nd2-glass-shadow: 0 22px 54px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.1);
            --mars-bg-x: 50%;
            --mars-bg-y: 50%;
            --mars-bg-rot: 0deg;
        }

        body.nd2-body {
            min-height: 100vh;
            overflow-x: hidden;
            color: var(--nd2-text);
            background-color: var(--nd2-bg);
            position: relative;
            background-image:
                radial-gradient(circle at 50% 34%, rgba(87, 133, 178, 0.22), transparent 38%),
                linear-gradient(90deg, rgba(4, 7, 12, 0.84), rgba(4, 7, 12, 0.33) 32%, rgba(4, 7, 12, 0.3) 68%, rgba(4, 7, 12, 0.86)),
                linear-gradient(180deg, rgba(3, 6, 12, 0.34), rgba(3, 6, 12, 0.78)),
                url("/asset/new-design/background.jpg");
            background-size: cover;
            background-position: center;
            background-attachment: fixed;
        }

        body.nd2-bg-day {
            background-image:
                radial-gradient(circle at 50% 26%, rgba(255, 239, 184, 0.18), transparent 34%),
                linear-gradient(90deg, rgba(4, 7, 12, 0.72), rgba(4, 7, 12, 0.28) 34%, rgba(4, 7, 12, 0.25) 68%, rgba(4, 7, 12, 0.78)),
                linear-gradient(180deg, rgba(3, 6, 12, 0.28), rgba(3, 6, 12, 0.72)),
                url("/asset/new-design/background-day.png");
        }

        body.nd2-bg-night {
            background-image:
                radial-gradient(circle at 50% 34%, rgba(87, 133, 178, 0.22), transparent 38%),
                linear-gradient(90deg, rgba(4, 7, 12, 0.84), rgba(4, 7, 12, 0.33) 32%, rgba(4, 7, 12, 0.3) 68%, rgba(4, 7, 12, 0.86)),
                linear-gradient(180deg, rgba(3, 6, 12, 0.34), rgba(3, 6, 12, 0.78)),
                url("/asset/new-design/background.jpg");
        }

        body.nd2-bg-video {
            background-image:
                radial-gradient(circle at 50% 34%, rgba(87, 133, 178, 0.18), transparent 38%),
                linear-gradient(90deg, rgba(4, 7, 12, 0.82), rgba(4, 7, 12, 0.24) 35%, rgba(4, 7, 12, 0.24) 65%, rgba(4, 7, 12, 0.84)),
                linear-gradient(180deg, rgba(3, 6, 12, 0.28), rgba(3, 6, 12, 0.78));
        }

        body.nd2-bg-mars {
            background-image: none;
            background-color: #271006;
        }

        body.nd2-bg-mars::before {
            content: "";
            position: fixed;
            inset: -24vmax;
            pointer-events: none;
            z-index: 0;
            background-image:
                url("/asset/new-design/media/mars-360.png");
            background-size: 220% auto;
            background-position: var(--mars-bg-x) var(--mars-bg-y);
            background-repeat: no-repeat;
            transform: rotate(var(--mars-bg-rot)) scale(1.08);
            transform-origin: center;
            will-change: transform, background-position;
        }

        body.nd2-bg-mars::after {
            content: "";
            position: fixed;
            inset: 0;
            pointer-events: none;
            z-index: 1;
            background:
                radial-gradient(circle at 50% 24%, rgba(255, 232, 184, 0.18), transparent 34%),
                linear-gradient(90deg, rgba(45, 19, 8, 0.58), rgba(45, 19, 8, 0.12) 34%, rgba(45, 19, 8, 0.12) 66%, rgba(45, 19, 8, 0.62)),
                linear-gradient(180deg, rgba(58, 21, 8, 0.14), rgba(8, 5, 4, 0.42));
        }

        .nd2-site-bg-video {
            position: fixed;
            inset: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            opacity: 0;
            pointer-events: none;
            transition: opacity 220ms ease;
            z-index: 0;
        }

        body.nd2-bg-video .nd2-site-bg-video {
            opacity: 1;
        }

        body.nd2-bg-video::before {
            content: "";
            position: fixed;
            inset: 0;
            pointer-events: none;
            z-index: 1;
            background:
                radial-gradient(circle at 50% 34%, rgba(96, 151, 210, 0.16), transparent 42%),
                linear-gradient(90deg, rgba(4, 7, 12, 0.76), rgba(4, 7, 12, 0.22) 34%, rgba(4, 7, 12, 0.22) 66%, rgba(4, 7, 12, 0.82)),
                linear-gradient(180deg, rgba(3, 6, 12, 0.22), rgba(3, 6, 12, 0.78));
        }

        .nd2-shell {
            width: min(100%, 2048px);
            margin: 0 auto;
            padding: 26px 28px 32px;
            position: relative;
            z-index: 2;
        }

        .nd2-glass-panel {
            background: linear-gradient(180deg, rgba(17, 22, 31, 0.58), rgba(8, 12, 18, 0.44));
            border: 1px solid rgba(210, 226, 255, 0.16);
            border-radius: var(--nd2-panel-radius);
            box-shadow: var(--nd2-glass-shadow);
            backdrop-filter: blur(var(--nd2-glass-blur)) saturate(var(--nd2-glass-saturate));
            -webkit-backdrop-filter: blur(var(--nd2-glass-blur)) saturate(var(--nd2-glass-saturate));
        }

        .nd2-heading {
            color: var(--nd2-text);
            font-size: 12px;
            font-weight: 700;
            letter-spacing: 0.42em;
            text-transform: uppercase;
        }

        .nd2-subtle {
            color: var(--nd2-text-2);
        }

        .nd2-brand-wordmark {
            color: var(--nd2-yellow);
            font-size: 12px;
            font-weight: 700;
            letter-spacing: 0.42em;
        }

        .nd2-brand-logo {
            width: 148px;
            height: auto;
            object-fit: contain;
            object-position: left center;
        }

        .nd2-dashboard-grid {
            display: grid;
            grid-template-columns: minmax(340px, 1fr) minmax(640px, 2fr) minmax(340px, 1fr);
            gap: 18px;
            align-items: start;
            margin-top: 30px;
            position: relative;
            overflow: visible;
        }

        .nd2-rail {
            display: flex;
            flex-direction: column;
            gap: 24px;
            min-width: 0;
            position: relative;
            z-index: 30;
        }

        #col-center {
            position: relative;
            z-index: 10;
            min-width: 0;
        }

        .nd2-card {
            padding: 24px 24px 22px;
        }

        .nd2-card-compact {
            padding: 20px 22px;
        }

        .nd2-topbar {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 24px;
            position: relative;
            z-index: 120;
        }

        .nd2-top-actions {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: 18px;
            flex-wrap: wrap;
        }

        .nd2-stat-cluster,
        .nd2-toggle-cluster {
            display: flex;
            align-items: stretch;
            overflow: hidden;
            border-radius: 24px;
        }

        .nd2-render-controls {
            position: relative;
            z-index: 80;
        }

        .nd2-lens-settings-popover {
            position: absolute;
            top: calc(100% + 10px);
            right: 0;
            width: min(430px, calc(100vw - 40px));
            z-index: 140;
        }

        .nd2-settings-button {
            min-height: 28px;
            padding: 0 10px;
            border-radius: 999px;
            background: rgba(243, 239, 94, 0.12);
            border: 1px solid rgba(243, 239, 94, 0.28);
            color: var(--nd2-yellow);
            font-size: 10px;
            font-weight: 800;
            letter-spacing: 0.12em;
            text-transform: uppercase;
        }

        .nd2-stat {
            min-width: 92px;
            padding: 12px 18px;
            text-align: center;
            border-right: 1px solid rgba(255, 255, 255, 0.1);
        }

        .nd2-stat:last-child,
        .nd2-toggle-item:last-child {
            border-right: 0;
        }

        .nd2-stat-label,
        .nd2-field-label {
            color: var(--nd2-text-2);
            font-size: 10px;
            font-weight: 700;
            letter-spacing: 0.16em;
            text-transform: uppercase;
        }

        .nd2-stat-value {
            margin-top: 5px;
            color: var(--nd2-text);
            font-family: "JetBrains Mono", ui-monospace, monospace;
            font-size: 25px;
            font-weight: 700;
            line-height: 1;
        }

        .nd2-toggle-item {
            min-width: 92px;
            padding: 11px 14px 10px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 8px;
            border-right: 1px solid rgba(255, 255, 255, 0.1);
        }

        .nd2-segment {
            display: flex;
            overflow: hidden;
            border-radius: 16px;
            background: rgba(0, 0, 0, 0.46);
            border: 1px solid rgba(255, 255, 255, 0.08);
        }

        .nd2-segment button {
            min-height: 46px;
            padding: 0 24px;
            font-size: 12px;
            font-weight: 800;
            letter-spacing: 0.08em;
            text-transform: uppercase;
        }

        .nd2-bg-mode-segment {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            overflow: hidden;
            border-radius: 14px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            background: rgba(0, 0, 0, 0.26);
        }

        .nd2-bg-mode-button {
            min-height: 40px;
            border-right: 1px solid rgba(255, 255, 255, 0.08);
            color: rgba(244, 247, 251, 0.7);
            font-size: 11px;
            font-weight: 800;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            transition: background 160ms ease, color 160ms ease;
        }

        .nd2-bg-mode-button:last-child {
            border-right: 0;
        }

        .nd2-bg-mode-button.is-active {
            background: var(--nd2-yellow);
            color: #101720;
        }

        .nd2-command-power {
            background: linear-gradient(180deg, #fff76b, var(--nd2-yellow));
            color: #101720;
        }

        .nd2-command-load {
            background: linear-gradient(180deg, #8f62ff, var(--nd2-purple));
            color: var(--nd2-text);
        }

        .nd2-pill {
            min-height: 46px;
            display: inline-flex;
            align-items: center;
            gap: 10px;
            padding: 0 20px;
            border-radius: 14px;
            font-size: 12px;
            font-weight: 800;
            letter-spacing: 0.08em;
            text-transform: uppercase;
        }

        .nd2-recording-pill {
            background: linear-gradient(180deg, #d83b36, #a91f1f);
            color: var(--nd2-text);
            border: 1px solid rgba(255, 110, 110, 0.22);
        }

        .nd2-status-pill {
            background: rgba(17, 22, 31, 0.76);
            border: 1px solid rgba(255, 255, 255, 0.08);
        }

        .nd2-status-dot {
            width: 8px;
            height: 8px;
            flex: 0 0 8px;
            border-radius: 999px;
            background: var(--nd2-red);
            color: var(--nd2-red);
            box-shadow: 0 0 14px currentColor;
        }

        .nd2-status-dot.is-connected {
            background: var(--nd2-green);
            color: var(--nd2-green);
        }

        .nd2-icon {
            width: 22px;
            height: 22px;
            object-fit: contain;
        }

        .nd2-icon-box {
            width: 36px;
            height: 36px;
            border-radius: 12px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background: rgba(243, 239, 94, 0.12);
            box-shadow: 0 0 20px rgba(243, 239, 94, 0.09);
        }

        .nd2-range,
        input.nd2-range {
            width: 100%;
            accent-color: var(--nd2-yellow);
        }

        .nd2-meter {
            height: 7px;
            border-radius: 999px;
            background: rgba(255, 255, 255, 0.12);
            border: 1px solid rgba(255, 255, 255, 0.08);
            overflow: hidden;
        }

        .nd2-meter-fill {
            height: 100%;
            background: var(--nd2-yellow);
            border-radius: 999px;
            box-shadow: 0 0 10px rgba(243, 239, 94, 0.45);
        }

        .nd2-lens-area {
            min-height: 760px;
            display: flex;
            align-items: flex-start;
            justify-content: center;
            position: relative;
            overflow: visible;
        }

        .nd2-lens-stage {
            position: relative;
            width: 1650px;
            height: 950px;
            flex: 0 0 auto;
            z-index: 5;
            margin-top: -72px;
        }

        .nd2-lens-screen {
            position: absolute;
            left: calc(50% - 320px);
            top: 25%;
            width: 640px;
            height: 400px;
            overflow: hidden;
            border-radius: 12px;
            background: transparent;
            box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.72), 0 0 28px rgba(0, 0, 0, 0.36);
        }

        .nd2-glasses-frame {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            object-fit: contain;
            pointer-events: none;
            z-index: 35;
        }

        .nd2-lens-label {
            position: absolute;
            left: calc(50% - 360px);
            top: 54px;
            width: 720px;
            z-index: 44;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-wrap: wrap;
            gap: 8px;
            color: rgba(244, 247, 251, 0.72);
            font-family: "JetBrains Mono", ui-monospace, monospace;
            font-size: 11px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
        }

        .nd2-viewport-pill {
            padding: 4px 10px;
            border-radius: 999px;
            background: rgba(0, 0, 0, 0.55);
            border: 1px solid rgba(255, 255, 255, 0.12);
            color: rgba(244, 247, 251, 0.86);
            font-size: 10px;
            letter-spacing: 0.14em;
        }

        .nd2-touch-button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            min-height: 64px;
        }

        .nd2-touch-action-icon {
            width: 24px;
            height: 24px;
            flex: 0 0 24px;
        }

        .nd2-cache-panel {
            width: min(100%, 980px);
            margin: -10px auto 0;
            position: relative;
            z-index: 30;
        }

        .nd2-bottom-bar {
            width: min(100%, 980px);
            margin: 24px auto 0;
            display: grid;
            grid-template-columns: 1fr;
            gap: 22px;
            position: relative;
            z-index: 30;
        }

        .nd2-menu-row {
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: space-between;
            min-height: 76px;
            padding: 0 22px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.08);
            text-align: left;
        }

        .nd2-menu-row:last-child {
            border-bottom: 0;
        }

        .nd2-menu-body {
            padding: 16px 22px 22px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.08);
            color: var(--nd2-text-2);
            font-family: "JetBrains Mono", ui-monospace, monospace;
            font-size: 12px;
            line-height: 1.5;
        }

        .nd2-release-mode .nd2-dev-panel,
        .nd2-release-mode #panel-asset-cache,
        .nd2-release-mode #btn-load-session,
        .nd2-release-mode #recording-shortcut,
        .nd2-release-mode #btn-recording-main,
        .nd2-release-mode #log-area {
            display: none !important;
        }

        .nd2-release-mode .nd2-dashboard-grid {
            grid-template-columns: minmax(340px, 1fr) minmax(640px, 2fr) minmax(340px, 1fr);
        }

        .nd2-menu-label {
            display: flex;
            align-items: center;
            gap: 18px;
        }

        .nd2-search {
            background: rgba(0, 0, 0, 0.22);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            color: var(--nd2-text);
        }

        .nd2-hidden-support {
            display: none !important;
        }

        #canvas-container {
            width: 640px !important;
            height: 400px !important;
            border-radius: 0 !important;
            background: transparent;
        }

        #canvas-container video,
        #c,
        #brightness-overlay {
            border-radius: 0 !important;
        }

        #c {
            image-rendering: auto;
        }

        @media (max-width: 1640px) {
            .nd2-shell {
                padding-inline: 18px;
            }

            .nd2-dashboard-grid {
                grid-template-columns: minmax(320px, 1fr) minmax(640px, 2fr) minmax(320px, 1fr);
                gap: 16px;
            }

            .nd2-lens-stage {
                width: 1560px;
                height: 898px;
                margin-top: -56px;
            }

            .nd2-lens-screen {
                left: calc(50% - 320px);
                top: 25%;
            }
        }

        @media (max-width: 1280px) {
            body.nd2-body {
                overflow: auto;
            }

            .nd2-topbar,
            .nd2-top-actions {
                align-items: flex-start;
                justify-content: flex-start;
            }

            .nd2-topbar {
                flex-direction: column;
            }

            .nd2-dashboard-grid {
                grid-template-columns: 1fr;
            }

            .nd2-lens-stage {
                margin-inline: auto;
            }

            .nd2-lens-area {
                min-height: 560px;
                overflow: hidden;
            }

            .nd2-bottom-bar {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>

<body class="nd2-body nd2-bg-night text-zinc-100 font-display min-h-screen select-none">
    <video id="site-bg-video" class="nd2-site-bg-video" muted loop playsinline preload="auto"></video>
    <div class="nd2-shell">
        <header class="nd2-topbar">
            <div class="flex items-center gap-4">
                <img src="/asset/new-design/brand-everysight-symbol-yellow.png" alt="Everysight" class="w-16 h-16 object-contain">
                <div>
                    <img src="/asset/new-design/brand-everysight-header-dark.png" alt="Everysight" class="nd2-brand-logo">
                    <h1 class="text-xl font-bold text-white uppercase tracking-tight mt-1">Maverick AI Simulator</h1>
                    <p id="simulator-version" class="text-[11px] nd2-subtle font-semibold uppercase tracking-[0.35em] mt-1"></p>
                    <p class="text-[11px] nd2-subtle font-semibold uppercase tracking-[0.35em]">The world through new eyes</p>
                </div>
            </div>

            <div class="nd2-top-actions">
                <div class="nd2-stat-cluster nd2-glass-panel">
                    <div class="nd2-stat">
                        <div class="nd2-stat-label">FPS</div>
                        <div id="stat-frames" class="nd2-stat-value">0</div>
                    </div>
                    <div class="nd2-stat">
                        <div class="nd2-stat-label">Objects</div>
                        <div id="obj-count" class="nd2-stat-value">0</div>
                    </div>
                    <div class="nd2-stat">
                        <div class="nd2-stat-label">Animators</div>
                        <div id="anim-count" class="nd2-stat-value">0</div>
                    </div>
                </div>

                <div class="nd2-render-controls">
                    <div class="nd2-toggle-cluster nd2-glass-panel">
                        <div class="nd2-toggle-item">
                            <div class="nd2-stat-label">Clipping</div>
                            <label class="ios-switch">
                                <input id="clip-enabled" type="checkbox" onchange="updateClipEnabled(event)">
                                <span class="ios-slider"></span>
                            </label>
                        </div>
                        <div class="nd2-toggle-item">
                            <div class="nd2-stat-label">Blacks</div>
                            <label class="ios-switch">
                                <input id="show-blacks" type="checkbox" onchange="updateShowBlacks(event)">
                                <span class="ios-slider"></span>
                            </label>
                        </div>
                        <div class="nd2-toggle-item">
                            <div class="nd2-stat-label">Transparent</div>
                            <label class="ios-switch">
                                <input id="transparent-blacks" type="checkbox" onchange="updateTransparentBlacks(event)">
                                <span class="ios-slider"></span>
                            </label>
                            <label class="flex items-center gap-1 text-[9px] font-semibold uppercase tracking-[0.12em] nd2-subtle cursor-pointer">
                                <input id="transparent-blacks-clip" data-transparent-blacks-clip="1" type="checkbox" onchange="updateTransparentBlackClip(event)">
                                <span>Clip</span>
                            </label>
                        </div>
                        <div class="nd2-toggle-item">
                            <div class="nd2-stat-label">Params</div>
                            <label class="ios-switch">
                                <input id="dbg-params" type="checkbox" onchange="updateDbgParams(event)">
                                <span class="ios-slider"></span>
                            </label>
                        </div>
                        <div class="nd2-toggle-item">
                            <div class="nd2-stat-label">Virtual Display</div>
                            <button type="button" class="nd2-settings-button" onclick="toggleLensViewSettings(event)">Settings</button>
                        </div>
                    </div>

                    <div id="lens-view-settings-popover" class="nd2-lens-settings-popover nd2-glass-panel nd2-card-compact space-y-4 hidden">
                        <div class="flex items-center justify-between">
                            <span class="nd2-heading">Virtual Display Settings</span>
                            <button type="button" class="text-[10px] font-bold uppercase tracking-[0.16em] text-zinc-400 hover:text-white" onclick="toggleLensViewSettings(event, false)">Close</button>
                        </div>
                        <div class="nd2-bg-mode-segment" role="radiogroup" aria-label="Lens view background">
                            <button type="button" class="nd2-bg-mode-button" data-site-bg-mode="day" onclick="setSiteBgMode('day')">Day</button>
                            <button type="button" class="nd2-bg-mode-button" data-site-bg-mode="night" onclick="setSiteBgMode('night')">Night</button>
                            <button type="button" class="nd2-bg-mode-button" data-site-bg-mode="video" onclick="setSiteBgMode('video')">Video</button>
                            <button type="button" class="nd2-bg-mode-button" data-site-bg-mode="mars" onclick="setSiteBgMode('mars')">Mars 360</button>
                        </div>
                        <p id="site-bg-msg" class="text-[10px] font-mono text-zinc-400">Night background</p>
                        <label class="flex items-center justify-between gap-3 cursor-pointer">
                            <span class="text-[11px] font-semibold uppercase tracking-[0.2em] nd2-subtle">Show Virtual Display Boundaries</span>
                            <input id="show-display-boundaries" type="checkbox" checked onchange="updateDisplayBoundaries(event)">
                        </label>
                        <div id="bg-hud-opacity-wrap" class="space-y-2">
                            <div class="flex items-center justify-between text-[11px] font-semibold uppercase tracking-[0.2em] nd2-subtle">
                                <span>Screen Content Opacity</span>
                                <span id="bg-hud-opacity-val" class="font-mono text-zinc-300">100%</span>
                            </div>
                            <input id="bg-hud-opacity" type="range" min="0" max="100" step="1" value="100" class="nd2-range accent-sim-accent">
                        </div>
                        <div class="space-y-2">
                            <div class="flex items-center justify-between text-[11px] font-semibold uppercase tracking-[0.2em] nd2-subtle">
                                <span>Screen BG Opacity</span>
                                <span id="screen-bg-opacity-val" class="font-mono text-zinc-300">100%</span>
                            </div>
                            <input id="screen-bg-opacity" type="range" min="0" max="100" step="1" value="100" class="nd2-range accent-sim-accent">
                        </div>
                    </div>
                </div>

                <div class="nd2-segment">
                    <button id="btn-power-on" onclick="powerOn()" class="nd2-command-power bg-sim-accent text-black">Power On</button>
                    <button id="btn-standby" onclick="powerOff()" class="text-white/80 bg-transparent">Standby</button>
                    <button id="btn-load-session" onclick="showReplayPicker()" class="nd2-command-load">Load</button>
                </div>

                <div id="recording-shortcut" onclick="toggleRecording()" class="nd2-pill nd2-recording-pill cursor-pointer bg-zinc-800/40 border-white/10 transition-all duration-300 group">
                    <div id="recording-pulse" class="w-2 h-2 rounded-full bg-white transition-all duration-300"></div>
                    <span id="recording-shortcut-text" class="text-white uppercase transition">Recording</span>
                </div>

                <div class="nd2-pill nd2-status-pill">
                    <div id="status-dot" class="nd2-status-dot bg-red-500 animate-pulse"></div>
                    <span id="status-badge" class="text-white uppercase">Disconnected</span>
                </div>
            </div>
        </header>

        <main id="main-grid" class="nd2-dashboard-grid">
            <section id="col-left" class="nd2-rail">
                <div id="brightness-panel" class="nd2-glass-panel nd2-card">
                    <div class="flex items-center justify-between">
                        <span class="nd2-heading">Brightness</span>
                        <span id="brightness-percent" class="text-white font-semibold text-xl">0%</span>
                    </div>
                    <div class="nd2-meter mt-5">
                        <div id="brightness-bar" class="nd2-meter-fill" style="width:0%;"></div>
                    </div>
                    <div class="mt-5 space-y-3">
                        <div class="flex items-center justify-between text-[11px] font-semibold uppercase nd2-subtle">
                            <span>Ambient</span><span id="ambient-lux-val">150 lx</span>
                        </div>
                        <input id="ambient-lux" type="range" min="0" max="10000" step="25" value="150" class="nd2-range accent-sim-accent">
                        <div id="auto-brightness-mode" class="text-[11px] font-mono text-white uppercase tracking-[0.12em]">AUTO BRIGHTNESS: OFF</div>
                    </div>
                </div>

                <div id="battery-panel" class="nd2-glass-panel nd2-card-compact">
                    <div class="flex items-center justify-between">
                        <div class="flex items-center gap-3">
                            <span class="nd2-heading">Battery</span>
                            <img src="/asset/new-design/icons/icon-battery.svg" alt="" class="w-8 h-auto">
                            <label class="flex items-center gap-2 cursor-pointer">
                                <input type="checkbox" id="battery-charging" class="sr-only peer">
                                <span class="text-[11px] font-semibold text-white uppercase tracking-wider">Charging</span>
                            </label>
                        </div>
                        <span id="battery-percent" class="text-white font-semibold text-xl">100%</span>
                    </div>
                    <div class="mt-4">
                        <input type="range" id="battery-slider" min="0" max="100" value="100" class="nd2-range accent-sim-accent">
                    </div>
                </div>

                <div id="sensor-panel" class="nd2-glass-panel nd2-card">
                    <div class="flex justify-between items-center mb-5">
                        <span class="nd2-heading">Sensors</span>
                        <span id="sensors-status-badge" class="text-[11px] text-zinc-400 font-mono">DISABLED</span>
                    </div>
                    <div class="space-y-4">
                        <div class="space-y-1.5">
                            <div class="flex justify-between text-[11px] font-semibold uppercase nd2-subtle">
                                <span>Yaw</span>
                                <span class="flex items-center gap-2">
                                    <span id="yaw-val" class="font-mono text-white">0°</span>
                                    <button type="button" data-sensor-axis-reset="1" onclick="resetSensorAxis('yaw')" disabled class="w-6 h-6 rounded-lg bg-white/5 border border-white/10 text-[10px] font-bold text-white hover:bg-white/10 disabled:opacity-40 disabled:pointer-events-none">R</button>
                                </span>
                            </div>
                            <input id="yaw-slider" type="range" min="-180" max="180" step="1" value="0" class="nd2-range disabled:opacity-50" disabled oninput="sendSensorData()">
                        </div>
                        <div class="space-y-1.5">
                            <div class="flex justify-between text-[11px] font-semibold uppercase nd2-subtle">
                                <span>Pitch</span>
                                <span class="flex items-center gap-2">
                                    <span id="pitch-val" class="font-mono text-white">0°</span>
                                    <button type="button" data-sensor-axis-reset="1" onclick="resetSensorAxis('pitch')" disabled class="w-6 h-6 rounded-lg bg-white/5 border border-white/10 text-[10px] font-bold text-white hover:bg-white/10 disabled:opacity-40 disabled:pointer-events-none">R</button>
                                </span>
                            </div>
                            <input id="pitch-slider" type="range" min="-90" max="90" step="1" value="0" class="nd2-range disabled:opacity-50" disabled oninput="sendSensorData()">
                        </div>
                        <div class="space-y-1.5">
                            <div class="flex justify-between text-[11px] font-semibold uppercase nd2-subtle">
                                <span>Roll</span>
                                <span class="flex items-center gap-2">
                                    <span id="roll-val" class="font-mono text-white">0°</span>
                                    <button type="button" data-sensor-axis-reset="1" onclick="resetSensorAxis('roll')" disabled class="w-6 h-6 rounded-lg bg-white/5 border border-white/10 text-[10px] font-bold text-white hover:bg-white/10 disabled:opacity-40 disabled:pointer-events-none">R</button>
                                </span>
                            </div>
                            <input id="roll-slider" type="range" min="-180" max="180" step="1" value="0" class="nd2-range disabled:opacity-50" disabled oninput="sendSensorData()">
                        </div>
                        <button onclick="resetSensors()" id="btn-reset-sensors" disabled class="w-full mt-2 py-3 rounded-xl bg-white/5 border border-white/10 text-[12px] font-bold uppercase tracking-widest text-white hover:bg-white/10 transition disabled:opacity-50 disabled:pointer-events-none">Reset</button>
                    </div>
                </div>

                <div id="panel-system-cache" class="nd2-glass-panel overflow-hidden">
                    <button class="nd2-menu-row" data-accordion-toggle="#sys-content">
                        <div class="nd2-menu-label"><span class="nd2-icon-box"><img src="/asset/new-design/icons/icon-system-cache.svg" alt="" class="nd2-icon"></span><span class="nd2-heading tracking-[0.28em]">System & Cache</span></div>
                        <span id="sys-chevron" class="accordion-chevron">⌄</span>
                    </button>
                    <div id="sys-content" class="nd2-menu-body hidden space-y-3">
                        <button onclick="toggleScreen()" id="btn-screen-toggle" class="w-full flex justify-between items-center px-4 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-sim-accent"><span>Screen Toggle</span></button>
                        <button onclick="clearScene()" class="w-full flex justify-between items-center px-4 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-red-400">Clear Screen</button>
                        <button onclick="toggleRecording()" id="btn-recording-main" class="w-full flex justify-between items-center px-4 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition">
                            <span class="flex items-center gap-2"><span id="record-dot-main" class="w-2.5 h-2.5 rounded-full bg-zinc-600 transition-all duration-300"></span><span id="record-btn-text" class="text-zinc-300">Start Recording</span></span>
                        </button>
                        <button onclick="forceRender()" class="w-full flex justify-between items-center px-4 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-sim-accent">Render Cache</button>
                        <button onclick="openCacheFolder()" class="w-full flex justify-between items-center px-4 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-blue-300">Open Cache Folder</button>
                        <p id="power-msg" class="text-[10px] text-zinc-400 font-mono mt-1"></p>
                    </div>
                </div>

                <div id="panel-scene-objects" class="nd2-dev-panel nd2-glass-panel overflow-hidden">
                    <div class="px-6 py-5 border-b border-white/10">
                        <button data-accordion-toggle="#scene-objects-body" class="accordion-open w-full flex justify-between items-center">
                            <span class="nd2-heading">Scene Objects</span>
                            <span id="objtypes-badge" class="text-sim-accent font-mono">0 / 0</span>
                        </button>
                    </div>
                    <div id="draw-order-container" class="px-6 pt-4">
                        <div class="text-[11px] font-semibold uppercase tracking-wider nd2-subtle mb-3">Draw Order (Back to Front)</div>
                        <div id="draw-order-list" class="flex flex-wrap gap-2 min-h-8 p-1 overflow-x-auto"><div class="info-empty text-[11px] opacity-60">(none)</div></div>
                    </div>
                    <div class="px-6 py-4 flex items-center gap-3">
                        <input type="text" id="obj-search" placeholder="Search objects..." class="nd2-search flex-1 px-4 py-3 text-[13px] placeholder-zinc-600 focus:outline-none focus:border-sim-accent/50 font-mono" />
                        <button onclick="clearAllSelections()" title="Clear All selections" class="w-12 h-12 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 flex items-center justify-center">
                            <img src="/asset/new-design/icons/icon-filter.svg" alt="Clear" class="w-5 h-5">
                        </button>
                    </div>
                    <div id="scene-objects-body" class="p-5 border-t border-white/10 max-h-64 overflow-y-auto font-mono text-xs bg-black/10">
                        <div id="obj-stats" class="space-y-2 text-zinc-300"><div class="info-empty">(none)</div></div>
                    </div>
                </div>
            </section>

            <section id="col-center" class="min-w-0">
                <div class="nd2-lens-area">
                    <div class="nd2-lens-stage">
                        <div class="nd2-lens-label">
                            <span class="nd2-viewport-pill inline-flex items-center gap-2">
                                <span id="viewport-status-dot" class="nd2-status-dot"></span>
                                <span id="viewport-resolution">640x400</span>
                            </span>
                            <span id="viewport-status-badge" class="nd2-viewport-pill">DISCONNECTED</span>
                            <span id="viewport-fps-badge" class="nd2-viewport-pill">0 FPS</span>
                        </div>
                        <div class="nd2-lens-screen">
                            <div id="canvas-container" class="relative w-[640px] h-[400px] shrink-0 bg-black">
                                <div id="brightness-overlay" class="absolute inset-0 bg-black pointer-events-none opacity-0 transition-opacity duration-300 z-10"></div>
                                <canvas id="c" width="640" height="400" class="absolute inset-0 z-[5] w-full h-full block border-0 bg-transparent"></canvas>
                            </div>
                        </div>
                        <img src="/asset/new-design/glasses-back.png" alt="Right eye smart glasses lens" class="nd2-glasses-frame">
                    </div>
                </div>

                <div id="panel-asset-cache" class="nd2-dev-panel nd2-cache-panel nd2-glass-panel overflow-hidden">
                    <button data-accordion-toggle="#asset-cache-body" class="accordion-open w-full flex justify-between items-center px-8 py-5">
                        <span class="nd2-heading">Asset Cache <span id="cache-badge" class="ml-4 text-sim-accent font-mono tracking-normal">0</span></span>
                        <span class="text-white/80">⌃</span>
                    </button>
                    <div id="asset-cache-body" class="px-8 pb-5 border-t border-white/10 grid grid-cols-2 gap-8 bg-black/10">
                        <div class="pt-4">
                            <div class="text-[11px] uppercase nd2-subtle mb-2 tracking-[0.35em]">Images <span id="images-badge" class="ml-1 font-mono text-zinc-300"></span></div>
                            <div id="images-list" class="text-xs font-mono text-blue-300 space-y-1 max-h-28 overflow-y-auto"><div class="info-empty">(none)</div></div>
                        </div>
                        <div class="pt-4">
                            <div class="text-[11px] uppercase nd2-subtle mb-2 tracking-[0.35em]">Fonts <span id="fonts-badge" class="ml-1 font-mono text-zinc-300"></span></div>
                            <div id="fonts-list" class="text-xs font-mono text-white space-y-1 max-h-28 overflow-y-auto"><div class="info-empty">(none)</div></div>
                        </div>
                    </div>
                </div>

                <div class="nd2-bottom-bar">
                    <div class="nd2-glass-panel p-4 flex items-center gap-4">
                        <span class="nd2-heading whitespace-nowrap">Reported FW Version</span>
                        <input id="fw-version-input" type="number" min="34" max="255" class="w-24 bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-sim-accent" placeholder="34" />
                        <span class="text-[11px] uppercase tracking-[0.18em] nd2-subtle">Major only</span>
                        <button onclick="saveFwVersion()" class="px-6 py-3 rounded-xl bg-sim-accent text-black font-bold uppercase tracking-[0.12em]">Save</button>
                        <button onclick="resetFwVersion()" class="px-6 py-3 rounded-xl bg-white/10 text-white font-bold uppercase tracking-[0.12em]">Reset</button>
                        <p id="fw-version-msg" class="text-[10px] font-mono text-zinc-400 ml-auto"></p>
                    </div>
                </div>
            </section>

            <section id="col-right" class="nd2-rail">
                <div id="touch-panel" class="nd2-glass-panel nd2-card">
                    <div class="flex justify-between items-center mb-5">
                        <span class="nd2-heading">Touch Injection</span>
                    </div>
                    <div class="grid grid-cols-2 gap-3">
                        <button id="btn-tap" onclick="injectInput(1,5)" class="nd2-touch-button bg-white text-black py-4 rounded-xl font-bold text-[12px] uppercase tracking-widest hover:bg-sim-accent transition"><img src="/asset/new-design/icons/icon-touch-tap.svg" alt="" class="nd2-touch-action-icon">Tap</button>
                        <button id="btn-long-tap" onclick="injectInput(1,6)" class="nd2-touch-button bg-white text-black py-4 rounded-xl font-bold text-[12px] uppercase tracking-widest hover:bg-sim-accent transition"><img src="/asset/new-design/icons/icon-touch-long-tap.svg" alt="" class="nd2-touch-action-icon">Long Tap</button>
                        <button id="btn-back" onclick="injectInput(1,3)" class="nd2-touch-button bg-white text-black py-4 rounded-xl font-bold text-[12px] uppercase tracking-widest hover:bg-sim-accent transition"><img src="/asset/new-design/icons/icon-touch-back.svg" alt="" class="nd2-touch-action-icon">Back</button>
                        <button id="btn-forward" onclick="injectInput(1,4)" class="nd2-touch-button bg-white text-black py-4 rounded-xl font-bold text-[12px] uppercase tracking-widest hover:bg-sim-accent transition"><img src="/asset/new-design/icons/icon-touch-forward.svg" alt="" class="nd2-touch-action-icon">Forward</button>
                    </div>
                    <p class="text-[10px] text-zinc-400 mt-4 leading-relaxed tracking-wide">Shortcuts: Space / ↓ = Tap · ← = Back · → = Forward · L / ↑ = Long Tap</p>
                </div>

                <div id="camera-panel" class="nd2-glass-panel nd2-card-compact">
                    <div class="flex items-center justify-between mb-4">
                        <span class="nd2-heading tracking-[0.28em]">Camera Preview</span>
                        <span id="camera-status-label" class="text-[10px] text-sim-accent font-mono flex items-center gap-1">ACTIVE <span class="text-xs">●</span></span>
                    </div>
                    <div class="relative h-40 rounded-2xl border border-white/10 bg-black/70 overflow-hidden">
                        <img id="camera-preview" src="" class="w-full h-full object-cover opacity-60 hidden" alt="Camera Preview">
                        <div id="camera-no-signal" class="absolute inset-0 flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm z-10 transition-opacity duration-300">
                            <i data-lucide="video-off" class="w-8 h-8 text-zinc-600 mb-2"></i>
                            <span class="text-zinc-600 font-mono text-[10px] uppercase tracking-widest">No Signal</span>
                        </div>
                        <div class="absolute bottom-0 inset-x-0 h-16 bg-gradient-to-t from-black/80 to-transparent pointer-events-none"></div>
                    </div>
                </div>

                <div id="audio-panel" class="nd2-glass-panel nd2-card-compact">
                    <div class="flex justify-between items-center mb-4">
                        <span class="nd2-heading tracking-[0.28em]">Audio Analysis</span>
                        <span id="mic-level-label" class="text-[10px] font-mono text-sim-accent">-12.4 dB</span>
                    </div>
                    <div id="mic-eq" class="h-24 flex items-end gap-1 mb-4"></div>
                    <div class="space-y-2">
                        <div class="flex justify-between text-[10px] font-semibold uppercase tracking-[0.2em] nd2-subtle">
                            <span>Gain Override</span>
                            <span id="mic-gain-val" class="font-mono text-zinc-300">1.0</span>
                        </div>
                        <input id="mic-gain" type="range" min="0" max="6" step="0.1" value="1" class="nd2-range accent-sim-accent">
                    </div>
                </div>

                <div id="panel-uploads" class="nd2-dev-panel nd2-glass-panel nd2-card-compact">
                    <div class="flex justify-between items-center">
                        <span class="nd2-heading">Uploads</span>
                        <span id="uploads-badge" class="text-sim-accent font-mono">0</span>
                    </div>
                    <div id="uploads-list" class="mt-5 space-y-2 text-xs font-mono max-h-40 overflow-y-auto"><div class="info-empty">(none)</div></div>
                </div>

                <div id="nd2-right-menu" class="nd2-dev-panel nd2-glass-panel overflow-hidden">
                    <button class="nd2-menu-row" data-accordion-toggle="#nd2-assets-body">
                        <div class="nd2-menu-label"><span class="nd2-icon-box"><img src="/asset/new-design/icons/icon-assets.svg" alt="" class="nd2-icon"></span><span class="nd2-heading tracking-[0.28em]">Assets</span></div>
                        <span class="accordion-chevron">⌄</span>
                    </button>
                    <div id="nd2-assets-body" class="nd2-menu-body hidden">
                        <div>Images: <span id="nd2-assets-images-summary">0</span></div>
                        <div>Fonts: <span id="nd2-assets-fonts-summary">0</span></div>
                        <div class="mt-2 text-zinc-400">Full cache details stay in the Asset Cache panel below the lens.</div>
                    </div>
                    <button class="nd2-menu-row" data-accordion-toggle="#nd2-animators-body">
                        <div class="nd2-menu-label"><span class="nd2-icon-box"><img src="/asset/new-design/icons/icon-animators.svg" alt="" class="nd2-icon"></span><span class="nd2-heading tracking-[0.28em]">Animators</span></div>
                        <span class="accordion-chevron">⌄</span>
                    </button>
                    <div id="nd2-animators-body" class="nd2-menu-body hidden">
                        <div class="flex justify-between mb-2"><span>Active</span><span id="animators-badge" class="text-sim-accent">0</span></div>
                        <div id="animators-list" class="space-y-2 max-h-40 overflow-y-auto"><div class="info-empty">(none)</div></div>
                    </div>
                    <button class="nd2-menu-row" data-accordion-toggle="#nd2-params-body">
                        <div class="nd2-menu-label"><span class="nd2-icon-box"><img src="/asset/new-design/icons/icon-params.svg" alt="" class="nd2-icon"></span><span class="nd2-heading tracking-[0.28em]">Params</span></div>
                        <span class="accordion-chevron">⌄</span>
                    </button>
                    <div id="nd2-params-body" class="nd2-menu-body hidden space-y-3">
                        <label class="flex items-center justify-between gap-3"><span>Clipping</span><input type="checkbox" onchange="document.getElementById('clip-enabled').checked=this.checked; updateClipEnabled(event)"></label>
                        <label class="flex items-center justify-between gap-3"><span>Show Blacks</span><input type="checkbox" onchange="document.getElementById('show-blacks').checked=this.checked; updateShowBlacks(event)"></label>
                        <label class="flex items-center justify-between gap-3"><span>Transparent Blacks</span><input type="checkbox" onchange="document.getElementById('transparent-blacks').checked=this.checked; updateTransparentBlacks(event)"></label>
                        <label class="flex items-center justify-between gap-3"><span>Clip Transparent Blacks</span><input data-transparent-blacks-clip="1" type="checkbox" onchange="updateTransparentBlackClip(event)"></label>
                        <label class="flex items-center justify-between gap-3"><span>Debug Params</span><input type="checkbox" onchange="document.getElementById('dbg-params').checked=this.checked; updateDbgParams(event)"></label>
                    </div>
                    <button class="nd2-menu-row" data-accordion-toggle="#nd2-debug-body">
                        <div class="nd2-menu-label"><span class="nd2-icon-box"><img src="/asset/new-design/icons/icon-debug-logs.svg" alt="" class="nd2-icon"></span><span class="nd2-heading tracking-[0.28em]">Debug Logs</span></div>
                        <span class="accordion-chevron">⌄</span>
                    </button>
                    <div id="nd2-debug-body" class="nd2-menu-body hidden">
                        <label class="flex items-center justify-between gap-3">
                            <span>Show Logs</span>
                            <input id="logs-enabled" type="checkbox" onchange="updateLogsEnabled(event)">
                        </label>
                    </div>
                </div>

            </section>

            <section class="nd2-hidden-support">
                <div>
                    <input id="fail-ota" type="checkbox" onchange="toggleFailOta(event)">
                    <p id="fail-ota-msg"></p>
                </div>
            </section>
        </main>

        <div id="log-area" class="hidden flex-col nd2-glass-panel overflow-hidden mt-6">
            <div id="log-header" class="bg-white/5 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.35em]">Logs Stream</div>
            <div id="logs-console" class="bg-black/60 max-h-80 overflow-y-auto font-mono text-sm p-4"></div>
        </div>
    </div>

    <!-- Render option toggles moved into header -->

    <script>
        if (window.lucide) {
            window.lucide.createIcons();
        }
    </script>

    <script>
        (function() {
            if (!window.__SIM_UI_RELEASE__) return;
            document.body.classList.add('nd2-release-mode');
        })();
    </script>

    <script>
        let clipEnabled = false;
        let logsEnabled = true;
        let showBlacks = false;
        let transparentBlacks = false;
        let transparentBlackClip = false;
        let dbgParams = false;
        let fwDirty = false;
        const FIXED_BG_VIDEO_URL = "/asset/bg_video";
        const SITE_BG_MODES = new Set(["day", "night", "video", "mars"]);
        let bgVideoEnabled = false;
        let bgVideoEl = null;
        let previewHudOpacity = 1.0;
        let screenBgOpacity = 1.0;
        let showDisplayBoundaries = true;
        window.__lastSceneSnapshot = null;
        const selectedObjIds = new Set();
        window.__expandedObjType = null;
        window.__uiDirty = true;
        let lastUiUpdateTime = 0;
        const UI_UPDATE_INTERVAL = 300; // ms (approx 3 fps for UI is plenty)
        window.__lastPlaybackIdx = -1;
        window.__isRecording = false;
        window.__replayRowToRawIdx = null; // row_index -> raw log command index
        window.__replayRawToRowIdx = null; // raw log command index -> preferred row_index (container/original)

        function _replay_row_idx_from_raw(rawIdx) {
            const r = Number(rawIdx);
            if (!Number.isFinite(r)) return null;
            const map = window.__replayRawToRowIdx;
            if (map && map[r] !== undefined && map[r] !== null) {
                const v = Number(map[r]);
                if (Number.isFinite(v)) return v;
            }
            return r;
        }

        function _replay_raw_idx_from_row(rowIdx) {
            const i = Number(rowIdx);
            if (!Number.isFinite(i)) return null;
            const arr = window.__replayRowToRawIdx;
            if (arr && i >= 0 && i < arr.length) {
                const v = Number(arr[i]);
                if (Number.isFinite(v)) return v;
            }
            return i;
        }

        // Fast replay UI update that does NOT redraw the canvas.
        // (handleSnapshot() can be very heavy when there are many objects.)
        function __replayVirtUpdateCurrent(rawIdx, paused) {
            try {
                const virt = window.__replayVirt;
                if (!virt) return;
                const prev = virt.currentRawIdx;
                const changed = (Number.isFinite(prev) ? (rawIdx !== prev) : true);
                virt.currentRawIdx = rawIdx;

                // Auto-follow current step:
                // - When PLAYING: scroll only if the current step is out of view (throttled).
                // - When PAUSED: snap to the current step only when it changes (no fighting user scroll).
                if (changed) {
                    const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
                    const lastUser = Number(virt.lastUserScrollAt || 0);
                    const userIdle = (!Number.isFinite(lastUser)) || ((now - lastUser) > 250);
                    const rowIdx = _replay_row_idx_from_raw(rawIdx);
                    if (rowIdx !== null && userIdle) {
                        if (!paused) {
                            // Coalesce: at most 1 auto-follow scroll per animation frame, always to the latest step.
                            // This prevents getting "stuck in the middle" when steps advance faster than the throttle.
                            const okNow = (typeof virt.isRowComfortable === "function") ? virt.isRowComfortable(rowIdx) : false;
                            if (!okNow) {
                                virt._pendingFollowRowIdx = rowIdx;
                                if (!virt._followScheduled) {
                                    virt._followScheduled = true;
                                    requestAnimationFrame(() => {
                                        try {
                                            virt._followScheduled = false;
                                            const r = virt._pendingFollowRowIdx;
                                            virt._pendingFollowRowIdx = null;
                                            if (r === null || r === undefined) return;
                                            const ok = (typeof virt.isRowComfortable === "function") ? virt.isRowComfortable(r) : false;
                                            if (!ok) {
                                            virt.ensureVisible(r);
                                    virt.lastAutoFollowAt = now;
                                            }
                                        } catch (e) { }
                                    });
                                }
                            }
                        } else {
                            virt.ensureVisible(rowIdx);
                        }
                    }
                }

                // Always update visible row classes (bounded to visible range).
                virt.render(false);
            } catch (e) { }
        }

        function updateReplayUiOnly(idx, paused) {
            try {
                const rawIdx = Number(idx);
                if (!Number.isFinite(rawIdx)) return;
                const i = rawIdx;

                const currentEl = document.getElementById("replay-step-current");
                if (currentEl) currentEl.textContent = String(i + 1);

                window.__lastPlaybackIdx = rawIdx;
                __replayVirtUpdateCurrent(rawIdx, !!paused);

                const btnPlay = document.getElementById("btn-replay-play");
                const btnPause = document.getElementById("btn-replay-pause");
                if (btnPlay && btnPause) {
                    if (paused) {
                        btnPlay.classList.replace("bg-sim-accent", "bg-white/10");
                        btnPlay.classList.replace("text-black", "text-white");

                        btnPause.classList.replace("bg-white/10", "bg-sim-accent");
                        btnPause.classList.replace("text-white", "text-black");
                    } else {
                        btnPlay.classList.replace("bg-white/10", "bg-sim-accent");
                        btnPlay.classList.replace("text-white", "text-black");

                        btnPause.classList.replace("bg-sim-accent", "bg-white/10");
                        btnPause.classList.replace("text-black", "text-white");
                    }
                }
                // Ensure icons are always present (covers cases where init ran before replay DOM existed).
                try { __setReplayPlayIcon(); } catch (e) { }
                __setReplayPauseIcon(!!paused);
            } catch (e) { }
        }

        window.toggleObjType = function (t) {
            console.log(`Toggle Obj Type: ${t}`);
            window.__expandedObjType = (window.__expandedObjType === t) ? null : t;
            window.__uiDirty = true;
            if (window.__lastSceneSnapshot) handleSnapshot(window.__lastSceneSnapshot);
        };

        const OBJ_NAMES = {
            1: "CLIP",
            3: "MASK_ROUND",
            4: "TEXT",
            5: "RECT",
            6: "RECT_FILLED",
            7: "ELLIPSE",
            8: "ELLIPSE_FILLED",
            9: "ARC",
            10: "PATH",
            11: "POLY_TRI",
            12: "POLY_STRIP",
            13: "POLY_FAN",
            14: "POINT_3D",
            15: "LINE_3D",
            16: "MESH_3D",
            17: "LIGHT_3D",
        };

        function objTypeName(t) {
            return OBJ_NAMES[Number(t)] || `TYPE_${t}`;
        }

        function renderObjectParams(o) {
            try {
                if (!o) return "";
                const parts = [];

                // Position and Size
                if (o.x !== undefined || o.y !== undefined) {
                    parts.push(`<span class="text-zinc-500">pos</span>:(${Math.round(o.x)},${Math.round(o.y)})`);
                }
                if (o.w !== undefined || o.h !== undefined) {
                    parts.push(`<span class="text-zinc-500">sz</span>:(${Math.round(o.w)}x${Math.round(o.h)})`);
                }

                // Color and Alpha
                if (o.color !== undefined) {
                    const c = Number(o.color) >>> 0;
                    const r = (c >> 16) & 0xFF;
                    const g = (c >> 8) & 0xFF;
                    const b = c & 0xFF;
                    const a = (c >>> 24) & 0xFF;
                    const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
                    const alphaPct = Math.round((a / 255) * 100);
                    const circleColor = `rgba(${r},${g},${b},${a / 255})`;

                    parts.push(`
                        <div style="display:flex; align-items:center; gap:4px;">
                            <span class="text-zinc-500">clr</span>:
                            <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:${circleColor}; border:1px solid rgba(255,255,255,0.2);"></span>
                            <span class="select-all cursor-pointer font-semibold" title="Click to copy color">${hex}</span>
                            <span class="text-zinc-600">(${alphaPct}%)</span>
                        </div>`);
                }

                // Slots
                if (o.fontSlot !== undefined && o.fontSlot !== 127) {
                    parts.push(`<span class="text-zinc-500">font</span>:${o.fontSlot}`);
                }
                const ts = getTextureSlotFromObj(o);
                if (ts !== null && ts !== 127) {
                    parts.push(`<span class="text-zinc-500">tex</span>:${ts}`);
                }

                // Shape Specific
                if (o.rx || o.ry) parts.push(`<span class="text-zinc-500">r</span>:(${o.rx},${o.ry})`);
                if (o.a0 || o.a1) parts.push(`<span class="text-zinc-500">ang</span>:(${o.a0},${o.a1})`);
                if (o.textScale && o.textScale !== 1) parts.push(`<span class="text-zinc-500">scale</span>:${o.textScale.toFixed(2)}`);
                if (o.penSize && o.penSize !== 1) parts.push(`<span class="text-zinc-500">pen</span>:${o.penSize}`);
                if (Array.isArray(o.vec) && o.vec.length >= 3) {
                    parts.push(`<span class="text-zinc-500">vec</span>:(${Number(o.vec[0]).toFixed(2)},${Number(o.vec[1]).toFixed(2)},${Number(o.vec[2]).toFixed(2)})`);
                }
                if (Array.isArray(o.projectedVec) && o.projectedVec.length >= 2) {
                    parts.push(`<span class="text-zinc-500">proj</span>:(${Math.round(Number(o.projectedVec[0]))},${Math.round(Number(o.projectedVec[1]))})`);
                }
                if (Array.isArray(o.vertices3d)) {
                    parts.push(`<span class="text-zinc-500">v3d</span>:${Math.floor(o.vertices3d.length / 3)}`);
                }
                if (Array.isArray(o.indices3d)) {
                    parts.push(`<span class="text-zinc-500">i3d</span>:${o.indices3d.length}`);
                }
                if (Array.isArray(o.uvs3d)) {
                    parts.push(`<span class="text-zinc-500">uv</span>:${Math.floor(o.uvs3d.length / 2)}`);
                }

                if (parts.length === 0) return "";
                return `<div style="font-size:9px; font-family:monospace; line-height:1.4; display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; opacity:1.0; padding-left:24px;">${parts.join("")}</div>`;
            } catch (err) {
                console.warn("renderObjectParams error", err);
                return "";
            }
        }

        function getTextureSlotFromObj(o) {
            if (!o) return null;
            if (o.textureSlot !== undefined && o.textureSlot !== null) return Number(o.textureSlot);
            const u = o.textureUrl;
            if (typeof u === "string") {
                const m = u.match(/\/texture\/(\d+)\//);
                if (m) return Number(m[1]);
            }
            return null;
        }

        function objectsUsingTexture(slot, snap) {
            const out = [];
            if (!snap || !snap.order || !snap.objects) return out;
            for (const oid of snap.order) {
                const o = snap.objects[String(oid)];
                if (!o || o.type === 1) continue;
                const ts = getTextureSlotFromObj(o);
                if (ts !== null && ts === Number(slot)) out.push(Number(o.id));
            }
            return out;
        }

        function objectsUsingFont(slot, snap) {
            const out = [];
            if (!snap || !snap.order || !snap.objects) return out;
            for (const oid of snap.order) {
                const o = snap.objects[String(oid)];
                if (!o || o.type === 1) continue;
                // Some text objects might have o.text empty but we still want to find them by font.
                if (Number(o.fontSlot ?? -1) === Number(slot)) out.push(Number(o.id));
            }
            return out;
        }

        window.__expandedOffscreens = new Set();
        function toggleOffscreenList(target) {
            if (window.__expandedOffscreens.has(target)) {
                window.__expandedOffscreens.delete(target);
            } else {
                window.__expandedOffscreens.add(target);
            }
            // Update immediately if possible, or wait for next snapshot
            if (window.__lastSceneSnapshot) {
                const drawOrderList = document.getElementById("draw-order-list");
                if (drawOrderList) {
                    // We'll let the next sceneSnapshot update handle it to avoid duplicate logic
                }
            }
        }

        function openOffscreenPopup(target) {
            const t = Number(target);
            if (!Number.isFinite(t) || t < 0 || t > 3) return;
            const snap = window.__lastSceneSnapshot;
            if (!snap) return;
            if (!window.__offscreenCache || !window.__offscreenCache.canvases) return;
            const src = window.__offscreenCache.canvases.get(t);
            if (!src) return;

            let modal = document.getElementById("offscreen-modal");
            if (!modal) {
                modal = document.createElement("div");
                modal.id = "offscreen-modal";
                modal.style.position = "fixed";
                modal.style.inset = "0";
                modal.style.zIndex = "9999";
                modal.style.background = "rgba(0,0,0,0.65)";
                modal.style.display = "flex";
                modal.style.alignItems = "center";
                modal.style.justifyContent = "center";
                modal.innerHTML = `
                    <div style="width:min(960px, 92vw); max-height:92vh; overflow:auto; background:rgba(10,10,10,0.95); border:1px solid rgba(255,255,255,0.12); border-radius:16px; padding:16px;">
                        <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px;">
                            <div style="font-size:12px; font-weight:800; letter-spacing:0.25em; text-transform:uppercase; color:rgba(229,231,235,0.75);">
                                Offscreen <span id="offscreen-modal-title"></span>
                            </div>
                            <button id="offscreen-modal-close" style="padding:6px 10px; border-radius:10px; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); color:rgba(229,231,235,0.8); font-size:11px; font-weight:800; letter-spacing:0.2em; text-transform:uppercase; cursor:pointer;">
                                Close
                            </button>
                        </div>
                        <div style="display:flex; gap:14px; flex-wrap:wrap;">
                            <div style="flex: 0 0 auto;">
                                <canvas id="offscreen-modal-canvas" style="background:#000; border-radius:12px; border:1px solid rgba(255,255,255,0.1);"></canvas>
                            </div>
                            <div style="flex: 1 1 320px; min-width: 260px;">
                                <div style="font-size:10px; font-weight:800; letter-spacing:0.2em; text-transform:uppercase; color:rgba(229,231,235,0.55); margin-bottom:8px;">
                                    Draw list
                                </div>
                                <div id="offscreen-modal-list" style="display:flex; flex-wrap:wrap; gap:6px;"></div>
                            </div>
                        </div>
                    </div>
                `;
                document.body.appendChild(modal);
                modal.addEventListener("click", (e) => {
                    if (e.target === modal) modal.remove();
                });
                const closeBtn = modal.querySelector("#offscreen-modal-close");
                if (closeBtn) closeBtn.addEventListener("click", () => modal.remove());
            }

            const title = modal.querySelector("#offscreen-modal-title");
            if (title) title.textContent = String(t);

            const canv = modal.querySelector("#offscreen-modal-canvas");
            if (canv && canv.getContext) {
                const c2 = canv.getContext("2d");
                // scale up for visibility but keep crisp
                const scale = 2;
                canv.width = src.width * scale;
                canv.height = src.height * scale;
                canv.style.width = String(src.width * scale) + "px";
                canv.style.height = String(src.height * scale) + "px";
                c2.setTransform(scale, 0, 0, scale, 0, 0);
                c2.imageSmoothingEnabled = false;
                c2.clearRect(0, 0, src.width, src.height);
                c2.drawImage(src, 0, 0);
            }

            const list = modal.querySelector("#offscreen-modal-list");
            const order = (snap.offscreenOrders && snap.offscreenOrders[String(t)]) ? snap.offscreenOrders[String(t)] : [];
            if (list) {
                const pills = [];
                for (const oid of order) {
                    const id = Number(oid);
                    if (!Number.isFinite(id)) continue;
                    if (id === (0xFFF1 + t)) continue; // root
                    const o = snap.objects ? snap.objects[String(id)] : null;
                    if (!o) continue;
                    const tname = objTypeName(o.type);
                    const abbrev = tname ? tname[0].toUpperCase() : "?";
                    pills.push(`
                        <div class="draw-order-pill flex-none px-1.5 py-0.5 rounded text-[10px] bg-white/10 text-zinc-300 border border-white/5" title="${tname} ID ${id}">
                            <span class="opacity-60 mr-0.5">${abbrev}</span>${id}
                        </div>
                    `);
                }
                list.innerHTML = pills.length ? pills.join("") : `<div class="info-empty text-[9px] opacity-40">(none)</div>`;
            }
        }

        function setSelectedIds(ids, checked) {
            for (const id of ids) {
                const n = Number(id);
                if (!Number.isFinite(n)) continue;
                if (checked) selectedObjIds.add(n);
                else selectedObjIds.delete(n);
            }
        }

        async function postFocus(ids, focus) {
            try {
                await fetch("/focus", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ ids: (ids || []).map(x => Number(x)), focus: !!focus })
                });
            } catch (e) {
                // best-effort
            }
        }

        const logArea = document.getElementById("log-area");

        function toggleLogs() {
            if (!logArea) return;
            logArea.classList.toggle("hidden");
        }

        function setupAccordions() {
            document.querySelectorAll("[data-accordion-toggle]").forEach((btn) => {
                const selector = btn.getAttribute("data-accordion-toggle");
                const target = selector ? document.querySelector(selector) : null;
                btn.addEventListener("click", () => {
                    if (!target) return;
                    target.classList.toggle("hidden");
                    btn.classList.toggle("accordion-open", !target.classList.contains("hidden"));
                });
            });
        }
        setupAccordions();

        function toggleLensViewSettings(e, forceOpen) {
            if (e) e.stopPropagation();
            const panel = document.getElementById("lens-view-settings-popover");
            if (!panel) return;
            const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : panel.classList.contains("hidden");
            panel.classList.toggle("hidden", !shouldOpen);
        }

        // Close the Virtual Display Settings popover when clicking anywhere outside it.
        document.addEventListener("click", (e) => {
            const panel = document.getElementById("lens-view-settings-popover");
            if (!panel || panel.classList.contains("hidden")) return;
            if (panel.contains(e.target)) return;
            // Ignore clicks on the Settings button itself — toggleLensViewSettings handles that
            // and stops propagation, but guard in case stopPropagation is bypassed.
            if (e.target.closest && e.target.closest('[onclick*="toggleLensViewSettings"]')) return;
            panel.classList.add("hidden");
        });

        let lastFrameCount = 0;
        let lastFrameTime = Date.now();
        let fps = 0;

        async function clearScene() {
            try {
                await fetch('/clear', { method: 'POST' });
            } catch (e) {
                console.error("Clear failed", e);
            }
        }


        function updateClipEnabled(e) {
            if (e) e.stopPropagation();
            const cb = document.getElementById("clip-enabled");
            if (cb) {
                clipEnabled = cb.checked;
                localStorage.setItem("sim_clip_enabled", clipEnabled ? "1" : "0");
                window.__uiDirty = true;

                // Sync with server
                fetch("/clipping", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ enabled: clipEnabled })
                }).catch(e => console.warn("Failed to sync clipping", e));
            }
        }

        function updateLogsEnabled(e) {
            const cb = document.getElementById("logs-enabled");
            if (cb) {
                const enabled = cb.checked;
                const logArea = document.getElementById("log-area");
                if (logArea) {
                    logArea.classList.toggle("hidden", !enabled);
                    logArea.classList.toggle("flex", enabled);
                }
                localStorage.setItem("sim_show_logs", enabled ? "1" : "0");
                // Important: don't keep processing /view_logs when hidden (can starve the main thread).
                try {
                    if (enabled) startLogWs();
                    else stopLogWs();
                } catch (e) { }
            }
        }

        function updateShowBlacks(e) {
            const cb = document.getElementById("show-blacks");
            if (cb) {
                showBlacks = cb.checked;
                localStorage.setItem("sim_show_blacks", showBlacks ? "1" : "0");
                window.__uiDirty = true;
            }
        }

        function updateTransparentBlacks(e) {
            const cb = document.getElementById("transparent-blacks");
            if (cb) {
                transparentBlacks = cb.checked;
                localStorage.setItem("sim_transparent_blacks", transparentBlacks ? "1" : "0");
                getTransparentBlackClipCheckboxes().forEach((clipCb) => {
                    clipCb.disabled = !transparentBlacks;
                    clipCb.parentElement?.classList.toggle("opacity-50", !transparentBlacks);
                });
                window.__uiDirty = true;
            }
        }

        function getTransparentBlackClipCheckboxes() {
            return Array.from(document.querySelectorAll('input[data-transparent-blacks-clip="1"]'));
        }

        function updateTransparentBlackClip(e) {
            if (e) e.stopPropagation();
            const checkboxes = getTransparentBlackClipCheckboxes();
            const source = e && e.target ? e.target : document.getElementById("transparent-blacks-clip");
            const checked = source ? !!source.checked : false;
            transparentBlackClip = checked;
            checkboxes.forEach((cb) => {
                cb.checked = checked;
                cb.disabled = !transparentBlacks;
                cb.parentElement?.classList.toggle("opacity-50", !transparentBlacks);
            });
            localStorage.setItem("sim_transparent_black_clip", checked ? "1" : "0");
            window.__uiDirty = true;
        }

        function updateDbgParams(e) {
            const cb = document.getElementById("dbg-params");
            if (cb) {
                dbgParams = cb.checked;
                localStorage.setItem("sim_dbg_params", dbgParams ? "1" : "0");
                window.__uiDirty = true;

                // Visual update for the toggle
                const label = cb.parentElement.previousElementSibling;
                if (label) {
                    if (dbgParams) label.classList.add("text-sim-accent");
                    else label.classList.remove("text-sim-accent");
                }
            }
        }

        function getBgVideoToggles() {
            return Array.from(document.querySelectorAll('input[data-bg-video-toggle="1"]'));
        }

        function setBgVideoTogglesChecked(checked) {
            const toggles = getBgVideoToggles();
            toggles.forEach((cb) => { cb.checked = !!checked; });
        }

        function setSiteBgMode(mode, persist = true) {
            const nextMode = SITE_BG_MODES.has(mode) ? mode : "night";
            document.body.classList.toggle("nd2-bg-day", nextMode === "day");
            document.body.classList.toggle("nd2-bg-night", nextMode === "night");
            document.body.classList.toggle("nd2-bg-video", nextMode === "video");
            document.body.classList.toggle("nd2-bg-mars", nextMode === "mars");

            document.querySelectorAll("[data-site-bg-mode]").forEach((btn) => {
                const isActive = btn.dataset.siteBgMode === nextMode;
                btn.classList.toggle("is-active", isActive);
                btn.setAttribute("aria-checked", isActive ? "true" : "false");
            });

            const msg = document.getElementById("site-bg-msg");
            if (msg) {
                if (nextMode === "video") {
                    msg.textContent = "Full site video background";
                } else if (nextMode === "mars") {
                    msg.textContent = "Mars 360 background follows yaw, pitch, and roll";
                } else {
                    msg.textContent = `${nextMode.charAt(0).toUpperCase()}${nextMode.slice(1)} lens background`;
                }
            }

            const siteVideo = document.getElementById("site-bg-video");
            if (siteVideo) {
                if (nextMode === "video") {
                    if (!siteVideo.src || !siteVideo.src.endsWith(FIXED_BG_VIDEO_URL)) {
                        siteVideo.src = FIXED_BG_VIDEO_URL;
                    }
                    const playPromise = siteVideo.play();
                    if (playPromise && typeof playPromise.catch === "function") {
                        playPromise.catch(() => { });
                    }
                } else {
                    try {
                        siteVideo.pause();
                        siteVideo.removeAttribute("src");
                        siteVideo.load();
                    } catch (err) { }
                }
            }

            if (persist) {
                try { localStorage.setItem("sim_site_bg_mode", nextMode); } catch (err) { }
            }
            updateMars360BackgroundFromSensors();
        }

        function updateMars360Background(yaw, pitch, roll) {
            if (!document.body.classList.contains("nd2-bg-mars")) return;
            const yawDeg = Number.isFinite(yaw) ? yaw : 0;
            const pitchDeg = Number.isFinite(pitch) ? pitch : 0;
            const rollDeg = Number.isFinite(roll) ? roll : 0;
            const x = Math.max(0, Math.min(100, 50 + (yawDeg / 180) * 50));
            // The panorama has extra vertical pixels, but full +/-90 made pitch feel almost static.
            // Hit the sky/ground limits by about +/-50 degrees for a more natural LOS preview.
            const y = Math.max(0, Math.min(100, 50 - (pitchDeg / 50) * 50));
            document.documentElement.style.setProperty("--mars-bg-x", `${x.toFixed(2)}%`);
            document.documentElement.style.setProperty("--mars-bg-y", `${y.toFixed(2)}%`);
            document.documentElement.style.setProperty("--mars-bg-rot", `${(-rollDeg).toFixed(2)}deg`);
        }

        function updateMars360BackgroundFromSensors() {
            const yawEl = document.getElementById("yaw-slider");
            const pitchEl = document.getElementById("pitch-slider");
            const rollEl = document.getElementById("roll-slider");
            updateMars360Background(
                parseFloat(yawEl ? yawEl.value || "0" : "0"),
                parseFloat(pitchEl ? pitchEl.value || "0" : "0"),
                parseFloat(rollEl ? rollEl.value || "0" : "0")
            );
        }

        function updateDisplayBoundaries(e) {
            if (e) e.stopPropagation();
            const cb = document.getElementById("show-display-boundaries");
            showDisplayBoundaries = cb ? !!cb.checked : true;
            window.__uiDirty = true;
        }

        function pseudoRandomBrightRgb(id) {
            // deterministic per object id
            const x = (Math.imul((id | 0) ^ 0x9E3779B9, 0x85ebca6b) >>> 0);
            let r = (x & 0xFF);
            let g = ((x >>> 8) & 0xFF);
            let b = ((x >>> 16) & 0xFF);
            // force brighter
            r = (r | 0x80) & 0xFF;
            g = (g | 0x80) & 0xFF;
            b = (b | 0x80) & 0xFF;
            return { r, g, b };
        }

        async function loadFwVersion() {
            const input = document.getElementById("fw-version-input");
            const msg = document.getElementById("fw-version-msg");
            const failCb = document.getElementById("fail-ota");
            const failMsg = document.getElementById("fail-ota-msg");
            const bgMsg = document.getElementById("bg-video-msg");
            if (!input) return;
            try {
                const resp = await fetch("/sim_settings");
                const data = await resp.json();
                if (data && data.fwVersion) input.value = data.fwVersion;
                if (data && typeof data.failOta === "boolean" && failCb) failCb.checked = data.failOta;
                bgVideoEnabled = false;
                setBgVideoTogglesChecked(false);
                applyBgVideoSettings();
                if (msg) msg.textContent = "";
                if (failMsg) failMsg.textContent = "";
                if (bgMsg) bgMsg.textContent = "";
                fwDirty = false;
            } catch (e) {
                if (msg) msg.textContent = "Failed to load settings";
            }
        }

        async function saveFwVersion() {
            const input = document.getElementById("fw-version-input");
            const msg = document.getElementById("fw-version-msg");
            if (!input) return;
            const value = String(input.value || "").trim();
            if (!value) {
                if (msg) msg.textContent = "Enter a major version (34..255)";
                return;
            }
            try {
                const resp = await fetch("/sim_settings", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ fwVersion: value }),
                });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    input.value = data.fwVersion || value;
                    fwDirty = false;
                    if (msg) msg.textContent = `Saved: ${input.value}`;
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Save failed";
                }
            } catch (e) {
                if (msg) msg.textContent = "Save failed";
            }
        }

        async function resetFwVersion() {
            const input = document.getElementById("fw-version-input");
            const msg = document.getElementById("fw-version-msg");
            if (!input) return;
            input.value = "34";
            fwDirty = false;
            await saveFwVersion();
            if (msg) msg.textContent = "Reset to default: 34";
        }

        async function toggleFailOta(e) {
            const cb = document.getElementById("fail-ota");
            const msg = document.getElementById("fail-ota-msg");
            if (!cb) return;
            try {
                const resp = await fetch("/sim_settings", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ failOta: !!cb.checked }),
                });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    if (typeof data.failOta === "boolean") cb.checked = data.failOta;
                    if (msg) msg.textContent = cb.checked ? "Will fail OTA at ~30%" : "OTA failure disabled";
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Save failed";
                }
            } catch (err) {
                if (msg) msg.textContent = "Save failed";
            }
        }

        function ensureBgVideoElement() {
            if (bgVideoEl) return bgVideoEl;
            const v = document.createElement("video");
            v.id = "bg-video";
            v.muted = true;
            v.loop = true;
            v.playsInline = true;
            v.autoplay = true;
            v.preload = "auto";
            v.crossOrigin = "anonymous";
            bgVideoEl = v;
            return v;
        }

        function mountBgVideoUnderCanvas() {
            const v = ensureBgVideoElement();
            const box = document.getElementById("canvas-container");
            const c = document.getElementById("c");
            if (!box || !c) return;
            if (v.parentNode !== box) box.insertBefore(v, c);
            v.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;object-fit:cover;border-radius:28px;z-index:0;pointer-events:none;";
        }

        function applyBgVideoSettings() {
            const msg = document.getElementById("bg-video-msg");
            if (!bgVideoEnabled) {
                if (bgVideoEl) {
                    try {
                        bgVideoEl.pause();
                        bgVideoEl.removeAttribute("src");
                        bgVideoEl.load();
                        if (bgVideoEl.parentNode) bgVideoEl.parentNode.removeChild(bgVideoEl);
                    } catch (err) { }
                }
                if (msg) msg.textContent = "BG video disabled";
                return;
            }

            const v = ensureBgVideoElement();
            mountBgVideoUnderCanvas();
            if (!v.src || !v.src.endsWith(FIXED_BG_VIDEO_URL)) {
                v.src = FIXED_BG_VIDEO_URL;
            }
            const playPromise = v.play();
            if (playPromise && typeof playPromise.catch === "function") {
                playPromise.catch(() => { });
            }
            if (msg) msg.textContent = "BG video enabled (fixed SDK clip)";
        }

        async function saveBgVideoSettings() {
            const msg = document.getElementById("bg-video-msg");
            const toggles = getBgVideoToggles();
            if (!toggles.length) return;
            const enabled = !!toggles[0].checked;
            try {
                const resp = await fetch("/sim_settings", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ bgVideoEnabled: enabled }),
                });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    bgVideoEnabled = !!data.bgVideoEnabled;
                    setBgVideoTogglesChecked(bgVideoEnabled);
                    applyBgVideoSettings();
                    if (msg) msg.textContent = "Saved";
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Save failed";
                }
            } catch (err) {
                if (msg) msg.textContent = "Save failed";
            }
        }

        async function toggleBgVideo(e) {
            await saveBgVideoSettings();
        }

        async function toggleScreen() {
            const msg = document.getElementById("power-msg");
            try {
                const resp = await fetch("/screen_toggle", { method: "POST" });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    if (msg) msg.textContent = data.display_on ? "Screen: ON" : "Screen: OFF";
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Screen toggle failed";
                }
            } catch (e) {
                if (msg) msg.textContent = "Screen toggle failed";
            }
        }

        async function powerOff() {
            const msg = document.getElementById("power-msg");
            try {
                const resp = await fetch("/power_off", { method: "POST" });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    if (msg) msg.textContent = "Power: OFF (client disconnected)";
                    setPowerUi(false);
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Power off failed";
                }
            } catch (e) {
                if (msg) msg.textContent = "Power off failed";
            }
        }

        async function powerOn() {
            const msg = document.getElementById("power-msg");
            try {
                const resp = await fetch("/power_on", { method: "POST" });
                const data = await resp.json();
                if (data && data.status === "ok") {
                    if (msg) msg.textContent = "Power: ON (ready for reconnect)";
                    setPowerUi(true);
                } else {
                    if (msg) msg.textContent = (data && data.error) ? data.error : "Power on failed";
                }
            } catch (e) {
                if (msg) msg.textContent = "Power on failed";
            }
        }

        async function powerToggle() {
            try {
                const resp = await fetch("/sim_state");
                const s = await resp.json();
                const isOn = !!(s && s.poweredOn);
                if (isOn) return powerOff();
                return powerOn();
            } catch (e) {
                // fallback to last known
                const isOn = !!(window.__sim_powered_on ?? true);
                if (isOn) return powerOff();
                return powerOn();
            }
        }

        function setPowerUi(isOn) {
            const btnOn = document.getElementById("btn-power-on");
            const btnOff = document.getElementById("btn-standby");
            if (!btnOn || !btnOff) return;
            // ON highlighted
            btnOn.classList.toggle("bg-sim-accent", !!isOn);
            btnOn.classList.toggle("text-black", !!isOn);
            btnOn.classList.toggle("bg-transparent", !isOn);
            btnOn.classList.toggle("text-zinc-500", !isOn);
            btnOn.classList.toggle("hover:text-white", !isOn);
            btnOn.classList.toggle("transition", !isOn);
            // OFF highlighted
            btnOff.classList.toggle("bg-sim-accent", !isOn);
            btnOff.classList.toggle("text-black", !isOn);
            btnOff.classList.toggle("bg-transparent", !!isOn);
            btnOff.classList.toggle("text-zinc-500", !!isOn);
            btnOff.classList.toggle("hover:text-white", !!isOn);
            btnOff.classList.toggle("transition", !!isOn);
        }


        const canvas = document.getElementById("c");
        const ctx = canvas.getContext("2d");
        window.__mainCtx = ctx;
        const statusBadge = document.getElementById("status-badge");
        const fpsBadge = document.getElementById("viewport-fps-badge");
        const viewportStatusBadge = document.getElementById("viewport-status-badge");
        const brightnessOverlay = document.getElementById("brightness-overlay");
        const statFrames = document.getElementById("stat-frames");
        const objCount = document.getElementById("obj-count");
        const animCount = document.getElementById("anim-count");
        const objStats = document.getElementById("obj-stats");
        const drawOrderList = document.getElementById("draw-order-list");
        const imagesList = document.getElementById("images-list");
        const fontsList = document.getElementById("fonts-list");
        const animatorsList = document.getElementById("animators-list");
        const logsDiv = document.getElementById("logs");
        const brightnessPercent = document.getElementById("brightness-percent");
        const brightnessBar = document.getElementById("brightness-bar");
        const objTypesBadge = document.getElementById("objtypes-badge");
        const animatorsBadge = document.getElementById("animators-badge");
        const cacheBadge = document.getElementById("cache-badge");
        const imagesBadge = document.getElementById("images-badge");
        const fontsBadge = document.getElementById("fonts-badge");
        const nd2AssetsImagesSummary = document.getElementById("nd2-assets-images-summary");
        const nd2AssetsFontsSummary = document.getElementById("nd2-assets-fonts-summary");
        const uploadsBadge = document.getElementById("uploads-badge");
        const uploadsList = document.getElementById("uploads-list");
        const cameraPreview = document.getElementById("camera-preview");
        const micEq = document.getElementById("mic-eq");
        const micGain = document.getElementById("mic-gain");
        const bgHudOpacity = document.getElementById("bg-hud-opacity");
        const bgHudOpacityVal = document.getElementById("bg-hud-opacity-val");
        const screenBgOpacitySlider = document.getElementById("screen-bg-opacity");
        const screenBgOpacityVal = document.getElementById("screen-bg-opacity-val");
        const ambientLux = document.getElementById("ambient-lux");
        const ambientLuxVal = document.getElementById("ambient-lux-val");
        const autoBrightnessMode = document.getElementById("auto-brightness-mode");
        const batterySlider = document.getElementById("battery-slider");
        const batteryCharging = document.getElementById("battery-charging");
        const uploadHistory = new Map();
        const MAX_UPLOAD_HISTORY = 12;
        let uploadsDomSig = "";
        let objStatsDomSig = "";
        let drawOrderDomSig = "";
        let imagesDomSig = "";
        let fontsDomSig = "";
        const micGainVal = document.getElementById("mic-gain-val");
        const netIp = null;
        const netLatency = null;
        const netDrops = null;
        const micLevelLabel = document.getElementById("mic-level-label");
        const batteryPercent = document.getElementById("battery-percent");
        const imgCache = new Map();
        const videoLastReadyBySlot = new Map(); // slotKey -> Image
        const videoUiLogLastByKey = new Map(); // imgKey -> ms

        function videoUiLog(imgKey, msg) {
            try {
                const now = performance.now();
                const last = Number(videoUiLogLastByKey.get(imgKey) || 0);
                if ((now - last) < 300) return;
                videoUiLogLastByKey.set(imgKey, now);
                // console.debug(`vid_mpeg1_strm_ui ${imgKey} ${msg}`);
            } catch (e) { }
        }

        function getTextureImgFromCache(o) {
            if (!o || !o.textureUrl) return null;
            const isVid = !!o.isVideoTexture;
            const slotKey = Number(o.textureSlot);
            const imgKey = String(o.textureUrl);

            let img = imgCache.get(imgKey);
            if (!img) {
                img = new Image();
                img.__url = String(o.textureUrl);
                img.src = o.textureUrl;
                imgCache.set(imgKey, img);
                if (isVid) videoUiLog(imgKey, `init url=${img.__url}`);
            } else if (!isVid && img.src !== o.textureUrl) {
                img.src = o.textureUrl;
            }

            if (isVid && Number.isFinite(slotKey) && slotKey >= 0) {
                const slotMapKey = `video:${slotKey}`;
                if (img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) {
                    videoLastReadyBySlot.set(slotMapKey, img);
                    videoUiLog(slotMapKey, `ready url=${img.__url || img.src} w=${img.naturalWidth} h=${img.naturalHeight}`);
                    return img;
                }
                const prev = videoLastReadyBySlot.get(slotMapKey);
                if (prev && prev.complete && prev.naturalWidth > 0 && prev.naturalHeight > 0) {
                    videoUiLog(slotMapKey, `reuse_last_ready url=${prev.__url || prev.src}`);
                    return prev;
                }
                videoUiLog(slotMapKey, `not_ready current=${img.__url || img.src}`);
                return null;
            }
            return (img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) ? img : null;
        }

        if (batterySlider) {
            batterySlider.addEventListener("input", (e) => {
                const val = parseInt(e.target.value, 10);
                if (batteryPercent) batteryPercent.textContent = `${val}%`;
                fetch("/battery", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ level: val })
                }).catch(e => console.warn("Failed to sync battery", e));
            });
        }

        if (batteryCharging) {
            batteryCharging.addEventListener("change", (e) => {
                const charging = !!e.target.checked;
                fetch("/battery", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ charging: charging })
                }).catch(e => console.warn("Failed to sync charging", e));
            });
        }
        if (ambientLux) {
            ambientLux.addEventListener("input", () => {
                ambientLux.dataset.dragging = "1";
                const v = Number(ambientLux.value || "150");
                if (ambientLuxVal) ambientLuxVal.textContent = `${Math.round(v)} lx`;
            });
            ambientLux.addEventListener("change", async () => {
                const v = Number(ambientLux.value || "150");
                try {
                    localStorage.setItem("sim_ambient_lux", String(v));
                    await fetch("/ambient", {
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify({ ambientLux: v }),
                    });
                } catch (e) { }
                ambientLux.dataset.dragging = "";
            });
        }
        if (bgHudOpacity) {
            bgHudOpacity.addEventListener("input", () => {
                const pct = Math.max(0, Math.min(100, Number(bgHudOpacity.value || "100")));
                previewHudOpacity = pct / 100.0;
                if (bgHudOpacityVal) bgHudOpacityVal.textContent = `${Math.round(pct)}%`;
                try { localStorage.setItem("sim_preview_hud_opacity", String(Math.round(pct))); } catch (e) { }
                window.__uiDirty = true;
            });
        }

        function applyScreenBgOpacity() {
            const box = document.getElementById("canvas-container");
            if (box) box.style.backgroundColor = `rgba(0, 0, 0, ${screenBgOpacity.toFixed(2)})`;
            const lensScreen = document.querySelector(".nd2-lens-screen");
            if (lensScreen) {
                lensScreen.style.boxShadow = screenBgOpacity <= 0.001
                    ? "none"
                    : "inset 0 0 30px rgba(0, 0, 0, 0.72), 0 0 28px rgba(0, 0, 0, 0.36)";
            }
            window.__uiDirty = true;
        }

        if (screenBgOpacitySlider) {
            screenBgOpacitySlider.addEventListener("input", () => {
                const pct = Math.max(0, Math.min(100, Number(screenBgOpacitySlider.value || "100")));
                screenBgOpacity = pct / 100.0;
                if (screenBgOpacityVal) screenBgOpacityVal.textContent = `${Math.round(pct)}%`;
                try { localStorage.setItem("sim_screen_bg_opacity", String(Math.round(pct))); } catch (e) { }
                applyScreenBgOpacity();
            });
        }
        const textObjCache = new Map(); // id -> { img, offsetX, offsetY } for preventing flash
        let lastCheckedObjId = null;

        // Limit concurrent /text fetches (server-side rasterization) to avoid stampeding the server during replay bursts.
        const __textFetchQueue = [];
        let __textFetchInFlight = 0;
        const __textFetchMax = 1000;

        function __pumpTextFetchQueue() {
            while (__textFetchInFlight < __textFetchMax && __textFetchQueue.length > 0) {
                const fn = __textFetchQueue.shift();
                if (typeof fn !== "function") continue;
                __textFetchInFlight++;
                Promise.resolve()
                    .then(fn)
                    .catch(() => { })
                    .finally(() => {
                        __textFetchInFlight = Math.max(0, __textFetchInFlight - 1);
                        setTimeout(__pumpTextFetchQueue, 0);
                    });
            }
        }

        // UI interactions (event delegation; content is re-rendered every frame)
        try {
            if (objStats) {
                // Toggling is now handled by inline onclick for better reliability
                objStats.addEventListener("click", (e) => {
                    const header = e.target.closest(".info-item-header");
                    if (header) {
                        const row = header.closest("[data-obj-type]");
                        if (row) {
                            const t = row.getAttribute("data-obj-type");
                            window.toggleObjType(t);
                            return;
                        }
                    }

                    const chk = e.target.closest("input[data-obj-id]");
                    if (chk) {
                        const id = Number(chk.getAttribute("data-obj-id"));
                        if (Number.isFinite(id)) {
                            if (chk.checked) {
                                selectedObjIds.add(id);
                                lastCheckedObjId = id;
                                // Auto-scroll to the checked item
                                setTimeout(() => {
                                    chk.scrollIntoView({ behavior: 'smooth', block: 'center' });
                                    // Also allow draw order list to re-scroll
                                    window.__lastScrolledOid = null;
                                }, 50);
                            } else {
                                selectedObjIds.delete(id);
                                if (lastCheckedObjId === id) lastCheckedObjId = null;
                            }
                            postFocus([id], !!chk.checked);
                            window.__uiDirty = true;
                        }
                        return;
                    }
                });
            }
            if (imagesList) {
                imagesList.addEventListener("change", (e) => {
                    const el = e.target;
                    if (!el || !el.matches || !el.matches("input[data-focus-tex]")) return;
                    const slot = Number(el.getAttribute("data-focus-tex"));
                    const ids = objectsUsingTexture(slot, window.__lastSceneSnapshot);
                    console.log(`Texture Focus: slot ${slot}, found ${ids.length} objects`);
                    setSelectedIds(ids, el.checked);
                    postFocus(ids, !!el.checked);
                    window.__uiDirty = true;
                });
            }
            if (fontsList) {
                fontsList.addEventListener("change", (e) => {
                    const el = e.target;
                    if (!el || !el.matches || !el.matches("input[data-focus-font]")) return;
                    const slot = Number(el.getAttribute("data-focus-font"));
                    const ids = objectsUsingFont(slot, window.__lastSceneSnapshot);
                    console.log(`Font Focus: slot ${slot}, found ${ids.length} objects`);
                    setSelectedIds(ids, el.checked);
                    postFocus(ids, !!el.checked);
                    window.__uiDirty = true;
                });
            }
            // Cache preview click (cached assets)
            if (imagesList) {
                imagesList.addEventListener("click", (e) => {
                    const el = e.target && e.target.closest ? e.target.closest("[data-preview-kind]") : null;
                    if (!el) return;
                    const kind = el.getAttribute("data-preview-kind");
                    const slot = Number(el.getAttribute("data-preview-slot"));
                    const sz = Number(el.getAttribute("data-preview-size") || 0);
                    if (!kind || !Number.isFinite(slot)) return;
                    openCachePreview(kind, slot, sz);
                });
            }
            if (fontsList) {
                fontsList.addEventListener("click", (e) => {
                    const el = e.target && e.target.closest ? e.target.closest("[data-preview-kind]") : null;
                    if (!el) return;
                    const kind = el.getAttribute("data-preview-kind");
                    const slot = Number(el.getAttribute("data-preview-slot"));
                    const sz = Number(el.getAttribute("data-preview-size") || 0);
                    if (!kind || !Number.isFinite(slot)) return;
                    openCachePreview(kind, slot, sz);
                });
            }
            if (uploadsList) {
                uploadsList.addEventListener("change", (e) => {
                    const el = e.target;
                    if (!el || !el.matches || !el.matches("input[data-focus-upload]")) return;
                    const typ = Number(el.getAttribute("data-upload-type"));
                    const slot = Number(el.getAttribute("data-upload-slot"));
                    let ids = [];
                    if (typ === 3) ids = objectsUsingTexture(slot, window.__lastSceneSnapshot);
                    else if (typ === 4) ids = objectsUsingFont(slot, window.__lastSceneSnapshot);
                    // Select logic
                    if (el.checked) {
                        // Add ids
                        for (const id of ids) {
                            selectedObjIds.add(id);
                            lastCheckedObjId = id; // Set last checked to last in this group
                        }
                    } else {
                        // Remove ids
                        for (const id of ids) selectedObjIds.delete(id);
                    }
                    postFocus(ids, !!el.checked);
                    window.__uiDirty = true;
                });
            }
        } catch (e) { }

        // Init replay button icons (safe even if replay panel is hidden)
        try {
            __setReplayPlayIcon();
            __setReplayPauseIcon(!!(window.__lastSceneSnapshot && window.__lastSceneSnapshot.playbackPaused));
        } catch (e) { }

        // Network Stream card removed

        function fmtBytes(n) {
            const v = Number(n || 0);
            if (v < 1024) return `${v}B`;
            if (v < 1024 * 1024) return `${(v / 1024).toFixed(1)}KB`;
            return `${(v / (1024 * 1024)).toFixed(2)}MB`;
        }

        function argbToRgba(argb) {
            const a = ((argb >>> 24) & 0xFF) / 255.0;
            const r = (argb >>> 16) & 0xFF;
            const g = (argb >>> 8) & 0xFF;
            const b = (argb >>> 0) & 0xFF;
            return { r, g, b, a, css: `rgba(${r},${g},${b},${a.toFixed(3)})` };
        }

        function applyMat3(m, x, y) {
            const a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5];
            const g = m[6], h = m[7], i = m[8];
            const w = (g * x + h * y + i);
            // If w is too small (near zero) or invalid, fall back to affine
            if (!Number.isFinite(w) || Math.abs(w) < 1e-9) {
                const ax = a * x + b * y + c;
                const ay = d * x + e * y + f;
                return { x: Number.isFinite(ax) ? ax : 0, y: Number.isFinite(ay) ? ay : 0 };
            }
            const wx = (a * x + b * y + c) / w;
            const wy = (d * x + e * y + f) / w;
            // If result is invalid, fall back to affine
            if (!Number.isFinite(wx) || !Number.isFinite(wy)) {
                const ax = a * x + b * y + c;
                const ay = d * x + e * y + f;
                return { x: Number.isFinite(ax) ? ax : 0, y: Number.isFinite(ay) ? ay : 0 };
            }
            return { x: wx, y: wy };
        }

        function _affineFromTri(sx0, sy0, sx1, sy1, sx2, sy2, dx0, dy0, dx1, dy1, dx2, dy2) {
            const denom = (sx0 * (sy1 - sy2) + sx1 * (sy2 - sy0) + sx2 * (sy0 - sy1));
            if (Math.abs(denom) < 1e-9) return null;

            const a = (dx0 * (sy1 - sy2) + dx1 * (sy2 - sy0) + dx2 * (sy0 - sy1)) / denom;
            const c = (dx0 * (sx2 - sx1) + dx1 * (sx0 - sx2) + dx2 * (sx1 - sx0)) / denom;
            const e = (dx0 * (sx1 * sy2 - sx2 * sy1) + dx1 * (sx2 * sy0 - sx0 * sy2) + dx2 * (sx0 * sy1 - sx1 * sy0)) / denom;

            const b = (dy0 * (sy1 - sy2) + dy1 * (sy2 - sy0) + dy2 * (sy0 - sy1)) / denom;
            const d = (dy0 * (sx2 - sx1) + dy1 * (sx0 - sx2) + dy2 * (sx1 - sx0)) / denom;
            const f = (dy0 * (sx1 * sy2 - sx2 * sy1) + dy1 * (sx2 * sy0 - sx0 * sy2) + dy2 * (sx0 * sy1 - sx1 * sy0)) / denom;

            return { a, b, c, d, e, f };
        }

        function _drawImageTri(ctx, img, sx0, sy0, sx1, sy1, sx2, sy2, dx0, dy0, dx1, dy1, dx2, dy2) {
            const m = _affineFromTri(sx0, sy0, sx1, sy1, sx2, sy2, dx0, dy0, dx1, dy1, dx2, dy2);
            if (!m) return;
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(dx0, dy0);
            ctx.lineTo(dx1, dy1);
            ctx.lineTo(dx2, dy2);
            ctx.closePath();
            ctx.clip();
            // IMPORTANT:
            // Use `transform()` (not `setTransform()`) so we preserve the caller's current transform
            // (viewport translate + active CLIP region coordinate space). The affine matrix `m`
            // maps source->dest in the caller's current user space.
            ctx.transform(m.a, m.b, m.c, m.d, m.e, m.f);
            ctx.drawImage(img, 0, 0);
            ctx.restore();
        }

        // Projective (homography) textured-rect rendering for Canvas2D:
        // Approximate by subdividing into a grid and drawing each cell as 2 affine triangles.
        // This matches the glasses semantics (row-major mat3 with perspective terms m[6], m[7]).
        const __projTexCanvasCache = new Map(); // key -> { canvas, w, h, rr, spriteKey, url }

        function _roundRectPath(ctx, x, y, w, h, r) {
            const rr = Math.max(0, Math.min(r || 0, Math.min(w, h) * 0.5));
            if (rr <= 0) { ctx.rect(x, y, w, h); return; }
            if (typeof ctx.roundRect === "function") { ctx.roundRect(x, y, w, h, rr); return; }
            // Fallback: manual rounded rect
            ctx.moveTo(x + rr, y);
            ctx.lineTo(x + w - rr, y);
            ctx.arcTo(x + w, y, x + w, y + rr, rr);
            ctx.lineTo(x + w, y + h - rr);
            ctx.arcTo(x + w, y + h, x + w - rr, y + h, rr);
            ctx.lineTo(x + rr, y + h);
            ctx.arcTo(x, y + h, x, y + h - rr, rr);
            ctx.lineTo(x, y + rr);
            ctx.arcTo(x, y, x + rr, y, rr);
        }

        function _getProjectiveTexCanvas(o, src, rr) {
            // Build a local (untransformed) raster of the texture at the object's logical size.
            // This makes the projective warp math simple: source coords are in [0..lw]x[0..lh].
            const lw = Math.max(1, Math.round(Number(o.w || 0)));
            const lh = Math.max(1, Math.round(Number(o.h || 0)));
            const spriteKey = (o.sprite && o.sprite.length === 4) ? o.sprite.join(",") : "";
            const urlKey = (o.textureUrl ? String(o.textureUrl) : "");

            // Offscreen USER_TEXTURE canvases can change every frame; don't cache them.
            const canCache = (src && src.kind === "img" && urlKey);
            const key = canCache ? `${urlKey}|${lw}x${lh}|rr:${Math.round(rr || 0)}|spr:${spriteKey}` : null;
            if (canCache && __projTexCanvasCache.has(key)) {
                const ent = __projTexCanvasCache.get(key);
                if (ent && ent.canvas && ent.canvas.width === lw && ent.canvas.height === lh) return ent.canvas;
            }

            const c = document.createElement("canvas");
            c.width = lw;
            c.height = lh;
            const cctx = c.getContext("2d");
            if (!cctx) return c;
            cctx.imageSmoothingEnabled = true;

            if (rr > 0) {
                cctx.save();
                cctx.beginPath();
                _roundRectPath(cctx, 0, 0, lw, lh, rr);
                cctx.closePath();
                cctx.clip();
            }

            try {
                if (src.kind === "img") {
                    if (o.sprite && o.sprite.length === 4) {
                        const [sx, sy, sw, sh] = o.sprite;
                        if (sw > 0 && sh > 0) cctx.drawImage(src.img, sx, sy, sw, sh, 0, 0, lw, lh);
                    } else {
                        cctx.drawImage(src.img, 0, 0, lw, lh);
                    }
                } else if (src.kind === "canvas") {
                    if (o.sprite && o.sprite.length === 4) {
                        const [sx, sy, sw, sh] = o.sprite;
                        if (sw > 0 && sh > 0) cctx.drawImage(src.canvas, sx, sy, sw, sh, 0, 0, lw, lh);
                    } else {
                        cctx.drawImage(src.canvas, 0, 0, lw, lh);
                    }
                }
            } catch (e) { }

            if (rr > 0) cctx.restore();
            if (canCache) __projTexCanvasCache.set(key, { canvas: c });
            return c;
        }

        function _drawTexturedRectProjective(ctx, o, src, rr) {
            const m = o.mat3;
            if (!m || m.length !== 9) return false;
            const tex = _getProjectiveTexCanvas(o, src, rr);
            const lw = tex.width, lh = tex.height;
            if (!lw || !lh) return false;

            const _prevSmooth = ctx.imageSmoothingEnabled;
            ctx.imageSmoothingEnabled = true;

            // Map local canvas pixels -> object logical coords (mat3 operates in logical units).
            const sx = (Number(o.w || 0) / lw) || 1.0;
            const sy = (Number(o.h || 0) / lh) || 1.0;

            // Grid resolution: keep moderate for perf; scale with size.
            const _clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
            const nx = _clamp(Math.round((Number(o.w || 0)) / 40), 6, 24);
            const ny = _clamp(Math.round((Number(o.h || 0)) / 30), 6, 24);

            for (let iy = 0; iy < ny; iy++) {
                const y0 = (iy / ny) * lh;
                const y1 = ((iy + 1) / ny) * lh;
                const ly0 = y0 * sy;
                const ly1 = y1 * sy;
                for (let ix = 0; ix < nx; ix++) {
                    const x0 = (ix / nx) * lw;
                    const x1 = ((ix + 1) / nx) * lw;
                    const lx0 = x0 * sx;
                    const lx1 = x1 * sx;

                    const p00 = applyMat3(m, lx0, ly0);
                    const p10 = applyMat3(m, lx1, ly0);
                    const p11 = applyMat3(m, lx1, ly1);
                    const p01 = applyMat3(m, lx0, ly1);

                    const dx00 = o.x + p00.x, dy00 = o.y + p00.y;
                    const dx10 = o.x + p10.x, dy10 = o.y + p10.y;
                    const dx11 = o.x + p11.x, dy11 = o.y + p11.y;
                    const dx01 = o.x + p01.x, dy01 = o.y + p01.y;

                    // Two triangles per quad
                    _drawImageTri(ctx, tex, x0, y0, x1, y0, x1, y1, dx00, dy00, dx10, dy10, dx11, dy11);
                    _drawImageTri(ctx, tex, x0, y0, x1, y1, x0, y1, dx00, dy00, dx11, dy11, dx01, dy01);
                }
            }
            ctx.imageSmoothingEnabled = _prevSmooth;
            return true;
        }

        function drawSelectionOutline(o) {
            try {
                ctx.save();
                ctx.globalAlpha = 1.0;
                ctx.strokeStyle = "#EBEB70";
                ctx.lineWidth = 2;
                // Rects (including textured)
                if (o.type === 5 || o.type === 6) {
                    const rr = o.rx || 0;
                    if (o.mat3 && o.mat3.length === 9) {
                        const ptsLocal = [[0, 0], [o.w, 0], [o.w, o.h], [0, o.h]];
                        ctx.beginPath();
                        for (let i = 0; i < ptsLocal.length; i++) {
                            const p = applyMat3(o.mat3, ptsLocal[i][0], ptsLocal[i][1]);
                            const px = o.x + p.x, py = o.y + p.y;
                            if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
                        }
                        ctx.closePath();
                        ctx.stroke();
                    } else {
                        ctx.beginPath();
                        if (rr > 0 && typeof ctx.roundRect === "function") ctx.roundRect(o.x, o.y, o.w, o.h, rr);
                        else ctx.rect(o.x, o.y, o.w, o.h);
                        ctx.stroke();
                    }
                    ctx.restore();
                    return;
                }
                // Ellipses
                if (o.type === 7 || o.type === 8) {
                    const rx = o.rx || 1;
                    const ry = o.ry || rx;
                    ctx.beginPath();
                    ctx.ellipse(o.x, o.y, rx, ry, 0, 0, Math.PI * 2);
                    ctx.stroke();
                    ctx.restore();
                    return;
                }
                // Arcs: use outer radius for bounding circle
                if (o.type === 9) {
                    const rOuter = Math.max(o.rx || 1, o.ry || o.rx || 1);
                    ctx.beginPath();
                    ctx.arc(o.x, o.y, rOuter, 0, Math.PI * 2);
                    ctx.stroke();
                    ctx.restore();
                    return;
                }
                // Paths/polys: draw bounding box
                if ((o.type >= 10 && o.type <= 13) && o.points) {
                    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
                    for (let i = 0; i < o.points.length; i++) {
                        let px = o.points[i][0], py = o.points[i][1];
                        if (o.mat3 && o.mat3.length === 9) {
                            const p = applyMat3(o.mat3, px, py);
                            px = p.x; py = p.y;
                        }
                        px += o.x; py += o.y;
                        if (px < minX) minX = px;
                        if (py < minY) minY = py;
                        if (px > maxX) maxX = px;
                        if (py > maxY) maxY = py;
                    }
                    if (minX !== Infinity) {
                        ctx.beginPath();
                        ctx.rect(minX - 2, minY - 2, (maxX - minX) + 4, (maxY - minY) + 4);
                        ctx.stroke();
                    }
                    ctx.restore();
                    return;
                }
                // Text: bbox if available
                if (o.type === 4 && o.text) {
                    // Use rendered SIF2 PNG bounds when available
                    try {
                        const scale = (o.textScale && o.textScale > 0) ? o.textScale : 1.0;
                        const align = (o.textAlign !== undefined && o.textAlign !== null) ? o.textAlign : 0;
                        const url = `/text/${o.fontSlot}?t=${encodeURIComponent(o.text)}&s=${encodeURIComponent(scale)}&a=${encodeURIComponent(align)}&c=${encodeURIComponent(o.color >>> 0)}`;
                        const imgData = imgCache.get(url);
                        if (imgData && imgData.img && imgData.img.complete && imgData.img.naturalWidth > 0) {
                            const { img, offsetX, offsetY } = imgData;
                            ctx.beginPath();
                            ctx.rect(o.x + (offsetX || 0), o.y + (offsetY || 0), img.naturalWidth, img.naturalHeight);
                            ctx.stroke();
                            ctx.restore();
                            return;
                        }
                    } catch (e) { }
                    // fallback bbox if provided
                    if (o.w && o.h) {
                        ctx.beginPath();
                        ctx.rect(o.x, o.y, o.w, o.h);
                        ctx.stroke();
                        ctx.restore();
                        return;
                    }
                }
                ctx.restore();
            } catch (e) { }
        }

        function clearCanvas() {
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            if (bgVideoEnabled && bgVideoEl && bgVideoEl.readyState >= 2) {
                try {
                    ctx.drawImage(bgVideoEl, 0, 0, canvas.width, canvas.height);
                    return;
                } catch (e) { }
            }
            ctx.fillStyle = `rgba(0, 0, 0, ${Math.max(0, Math.min(1, screenBgOpacity)).toFixed(3)})`;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }

        // Draw diagnostic text when no objects are present (helps debug rendering issues)
        function drawDiagnosticInfo(connected, objectCount, orderLen) {
            if (objectCount > 0) return; // Only show when empty
            const contentOpacity = Math.max(0, Math.min(1, previewHudOpacity));
            if (contentOpacity <= 0.001) return;

            ctx.save();
            ctx.globalAlpha = contentOpacity;
            ctx.fillStyle = "#555";
            ctx.font = "12px monospace";
            ctx.textAlign = "center";

            const cx = canvas.width / 2;
            const cy = canvas.height / 2;

            if (!connected) {
                ctx.fillText("Waiting for phone connection...", cx, cy - 10);
                ctx.fillText("Connect via WebSocket to start rendering", cx, cy + 10);
            } else {
                ctx.fillText("Phone connected - waiting for draw commands", cx, cy - 10);
                ctx.fillText(`Snapshots: ${__snapshotDebug.received}, Objects seen: ${__snapshotDebug.withObjects}`, cx, cy + 10);
            }
            ctx.restore();
        }

        function drawViewport(vp) {
            if (!vp || vp.w <= 0 || vp.h <= 0) return;
            if (!showDisplayBoundaries) return;
            const viewportOpacity = Math.max(0, Math.min(1, previewHudOpacity));
            if (viewportOpacity <= 0.001) return;

            // Draw semi-transparent grey viewport frame
            const x = vp.x;
            const y = vp.y;
            const w = vp.w;
            const h = vp.h;

            ctx.globalAlpha = 0.5 * viewportOpacity;
            ctx.fillStyle = "#666";
            ctx.fillRect(x, y, w, h);

            ctx.globalAlpha = viewportOpacity;
            ctx.strokeStyle = "#aaa";
            ctx.lineWidth = 2;
            ctx.strokeRect(x, y, w, h);
        }

        function isTransparentBlackMaskObject(o) {
            if (!transparentBlacks || !transparentBlackClip || !o || o.textureUrl) return false;
            const col = argbToRgba(o.color);
            return col.a > 0 && col.r === 0 && col.g === 0 && col.b === 0;
        }

        let __transparentBlackMaskCanvas = null;
        function punchTransparentBlackMask(o, clipRegions, objectIndex, vp) {
            if (!isTransparentBlackMaskObject(o)) return false;
            if (!__transparentBlackMaskCanvas) __transparentBlackMaskCanvas = document.createElement("canvas");
            if (__transparentBlackMaskCanvas.width !== canvas.width) __transparentBlackMaskCanvas.width = canvas.width;
            if (__transparentBlackMaskCanvas.height !== canvas.height) __transparentBlackMaskCanvas.height = canvas.height;

            const mctx = __transparentBlackMaskCanvas.getContext("2d");
            mctx.setTransform(1, 0, 0, 1, 0, 0);
            mctx.globalCompositeOperation = "source-over";
            mctx.globalAlpha = 1;
            mctx.clearRect(0, 0, __transparentBlackMaskCanvas.width, __transparentBlackMaskCanvas.height);

            mctx.save();
            mctx.translate(Number(vp?.x) || 0, Number(vp?.y) || 0);
            if (clipEnabled) {
                try {
                    mctx.beginPath();
                    const vw = Number(vp?.w) || 640;
                    const vh = Number(vp?.h) || 400;
                    if (vw > 0 && vh > 0) mctx.rect(0, 0, vw, vh);
                    mctx.clip();
                } catch (e) { }
            }
            if (clipEnabled && Array.isArray(clipRegions)) {
                for (const r of clipRegions) {
                    if (r.start < objectIndex && objectIndex < r.end) {
                        const c = r.clip;
                        const rr = c.rx || 0;
                        mctx.beginPath();
                        if (rr > 0 && typeof mctx.roundRect === "function") mctx.roundRect(c.x, c.y, c.w, c.h, rr);
                        else mctx.rect(c.x, c.y, c.w, c.h);
                        mctx.clip();
                    }
                }
            }

            const prevTransparentBlacks = transparentBlacks;
            const prevTransparentBlackClip = transparentBlackClip;
            const prevShowBlacks = showBlacks;
            transparentBlacks = false;
            transparentBlackClip = false;
            showBlacks = false;
            try {
                drawObj(o, mctx);
            } finally {
                transparentBlacks = prevTransparentBlacks;
                transparentBlackClip = prevTransparentBlackClip;
                showBlacks = prevShowBlacks;
                mctx.restore();
            }

            ctx.save();
            ctx.globalCompositeOperation = "destination-out";
            ctx.globalAlpha = 1;
            ctx.drawImage(__transparentBlackMaskCanvas, -(Number(vp?.x) || 0), -(Number(vp?.y) || 0));
            ctx.restore();
            return true;
        }

        // Match mav2ws/model.py VhudProjection (billboard corners when server omits projectedBillboard).
        function __mul44x44(a, b) {
            const out = new Float64Array(16);
            for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) {
                out[r * 4 + c] =
                    a[r * 4 + 0] * b[0 * 4 + c] +
                    a[r * 4 + 1] * b[1 * 4 + c] +
                    a[r * 4 + 2] * b[2 * 4 + c] +
                    a[r * 4 + 3] * b[3 * 4 + c];
            }
            return out;
        }
        function __mul44x31(m, x, y, z) {
            return [
                m[0] * x + m[1] * y + m[2] * z + m[3],
                m[4] * x + m[5] * y + m[6] * z + m[7],
                m[8] * x + m[9] * y + m[10] * z + m[11],
                m[12] * x + m[13] * y + m[14] * z + m[15],
            ];
        }
        function __clipMaskHomogeneous(v) {
            const x = v[0], y = v[1], z = v[2], w = v[3];
            let mask = 0;
            if (x < -w) mask |= 1;
            if (x > w) mask |= 2;
            if (y < -w) mask |= 4;
            if (y > w) mask |= 8;
            if (z < -w) mask |= 16;
            return mask;
        }
        function __calcBillboardModelMatrix(px, py, pz) {
            const cam2objx = -px, cam2objy = -py, cam2objz = -pz;
            let x = -cam2objy;
            let y = cam2objx;
            let w = Math.sqrt(y * y + x * x + cam2objz * cam2objz) + cam2objz;
            if (w === 0.0) {
                return new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
            }
            const invNorm = 1.0 / Math.sqrt(x * x + y * y + w * w);
            x *= invNorm; y *= invNorm; w *= invNorm;
            const q00 = 2 * x * x;
            const q11 = 2 * y * y;
            const q01 = 2 * x * y;
            const q03 = 2 * x * w;
            const q13 = 2 * y * w;
            const mm = new Float64Array([
                1.0 - q11, q01, q13, px,
                q01, 1.0 - q00, -q03, py,
                -q13, q03, 1.0 - (q11 + q00), pz,
                0.0, 0.0, 0.0, 1.0,
            ]);
            const a = Math.atan2(cam2objy, cam2objx);
            const s = Math.sin(a - Math.PI * 0.5);
            const c = Math.cos(a - Math.PI * 0.5);
            const rotate = new Float64Array([
                c, -s, 0.0, 0.0,
                s, c, 0.0, 0.0,
                0.0, 0.0, 1.0, 0.0,
                0.0, 0.0, 0.0, 1.0,
            ]);
            return __mul44x44(mm, rotate);
        }
        function __clientBillboardScreenQuad(vp, vec, spriteSize) {
            if (!vp || vp.length !== 16 || !vec || vec.length < 3 || !spriteSize) return null;
            const sw = Number(spriteSize[0]);
            const sh = Number(spriteSize[1]);
            if (!(sw > 0) || !(sh > 0)) return null;
            const px = Number(vec[0]), py = Number(vec[1]), pz = Number(vec[2]);
            const bbModel = __calcBillboardModelMatrix(px, py, pz);
            const sx = 0.5 * sw;
            const sy = 0.5 * sh;
            const localVerts = [[sx, -sy, 0], [-sx, -sy, 0], [-sx, sy, 0], [sx, sy, 0]];
            const mvp = __mul44x44(vp, bbModel);
            let clipAgg = 0xffffffff;
            const tclip = [];
            for (const p of localVerts) {
                const tv = __mul44x31(mvp, p[0], p[1], p[2]);
                tclip.push(tv);
                clipAgg &= __clipMaskHomogeneous(tv);
            }
            if (clipAgg) return null;
            const W = 640, H = 400;
            const vpX0 = W * 0.5, vpY0 = -H * 0.5, vpX1 = W * 0.5, vpY1 = H * 0.5;
            const out = [];
            for (const v of tclip) {
                if (v[3] === 0.0) return null;
                const rhw = 1.0 / v[3];
                out.push([vpX1 + v[0] * rhw * vpX0, vpY1 + v[1] * rhw * vpY0]);
            }
            const vox = (typeof window !== "undefined" && window.__viewVp) ? (Number(window.__viewVp.x) || 0) : 0;
            const voy = (typeof window !== "undefined" && window.__viewVp) ? (Number(window.__viewVp.y) || 0) : 0;
            if (vox || voy) {
                for (let i = 0; i < out.length; i++) {
                    out[i] = [out[i][0] - vox, out[i][1] - voy];
                }
            }
            return out;
        }

        function getObjectHudAlphaFactor(o) {
            // Keep camera/video textures opaque so HUD opacity does not blend duplicate street/video frames.
            return o && o.isVideoTexture ? 1.0 : previewHudOpacity;
        }

        function drawObj(o, ctx2d) {
            const ctx = ctx2d || window.__mainCtx;
            // Brightness should be applied only once at final composition time.
            // Offscreen USER_TEXTURE pre-rendering should run at full brightness (HW behavior),
            // otherwise brightness is applied twice (once in offscreen, once when drawing the texture).
            const bright = (window.__offscreenPass ? 1.0 : ((window.__sim_brightness ?? 150) / 255.0));
            const hudAlphaFactor = getObjectHudAlphaFactor(o);
            let col = argbToRgba(o.color);
            const isOpaqueBlack = col.a > 0 && col.r === 0 && col.g === 0 && col.b === 0;
            const transparentBlackMask = transparentBlacks && transparentBlackClip && isOpaqueBlack && !o.textureUrl;
            if (transparentBlacks && isOpaqueBlack && !o.textureUrl && !transparentBlackClip) return;
            if (transparentBlackMask) {
                ctx.globalCompositeOperation = "destination-out";
                col = { r: 255, g: 255, b: 255, a: col.a, css: `rgba(255,255,255,${col.a.toFixed(3)})` };
            } else if (showBlacks && isOpaqueBlack) {
                const rr = pseudoRandomBrightRgb(o.id || 0);
                col = { r: rr.r, g: rr.g, b: rr.b, a: col.a, css: `rgba(${rr.r},${rr.g},${rr.b},${col.a.toFixed(3)})` };
            }
            const _ot = Number(o.type);
            if (o.worldClippedOut === true) {
                return;
            }
            // Some streams drive alpha via a dedicated scalar field (`o.alpha`)
            // instead of rewriting ARGB on `o.color`. Include both paths so
            // BLUR gradients animate alpha exactly like glasses/preview.
            const objectAlpha = Number.isFinite(Number(o.alpha))
                ? Math.max(0, Math.min(1, Number(o.alpha) / 255))
                : 1.0;
            ctx.globalAlpha = col.a * objectAlpha * (transparentBlackMask ? 1.0 : bright) * hudAlphaFactor;
            ctx.lineWidth = Math.max(1, o.penSize || 1);
            const isRoundPen = Number(o.penShape || 0) === 2;
            ctx.lineJoin = isRoundPen ? "round" : "miter";
            ctx.lineCap = isRoundPen ? "round" : "butt";

            // Handle gradients if present
            let fillStyle = `rgb(${col.r},${col.g},${col.b})`;
            let strokeStyle = `rgb(${col.r},${col.g},${col.b})`;
            // Set true by the BLUR ellipse branch when it has already drawn the
            // elliptical halo inline (via a scale-transform circle). The regular
            // ellipse-draw block below then skips its own ctx.fill()/stroke() to
            // avoid double-painting. Reset every object/frame because it is a
            // function-local let.
            let blurEllipseDrawn = false;

            if (o.grad && o.grad.size > 0) {
                // For ellipses (type 7/8), use angular/sweep gradient (HW behavior)
                if (o.type === 7 || o.type === 8) {
                    const rx = o.rx || 1;
                    const ry = o.ry || rx;
                    const cx = o.x;
                    const cy = o.y;
                    const angle = (o.grad.angle || 0); // degrees, 0=east, CW+ (SDK preview convention)
                    const size = o.grad.size; // gradient width in pixels

                    const isBlur = (o.grad.flags & 0x80) !== 0; // GRAD_FLAG_BLUR
                    const hasColor = (o.grad.flags & 0x08) !== 0;
                    let onStart = (o.grad.flags & 0x20) !== 0;
                    const onEnd = (o.grad.flags & 0x40) !== 0;
                    if (!onStart && !onEnd) onStart = true;

                    if (isBlur) {
                        // M2GradientBlur: radial centre→edge alpha falloff (matches firmware
                        // M2EllipseFilled w/ M2GradientBlur). The wire `size` is encoded by
                        // the SDK as `ellipseDiameterAtAngle(rx, ry, gradAngle) * percent`
                        // (see M2GradientFill.getGradientSizePix), so invert that exact
                        // formula here. Using `(rx + ry)` only matches when rx == ry; for
                        // elongated ellipses (e.g. Mav2Tester GlowRect halos with rx≈18,
                        // ry≈100) it under-recovered percent ~3× and the fade collapsed to
                        // a thin sliver at the edge.
                        // Linear falloff (MVP) — not the exact Gaussian curve, but visually
                        // matches the radial fade-to-transparent semantics.
                        const angRad = (angle || 0) * Math.PI / 180;
                        const cA = Math.cos(angRad);
                        const sA = Math.sin(angRad);
                        const denom = Math.sqrt((ry * cA) * (ry * cA) + (rx * sA) * (rx * sA));
                        const diameterAtAngle = denom > 0 ? (rx * ry) / denom * 2 : 0;
                        const percent = diameterAtAngle > 0
                            ? Math.max(0, Math.min(1, size / diameterAtAngle))
                            : 0;
                        const maxR = Math.max(rx, ry);
                        if (percent > 0 && maxR > 0) {
                            // Canvas2D `createRadialGradient` is strictly circular, so we
                            // can't get an elliptical fade by simply assigning the gradient
                            // to a non-circular ellipse path — for elongated rx/ry the
                            // gradient's solid disk (radius (1-percent)*maxR) covers the
                            // entire short axis and the visible fade collapses to a thin
                            // band along the long axis (the GlowRect repro: rx=18 ry=100
                            // p=0.45 → solid disk r=55, ellipse only extends 18px on X).
                            //
                            // Match the SDK preview's approach
                            // (see `M2PreviewComposable.kt#drawM2EllipseBlurGradient`):
                            // build the radial gradient as a CIRCLE in scaled space and
                            // apply scale(rx/maxR, ry/maxR) so it becomes an ellipse in
                            // screen space. The fade band is then proportional to each
                            // semi-axis. Drawn inline here; the regular ellipse draw block
                            // below skips this object via `blurEllipseDrawn`.
                            //
                            // For both filled (type 8) and outlined (type 7) ellipses we
                            // draw the BLUR halo as a fill — this matches the firmware
                            // M2GradientBlur semantics (a centre→edge alpha falloff inside
                            // the shape). render_tk.py's elliptical alpha mask treats both
                            // cases identically, so do the same here.
                            // Match glasses closer: avoid a hard "candle" core.
                            // Fade starts near the center and drops quickly.
                            const tunedPercent = Math.max(0, Math.min(1, percent * 0.52));
                            const innerStop = Math.max(0, Math.min(0.85, 1 - tunedPercent));
                            const outerSpan = 1 - innerStop;
                            const midStop = Math.max(0.12, Math.min(0.92, innerStop + outerSpan * 0.28));
                            ctx.save();
                            ctx.translate(cx, cy);
                            ctx.scale(rx / maxR, ry / maxR);
                            const blurGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, maxR);
                            blurGrad.addColorStop(0, `rgba(${col.r},${col.g},${col.b},0.62)`);
                            blurGrad.addColorStop(Math.max(0.08, innerStop * 0.45), `rgba(${col.r},${col.g},${col.b},0.35)`);
                            blurGrad.addColorStop(midStop, `rgba(${col.r},${col.g},${col.b},0.12)`);
                            blurGrad.addColorStop(1, `rgba(${col.r},${col.g},${col.b},0)`);
                            ctx.fillStyle = blurGrad;
                            ctx.beginPath();
                            ctx.arc(0, 0, maxR, 0, 2 * Math.PI);
                            ctx.fill();
                            ctx.restore();
                            blurEllipseDrawn = true;
                        }
                    } else {

                    const col2 = hasColor ? argbToRgba(o.grad.color2) : { r: col.r, g: col.g, b: col.b, a: 0, css: `rgba(${col.r},${col.g},${col.b},0)` };
                    const col1 = col; // base color

                    const avgRadius = (rx + ry) / 2;
                    // Angular size in radians: size (pixels) / radius
                    const angSizeRad = (size / avgRadius); // radians
                    const angSizeDeg = angSizeRad * 180 / Math.PI; // degrees

                    // Create a conic gradient if supported (Chrome 99+, Safari 15.4+)
                    if (typeof ctx.createConicGradient === 'function') {
                        // Canvas conic gradients use the same visual convention as the SDK preview here:
                        // 0° is east/right and positive angles move clockwise.
                        const startAngleDeg = angle;
                        const startAngleRad = startAngleDeg * Math.PI / 180;
                        const grad = ctx.createConicGradient(startAngleRad, cx, cy);

                        // The gradient should be: base color everywhere EXCEPT near gradAngle where it fades to col2.
                        // Python logic: angular_distance from gradAngle -> if dist < angSize, fade; else base.
                        // For conic gradient (0.0 to 1.0 wraps 360°):
                        // - Position 0.0 is at startAngle (the gradAngle position in HW coords)
                        // - We want col2 (fade color) at 0.0, transitioning to col1 at angSizeDeg/360
                        // - Then col1 for the rest of the circle back to col2 before wrapping

                        const t = Math.min(0.5, angSizeDeg / 360); // stop position (normalized 0-1)

                        // Symmetric fade: col2 at gradAngle, fade to col1 on both sides
                        grad.addColorStop(0, col2.css);              // at gradAngle
                        grad.addColorStop(t, col1.css);              // fade outward
                        grad.addColorStop(1 - t, col1.css);          // base for most of circle
                        grad.addColorStop(1, col2.css);              // fade back approaching gradAngle

                        fillStyle = grad;
                        strokeStyle = grad;
                    } else {
                        // Fallback: use radial gradient (not perfect but acceptable)
                        const grad = ctx.createRadialGradient(cx, cy, Math.max(0, avgRadius - size), cx, cy, avgRadius);
                        grad.addColorStop(0, col1.css);
                        grad.addColorStop(1, col2.css);
                        fillStyle = grad;
                        strokeStyle = grad;
                    }
                    } // end of "not blur" branch
                } else {
                    // Linear gradient for non-ellipse shapes (original code)
                    const angle = (o.grad.angle || 0) * Math.PI / 180;
                    // render_tk: theta = radians(angle), dx=sin(theta), dy=-cos(theta)
                    const dx = Math.sin(angle);
                    const dy = -Math.cos(angle);

                    // Find projection range
                    // corners relative to object origin (x,y)
                    const corners = [
                        { x: 0, y: 0 },
                        { x: o.w, y: 0 },
                        { x: o.w, y: o.h },
                        { x: 0, y: o.h }
                    ];

                    let minP = Infinity, maxP = -Infinity;
                    let minIdx = -1;

                    for (let i = 0; i < corners.length; i++) {
                        // projection p = x*dx + y*dy
                        const p = corners[i].x * dx + corners[i].y * dy;
                        if (p < minP) { minP = p; minIdx = i; }
                        if (p > maxP) maxP = p;
                    }

                    const span = maxP - minP;
                    const size = o.grad.size; // in pixels

                    if (span > 0.1) {
                        // Start point: global position of min corner
                        const sx = o.x + corners[minIdx].x;
                        const sy = o.y + corners[minIdx].y;
                        // End point: start + vector * span
                        const ex = sx + dx * span;
                        const ey = sy + dy * span;

                        const grad = ctx.createLinearGradient(sx, sy, ex, ey);

                        const hasColor = (o.grad.flags & 0x08) !== 0;
                        let onStart = (o.grad.flags & 0x20) !== 0;
                        const onEnd = (o.grad.flags & 0x40) !== 0;
                        if (!onStart && !onEnd) onStart = true;

                        const t1 = Math.max(0, Math.min(1, size / span));

                        // Colors
                        // t=0 (at edge): color2 (if color) or TRANSPARENT (if fade)
                        // t=1 (inside): color (base) or OPAQUE (if fade)

                        const col2 = hasColor ? argbToRgba(o.grad.color2) : { r: col.r, g: col.g, b: col.b, a: 0, css: `rgba(${col.r},${col.g},${col.b},0)` };
                        const col1 = col; // base

                        const c1Css = col1.css; // Inside/Base
                        const c2Css = col2.css; // Edge/Fade

                        // Add stops
                        // If ON_START: 0.0 -> c2, t1 -> c1
                        // If ON_END: (1-t1) -> c1, 1.0 -> c2

                        // We need to initialize the gradient with base color first if we are blending
                        // But linear gradient interpolates.

                        // Simple case: Start only
                        if (onStart && !onEnd) {
                            grad.addColorStop(0, c2Css);
                            grad.addColorStop(t1, c1Css);
                            grad.addColorStop(1, c1Css);
                        } else if (onEnd && !onStart) {
                            grad.addColorStop(0, c1Css);
                            grad.addColorStop(1 - t1, c1Css);
                            grad.addColorStop(1, c2Css);
                        } else if (onStart && onEnd) {
                            grad.addColorStop(0, c2Css);
                            grad.addColorStop(t1, c1Css);
                            grad.addColorStop(1 - t1, c1Css);
                            grad.addColorStop(1, c2Css);
                        }

                        fillStyle = grad;
                        strokeStyle = grad;
                    }
                }
            }

            ctx.strokeStyle = strokeStyle;
            ctx.fillStyle = fillStyle;

            // 3D projected primitives from server snapshot.
            if (_ot === 14 || _ot === 15 || _ot === 16 || _ot === 17) {
                // Triangles/billboards are in session viewport-local pixel space (640×400 scale).
                // USER_TEXTURE offscreen buffers use their own pixel space — skip to avoid wrong/clipped draws.
                if (!window.__offscreenPass) {
                const drawTexturedProjectedMesh = (triangles) => {
                    if (_ot !== 16 || !Array.isArray(triangles) || triangles.length === 0) return false;
                    const ts = getTextureSlotFromObj(o);
                    const userTextures = (window.__lastSceneSnapshot && window.__lastSceneSnapshot.userTextures) ? window.__lastSceneSnapshot.userTextures : {};
                    const ut = (ts !== null && ts !== 127 && userTextures) ? userTextures[String(ts)] : null;
                    let src = null; // { kind: "img", img } or { kind: "canvas", canvas }
                    if (ut && window.__offscreenCache && window.__offscreenCache.canvases) {
                        const target = Number(ut.target);
                        const canv = window.__offscreenCache.canvases.get(target);
                        if (canv) src = { kind: "canvas", canvas: canv };
                    }
                    if (!src && o.textureUrl) {
                        const img = getTextureImgFromCache(o);
                        if (img) {
                            src = { kind: "img", img };
                        }
                    }
                    if (!src) {
                        if (o.isVideoTexture) {
                            const slotKey = Number(o.textureSlot);
                            if (Number.isFinite(slotKey) && slotKey >= 0) {
                                videoUiLog(`video:${slotKey}`, "mesh_no_src");
                            }
                        }
                        return !!o.isVideoTexture;
                    }

                    const texW = (src.kind === "img") ? Number(src.img.naturalWidth || src.img.width || 0) : Number(src.canvas.width || 0);
                    const texH = (src.kind === "img") ? Number(src.img.naturalHeight || src.img.height || 0) : Number(src.canvas.height || 0);
                    const uvs = Array.isArray(o.uvs3d) ? o.uvs3d : null;
                    const hasUvArray = !!(uvs && uvs.length >= 6 && texW > 0 && texH > 0);
                    const texBaseA = (col.a > 1e-6) ? (col.a * bright * hudAlphaFactor) : (bright * hudAlphaFactor);

                    const drawAffineUvTri = (a, b, c, uvA, uvB, uvC, triAlpha) => {
                        const u0 = uvA[0], v0 = uvA[1];
                        const u1 = uvB[0], v1 = uvB[1];
                        const u2 = uvC[0], v2 = uvC[1];
                        const x0 = a[0], y0 = a[1];
                        const x1 = b[0], y1 = b[1];
                        const x2 = c[0], y2 = c[1];
                        const det = (u0 * (v1 - v2) + u1 * (v2 - v0) + u2 * (v0 - v1));
                        if (!Number.isFinite(det) || Math.abs(det) < 1e-7) return false;

                        const A = (x0 * (v1 - v2) + x1 * (v2 - v0) + x2 * (v0 - v1)) / det;
                        const B = (x0 * (u2 - u1) + x1 * (u0 - u2) + x2 * (u1 - u0)) / det;
                        const C = (x0 * (u1 * v2 - u2 * v1) + x1 * (u2 * v0 - u0 * v2) + x2 * (u0 * v1 - u1 * v0)) / det;
                        const D = (y0 * (v1 - v2) + y1 * (v2 - v0) + y2 * (v0 - v1)) / det;
                        const E = (y0 * (u2 - u1) + y1 * (u0 - u2) + y2 * (u1 - u0)) / det;
                        const F = (y0 * (u1 * v2 - u2 * v1) + y1 * (u2 * v0 - u0 * v2) + y2 * (u0 * v1 - u1 * v0)) / det;
                        if (![A, B, C, D, E, F].every(Number.isFinite)) return false;

                        ctx.save();
                        const p = new Path2D();
                        p.moveTo(x0, y0);
                        p.lineTo(x1, y1);
                        p.lineTo(x2, y2);
                        p.closePath();
                        ctx.clip(p);
                        ctx.globalAlpha = triAlpha;
                        ctx.transform(A, D, B, E, C, F);
                        if (src.kind === "img") ctx.drawImage(src.img, 0, 0);
                        else ctx.drawImage(src.canvas, 0, 0);
                        ctx.restore();
                        return true;
                    };
                    const applyTriShadeOverlay = (a, b, c, shade255) => {
                        const s = Math.max(0, Math.min(255, Number(shade255) || 0)) / 255;
                        if (s >= 0.999) return;
                        ctx.save();
                        ctx.beginPath();
                        ctx.moveTo(a[0], a[1]);
                        ctx.lineTo(b[0], b[1]);
                        ctx.lineTo(c[0], c[1]);
                        ctx.closePath();
                        ctx.clip();
                        ctx.globalAlpha = (1.0 - s) * texBaseA;
                        ctx.fillStyle = "#000";
                        ctx.fillRect(
                            Math.min(a[0], b[0], c[0]),
                            Math.min(a[1], b[1], c[1]),
                            Math.max(1, Math.max(a[0], b[0], c[0]) - Math.min(a[0], b[0], c[0])),
                            Math.max(1, Math.max(a[1], b[1], c[1]) - Math.min(a[1], b[1], c[1]))
                        );
                        ctx.restore();
                    };

                    let any = false;
                    for (const tri of triangles) {
                        if (!Array.isArray(tri) || tri.length < 3) continue;
                        const a = tri[0], b = tri[1], c = tri[2];
                        const shade = tri.length >= 4 ? Math.max(0, Math.min(255, Number(tri[3]) || 0)) : 255;
                        if (!a || !b || !c) continue;
                        const triAlpha = texBaseA;

                        let uvDrawn = false;
                        if (hasUvArray && tri.length >= 7) {
                            const vi0 = Number(tri[4]);
                            const vi1 = Number(tri[5]);
                            const vi2 = Number(tri[6]);
                            if (Number.isInteger(vi0) && Number.isInteger(vi1) && Number.isInteger(vi2) &&
                                vi0 >= 0 && vi1 >= 0 && vi2 >= 0 &&
                                (vi0 * 2 + 1) < uvs.length && (vi1 * 2 + 1) < uvs.length && (vi2 * 2 + 1) < uvs.length) {
                                const uvA = [Number(uvs[vi0 * 2]) * texW, Number(uvs[vi0 * 2 + 1]) * texH];
                                const uvB = [Number(uvs[vi1 * 2]) * texW, Number(uvs[vi1 * 2 + 1]) * texH];
                                const uvC = [Number(uvs[vi2 * 2]) * texW, Number(uvs[vi2 * 2 + 1]) * texH];
                                uvDrawn = drawAffineUvTri(a, b, c, uvA, uvB, uvC, triAlpha);
                                if (uvDrawn) applyTriShadeOverlay(a, b, c, shade);
                            }
                        }
                        if (!uvDrawn) {
                            const minX = Math.min(a[0], b[0], c[0]);
                            const minY = Math.min(a[1], b[1], c[1]);
                            const maxX = Math.max(a[0], b[0], c[0]);
                            const maxY = Math.max(a[1], b[1], c[1]);
                            const w = Math.max(1, maxX - minX);
                            const h = Math.max(1, maxY - minY);
                            const p = new Path2D();
                            p.moveTo(a[0], a[1]);
                            p.lineTo(b[0], b[1]);
                            p.lineTo(c[0], c[1]);
                            p.closePath();
                            ctx.save();
                            ctx.clip(p);
                            ctx.globalAlpha = triAlpha;
                            if (src.kind === "img") ctx.drawImage(src.img, minX, minY, w, h);
                            else ctx.drawImage(src.canvas, minX, minY, w, h);
                            ctx.restore();
                            applyTriShadeOverlay(a, b, c, shade);
                        }
                        any = true;
                    }
                    ctx.globalAlpha = 1.0;
                    return any;
                };

                if (Array.isArray(o.projectedMeshTriangles) && o.projectedMeshTriangles.length > 0) {
                    if (drawTexturedProjectedMesh(o.projectedMeshTriangles)) {
                        ctx.globalAlpha = 1.0;
                        return;
                    }
                    if (o.isVideoTexture) {
                        ctx.globalAlpha = 1.0;
                        return;
                    }
                    // MESH_3D often uses color alpha0 for texture-only; solid fallback must still draw.
                    ctx.save();
                    if (_ot === 16 && !(col.a > 1e-6)) {
                        ctx.globalAlpha = 1.0 * bright * hudAlphaFactor;
                    }
                    for (const tri of o.projectedMeshTriangles) {
                        if (!Array.isArray(tri) || tri.length < 3) continue;
                        const shade = tri.length >= 4 ? Math.max(0, Math.min(255, Number(tri[3]) || 0)) : 255;
                        ctx.beginPath();
                        ctx.moveTo(tri[0][0], tri[0][1]);
                        ctx.lineTo(tri[1][0], tri[1][1]);
                        ctx.lineTo(tri[2][0], tri[2][1]);
                        ctx.closePath();
                        if (_ot === 16) {
                            if (shade < 255) {
                                const s = shade / 255;
                                const base = argbToRgba(o.color);
                                const fa = (base.a > 1e-6) ? base.a : 1.0;
                                ctx.fillStyle = `rgba(${Math.round(base.r * s)},${Math.round(base.g * s)},${Math.round(base.b * s)},${fa})`;
                            } else if (!(col.a > 1e-6)) {
                                ctx.fillStyle = `rgba(${col.r},${col.g},${col.b},1)`;
                            } else {
                                ctx.fillStyle = fillStyle;
                            }
                            ctx.fill();
                        } else ctx.stroke();
                    }
                    ctx.restore();
                    ctx.globalAlpha = 1.0;
                    return;
                }
                // Fallback: client-side projection from modelMatrix + vertices3d.
                if (_ot === 16 && Array.isArray(o.vertices3d) && o.vertices3d.length >= 9 &&
                    Array.isArray(o.modelMatrix) && o.modelMatrix.length === 16 &&
                    window.__lastSceneSnapshot && Array.isArray(window.__lastSceneSnapshot.vp_matrix) &&
                    window.__lastSceneSnapshot.vp_matrix.length === 16) {
                    try {
                        const mm = o.modelMatrix;
                        const verts = o.vertices3d;
                        const idx = Array.isArray(o.indices3d) && o.indices3d.length > 0 ? o.indices3d : Array.from({length: verts.length / 3}, (_, i) => i);
                        function mul44(a, b) {
                            const r = new Float64Array(16);
                            for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) {
                                r[i*4+j] = a[i*4]*b[j] + a[i*4+1]*b[4+j] + a[i*4+2]*b[8+j] + a[i*4+3]*b[12+j];
                            }
                            return r;
                        }
                        function clipMask(v) {
                            const x = v[0], y = v[1], z = v[2], w = v[3];
                            let mask = 0;
                            if (x < -w) mask |= 1;
                            if (x >  w) mask |= 2;
                            if (y < -w) mask |= 4;
                            if (y >  w) mask |= 8;
                            if (z < -w) mask |= 16;
                            return mask;
                        }
                        function proj(mvp, x, y, z) {
                            const cx = mvp[0]*x + mvp[1]*y + mvp[2]*z + mvp[3];
                            const cy = mvp[4]*x + mvp[5]*y + mvp[6]*z + mvp[7];
                            const cz = mvp[8]*x + mvp[9]*y + mvp[10]*z + mvp[11];
                            const cw = mvp[12]*x + mvp[13]*y + mvp[14]*z + mvp[15];
                            if (cw <= 1e-4) return null;
                            const rhw = 1.0 / cw;
                            const vpx = 320, vpyScale = -200, vofsx = 320, vofsy = 200;
                            const vox = (window.__viewVp && Number.isFinite(window.__viewVp.x)) ? window.__viewVp.x : 0;
                            const voy = (window.__viewVp && Number.isFinite(window.__viewVp.y)) ? window.__viewVp.y : 0;
                            return [cx, cy, cz, cw, vofsx + cx * rhw * vpx - vox, vofsy + cy * rhw * vpyScale - voy];
                        }
                        const snap = window.__lastSceneSnapshot;
                        const vp = snap.vp_matrix;
                        if (!vp || vp.length !== 16) throw new Error("no vp_matrix");
                        const mvp = mul44(vp, mm);
                        let drawn = false;
                        const projectedTriangles = [];
                        const maxTri = Math.min(Math.floor(idx.length / 3), 4096);
                        for (let ti = 0; ti < maxTri; ti++) {
                            const ia = idx[ti*3]*3, ib = idx[ti*3+1]*3, ic = idx[ti*3+2]*3;
                            if (ia+2 >= verts.length || ib+2 >= verts.length || ic+2 >= verts.length) continue;
                            const p0 = proj(mvp, verts[ia], verts[ia+1], verts[ia+2]);
                            const p1 = proj(mvp, verts[ib], verts[ib+1], verts[ib+2]);
                            const p2 = proj(mvp, verts[ic], verts[ic+1], verts[ic+2]);
                            if (!p0 || !p1 || !p2) continue;
                            const clipAgg = clipMask(p0) & clipMask(p1) & clipMask(p2);
                            if (clipAgg) continue;
                            const frontS = (
                                p0[0] * (p1[1] - p2[1]) +
                                p1[0] * (p2[1] - p0[1]) +
                                p2[0] * (p0[1] - p1[1])
                            );
                            if (frontS <= 0.0) continue;
                            projectedTriangles.push([
                                [p0[4], p0[5]],
                                [p1[4], p1[5]],
                                [p2[4], p2[5]],
                                255,
                                Math.floor(ia / 3),
                                Math.floor(ib / 3),
                                Math.floor(ic / 3)
                            ]);
                        }
                        if (drawTexturedProjectedMesh(projectedTriangles)) { ctx.globalAlpha = 1.0; return; }
                        if (o.isVideoTexture) { ctx.globalAlpha = 1.0; return; }
                        ctx.save();
                        if (!(col.a > 1e-6)) {
                            ctx.globalAlpha = 1.0 * bright * hudAlphaFactor;
                        }
                        for (const tri of projectedTriangles) {
                            ctx.beginPath();
                            ctx.moveTo(tri[0][0], tri[0][1]);
                            ctx.lineTo(tri[1][0], tri[1][1]);
                            ctx.lineTo(tri[2][0], tri[2][1]);
                            ctx.closePath();
                            ctx.fill();
                            drawn = true;
                        }
                        ctx.restore();
                        if (drawn) { ctx.globalAlpha = 1.0; return; }
                    } catch (e) { console.warn("3D client proj err", e); }
                }
                if (Array.isArray(o.projectedBillboard) && o.projectedBillboard.length === 4) {
                    ctx.save();
                    if (_ot === 16 && !(col.a > 1e-6)) {
                        ctx.globalAlpha = 1.0 * bright * hudAlphaFactor;
                    }
                    ctx.beginPath();
                    ctx.moveTo(o.projectedBillboard[0][0], o.projectedBillboard[0][1]);
                    for (let i = 1; i < 4; i++) {
                        ctx.lineTo(o.projectedBillboard[i][0], o.projectedBillboard[i][1]);
                    }
                    ctx.closePath();
                    if (_ot === 16) ctx.fill();
                    else ctx.stroke();
                    ctx.restore();
                    ctx.globalAlpha = 1.0;
                    return;
                }
                if (Array.isArray(o.projectedVec) && o.projectedVec.length >= 2) {
                    const px = Number(o.projectedVec[0]);
                    const py = Number(o.projectedVec[1]);
                    if (_ot === 15) {
                        ctx.beginPath();
                        ctx.moveTo(px - 4, py);
                        ctx.lineTo(px + 4, py);
                        ctx.moveTo(px, py - 4);
                        ctx.lineTo(px, py + 4);
                        ctx.stroke();
                    } else {
                        ctx.beginPath();
                        ctx.arc(px, py, 3, 0, Math.PI * 2);
                        if (_ot === 17) ctx.stroke();
                        else ctx.fill();
                    }
                    ctx.globalAlpha = 1.0;
                    return;
                }
                }
            }

            // Billboard-projected RECT/RECT_FILLED (e.g. M2RectFilled.asBillboard with texture).
            // Prefer server quad; if missing (clip race / lock miss), match Python VhudProjection in-browser.
            let bbPtsRect = (Array.isArray(o.projectedBillboard) && o.projectedBillboard.length === 4) ? o.projectedBillboard : null;
            if (!bbPtsRect && (_ot === 5 || _ot === 6) && o.vec && o.spriteSize) {
                const snap = window.__lastSceneSnapshot;
                if (snap && Array.isArray(snap.vp_matrix) && snap.vp_matrix.length === 16) {
                    bbPtsRect = __clientBillboardScreenQuad(snap.vp_matrix, o.vec, o.spriteSize);
                }
            }
            if ((_ot === 5 || _ot === 6) && bbPtsRect && bbPtsRect.length === 4) {
                const bbPts = bbPtsRect;
                const bbPath = new Path2D();
                bbPath.moveTo(bbPts[0][0], bbPts[0][1]);
                for (let bi = 1; bi < 4; bi++) bbPath.lineTo(bbPts[bi][0], bbPts[bi][1]);
                bbPath.closePath();

                const bbTs = getTextureSlotFromObj(o);
                const bbUserTex = (window.__lastSceneSnapshot && window.__lastSceneSnapshot.userTextures) ? window.__lastSceneSnapshot.userTextures : {};
                const bbUt = (bbTs !== null && bbTs !== 127 && bbUserTex) ? bbUserTex[String(bbTs)] : null;
                let bbSrc = null;
                if (bbUt && window.__offscreenCache && window.__offscreenCache.canvases) {
                    const target = Number(bbUt.target);
                    const canv = window.__offscreenCache.canvases.get(target);
                    if (canv) bbSrc = { kind: "canvas", canvas: canv };
                }
                if (!bbSrc && o.textureUrl) {
                    const img = getTextureImgFromCache(o);
                    if (img) {
                        bbSrc = { kind: "img", img };
                    }
                }
                if (bbSrc) {
                    const minBx = Math.min(bbPts[0][0], bbPts[1][0], bbPts[2][0], bbPts[3][0]);
                    const minBy = Math.min(bbPts[0][1], bbPts[1][1], bbPts[2][1], bbPts[3][1]);
                    const maxBx = Math.max(bbPts[0][0], bbPts[1][0], bbPts[2][0], bbPts[3][0]);
                    const maxBy = Math.max(bbPts[0][1], bbPts[1][1], bbPts[2][1], bbPts[3][1]);
                    const bw = Math.max(1, maxBx - minBx);
                    const bh = Math.max(1, maxBy - minBy);
                    ctx.save();
                    ctx.clip(bbPath);
                    const texAlpha = col.a > 1e-6 ? col.a : 1.0;
                    ctx.globalAlpha = texAlpha * bright * hudAlphaFactor;
                    if (o.sprite && o.sprite.length === 4) {
                        const [sx, sy, sw, sh] = o.sprite;
                        if (bbSrc.kind === "img") ctx.drawImage(bbSrc.img, sx, sy, sw, sh, minBx, minBy, bw, bh);
                        else ctx.drawImage(bbSrc.canvas, sx, sy, sw, sh, minBx, minBy, bw, bh);
                    } else {
                        if (bbSrc.kind === "img") ctx.drawImage(bbSrc.img, minBx, minBy, bw, bh);
                        else ctx.drawImage(bbSrc.canvas, minBx, minBy, bw, bh);
                    }
                    ctx.restore();
                } else {
                    ctx.save();
                    if (_ot === 6) {
                        ctx.fillStyle = fillStyle;
                        if (!(col.a > 1e-6)) ctx.globalAlpha = 1.0 * bright * hudAlphaFactor;
                        ctx.fill(bbPath);
                    } else {
                        ctx.stroke(bbPath);
                    }
                    ctx.restore();
                }
                ctx.globalAlpha = 1.0;
                return;
            }
            if ((_ot === 5 || _ot === 6) && o.vec && o.spriteSize) {
                // World billboard rect with no valid projection this frame: clip out.
                return;
            }

            // RECT / RECT_FILLED: may be a solid shape OR a texture.
            // IMPORTANT: Only enter the texture branch when a real texture source exists.
            const ts = getTextureSlotFromObj(o);
            const userTextures = (window.__lastSceneSnapshot && window.__lastSceneSnapshot.userTextures) ? window.__lastSceneSnapshot.userTextures : {};
            const ut = (ts !== null && ts !== 127 && userTextures) ? userTextures[String(ts)] : null;
            const hasTextureSource = !!(o.textureUrl || ut);

            if ((_ot === 5 || _ot === 6) && hasTextureSource) {
                const rr = (o.rx || 0);

                // Source can be a regular image URL (cached in imgCache) OR an offscreen canvas.
                let src = null; // { kind: "img", img } or { kind: "canvas", canvas }
                // IMPORTANT: if this slot is a USER_TEXTURE, prefer offscreen canvas over /texture URL.
                if (ut && window.__offscreenCache && window.__offscreenCache.canvases) {
                    const target = Number(ut.target);
                    const canv = window.__offscreenCache.canvases.get(target);
                    if (canv) src = { kind: "canvas", canvas: canv };
                }
                if (!src && o.textureUrl) {
                    const img = getTextureImgFromCache(o);
                    if (img) {
                        src = { kind: "img", img };
                    }
                }

                if (src) {
                    try {
                        const drawSrc = (dctx, sx, sy, sw, sh, dx, dy, dw, dh) => {
                            if (src.kind === "img") {
                                if (sx !== null) dctx.drawImage(src.img, sx, sy, sw, sh, dx, dy, dw, dh);
                                else dctx.drawImage(src.img, dx, dy, dw, dh);
                            } else {
                                if (sx !== null) dctx.drawImage(src.canvas, sx, sy, sw, sh, dx, dy, dw, dh);
                                else dctx.drawImage(src.canvas, dx, dy, dw, dh);
                            }
                        };

                        if (o.mat3 && o.mat3.length === 9) {
                            const m = o.mat3;
                            const hasPersp = (Math.abs(m[6] || 0) + Math.abs(m[7] || 0)) > 1e-7;
                            if (hasPersp) {
                                // True projective: approximate via triangle grid.
                                const ok = _drawTexturedRectProjective(ctx, o, src, rr);
                                if (!ok) {
                                    // Fallback to affine if something went wrong.
                                    ctx.save();
                                    ctx.translate(o.x, o.y);
                                    ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
                                    if (rr > 0) {
                                        ctx.beginPath();
                                        if (typeof ctx.roundRect === 'function') ctx.roundRect(0, 0, o.w, o.h, rr);
                                        else ctx.rect(0, 0, o.w, o.h);
                                        ctx.clip();
                                    }
                                    if (o.sprite && o.sprite.length === 4) {
                                        const [sx, sy, sw, sh] = o.sprite;
                                        if (sw > 0 && sh > 0) drawSrc(ctx, sx, sy, sw, sh, 0, 0, o.w, o.h);
                                    } else {
                                        drawSrc(ctx, null, null, null, null, 0, 0, o.w, o.h);
                                    }
                                    ctx.restore();
                                }
                            } else {
                                // Fast affine path.
                                ctx.save();
                                ctx.translate(o.x, o.y);
                                // Apply affine part: ctx.transform(a, b, c, d, e, f) maps to matrix [a c e; b d f; 0 0 1]
                                // Our row-major mat3 is [m0 m1 m2; m3 m4 m5; m6 m7 m8]
                                // So: a=m0, b=m3, c=m1, d=m4, e=m2, f=m5
                                ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
                                if (rr > 0) {
                                    ctx.beginPath();
                                    if (typeof ctx.roundRect === 'function') ctx.roundRect(0, 0, o.w, o.h, rr);
                                    else ctx.rect(0, 0, o.w, o.h);
                                    ctx.clip();
                                }
                                if (o.sprite && o.sprite.length === 4) {
                                    const [sx, sy, sw, sh] = o.sprite;
                                    if (sw > 0 && sh > 0) drawSrc(ctx, sx, sy, sw, sh, 0, 0, o.w, o.h);
                                } else {
                                    drawSrc(ctx, null, null, null, null, 0, 0, o.w, o.h);
                                }
                                ctx.restore();
                            }
                        } else {
                            ctx.save();
                            if (rr > 0) {
                                ctx.beginPath();
                                if (typeof ctx.roundRect === 'function') ctx.roundRect(o.x, o.y, o.w, o.h, rr);
                                else ctx.rect(o.x, o.y, o.w, o.h);
                                ctx.clip();
                            }
                            if (o.sprite && o.sprite.length === 4) {
                                const [sx, sy, sw, sh] = o.sprite;
                                if (sw > 0 && sh > 0) drawSrc(ctx, sx, sy, sw, sh, o.x, o.y, o.w, o.h);
                            } else {
                                drawSrc(ctx, null, null, null, null, o.x, o.y, o.w, o.h);
                            }
                            ctx.restore();
                        }
                    } catch (err) { }
                }
            } else if (_ot === 5 || _ot === 6) { // rect (solid)
                if (o.isVideoTexture) {
                    // MPEG1 upload streams are not image textures in the web path; avoid white fallback.
                    ctx.globalAlpha = 1.0;
                    return;
                }
                if (o.mat3 && o.mat3.length === 9) {
                    const ptsLocal = [[0, 0], [o.w, 0], [o.w, o.h], [0, o.h]];
                    ctx.beginPath();
                    for (let i = 0; i < ptsLocal.length; i++) {
                        const p = applyMat3(o.mat3, ptsLocal[i][0], ptsLocal[i][1]);
                        const px = o.x + p.x, py = o.y + p.y;
                        if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
                    }
                    ctx.closePath();
                    if (_ot === 6) ctx.fill(); else ctx.stroke();
                } else {
                    const rx = o.rx || 0;
                    if (_ot === 6) {
                        ctx.beginPath();
                        if (typeof ctx.roundRect === 'function') {
                            ctx.roundRect(o.x, o.y, o.w, o.h, rx);
                        } else {
                            ctx.rect(o.x, o.y, o.w, o.h);
                        }
                        ctx.fill();
                    } else {
                        // Draw outline inside bounds to match glasses.
                        const inset = ctx.lineWidth / 2;
                        const sx = o.x + inset;
                        const sy = o.y + inset;
                        const sw = Math.max(0, o.w - ctx.lineWidth);
                        const sh = Math.max(0, o.h - ctx.lineWidth);
                        const sr = Math.max(0, rx - inset);
                        if (sw > 0 && sh > 0) {
                            ctx.beginPath();
                            if (typeof ctx.roundRect === 'function') {
                                ctx.roundRect(sx, sy, sw, sh, sr);
                            } else {
                                ctx.rect(sx, sy, sw, sh);
                            }
                            ctx.stroke();
                        }
                    }
                }
            } else if (o.type === 10 || o.type === 11 || o.type === 12 || o.type === 13) { // path/poly
                if (o.points) {
                    ctx.beginPath();
                    for (let i = 0; i < o.points.length; i++) {
                        const pt = o.points[i];
                        let px = pt[0], py = pt[1];
                        if (o.mat3 && o.mat3.length === 9) {
                            const p = applyMat3(o.mat3, px, py);
                            px = p.x; py = p.y;
                        }
                        px = o.x + px; py = o.y + py;
                        if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
                    }
                    if (o.type === 10) ctx.stroke();
                    else { ctx.closePath(); ctx.fill(); }
                }
            } else if (o.type === 7 || o.type === 8) { // ellipse
                if (!blurEllipseDrawn) {
                    const rx = o.rx || 1;
                    const ry = o.ry || rx;
                    ctx.beginPath();
                    ctx.ellipse(o.x, o.y, rx, ry, 0, 0, 2 * Math.PI);
                    if (o.type === 8) ctx.fill(); else ctx.stroke();
                }
            } else if (o.type === 9) { // arc
                const rFrom = o.rx || 1;
                const rTo = o.ry || rFrom;
                const rOuter = Math.max(rFrom, rTo);
                const rInner = Math.min(rFrom, rTo);
                const startAngle = (-(o.a0 - 90.0)) * Math.PI / 180.0;
                let rawSweep = o.a1 - o.a0;
                let sweep;
                if (Math.abs(rawSweep) >= 360.0) {
                    sweep = 360.0;
                } else {
                    sweep = ((rawSweep + 360.0) % 360.0);
                    if (sweep === 0) sweep = 360.0;
                }
                const endAngle = startAngle + (-sweep * Math.PI / 180.0);
                const isFilled = Math.abs(o.penSize - rOuter) < 0.001;
                const hasRing = Math.abs(rFrom - rTo) > 0.001;
                ctx.beginPath();
                if (isFilled && hasRing) {
                    ctx.arc(o.x, o.y, rOuter, startAngle, endAngle, true);
                    ctx.arc(o.x, o.y, rInner, endAngle, startAngle, false);
                    ctx.closePath();
                    ctx.fill();
                } else if (isFilled) {
                    ctx.arc(o.x, o.y, rOuter, startAngle, endAngle, true);
                    ctx.lineTo(o.x, o.y);
                    ctx.closePath();
                    ctx.fill();
                } else {
                    ctx.arc(o.x, o.y, rOuter, startAngle, endAngle, true);
                    ctx.stroke();
                }
            } else if (o.type === 3 || o.type === "MASK_ROUND") { // MASK_ROUND
                let lx = o.x, ly = o.y, lw = o.w || 0, lh = o.h || 0;
                let erx = o.rx || 1, ery = o.ry || erx;
                let ecx, ecy;

                if (lw > 0 && lh > 0) {
                    // Rect defined (x,y is top-left)
                    ecx = lx + lw / 2;
                    ecy = ly + lh / 2;
                } else {
                    // Rect not defined (x,y is center)
                    lw = erx * 2;
                    lh = ery * 2;
                    lx = o.x - erx;
                    ly = o.y - ery;
                    ecx = o.x;
                    ecy = o.y;
                }

                ctx.beginPath();
                // 1. Outer Rect
                ctx.rect(lx, ly, lw, lh);
                // 2. Inner Ellipse
                ctx.ellipse(ecx, ecy, erx, ery, 0, 0, 2 * Math.PI);

                // 3. Fill with hole (evenodd rule)
                // spec: "rectangle is black"
                const oldFill = ctx.fillStyle;
                ctx.fillStyle = "#000000";
                ctx.fill("evenodd");
                ctx.fillStyle = oldFill;
            } else if (o.type === 4 && o.text) { // text
                if (o.fontSlot !== undefined && o.fontSlot !== null && o.fontSlot !== 255 && o.fontSlot !== -1) {
                    const scale = (o.textScale && o.textScale > 0) ? o.textScale : 1.0;
                    const align = (o.textAlign !== undefined && o.textAlign !== null) ? o.textAlign : 0;
                    const url = `/text/${o.fontSlot}?t=${encodeURIComponent(o.text)}&s=${encodeURIComponent(scale)}&a=${encodeURIComponent(align)}&c=${encodeURIComponent(o.color >>> 0)}`;
                    let imgData = imgCache.get(url);
                    if (!imgData) {
                        // Create a placeholder to avoid multiple starting fetches
                        imgData = { img: new Image(), offsetX: 0, offsetY: 0, status: 'loading' };
                        imgCache.set(url, imgData);

                        // Queue the fetch to avoid stampeding the server during playback bursts
                        __textFetchQueue.push(async () => {
                            const resp = await fetch(url);
                            if (!resp.ok) {
                                imgData.status = 'error';
                                return;
                            }
                            const offsetX = parseFloat(resp.headers.get('X-Text-Offset-X') || '0');
                            const offsetY = parseFloat(resp.headers.get('X-Text-Offset-Y') || '0');
                            const blob = await resp.blob();
                            imgData.img.onload = () => {
                                imgData.status = 'ready';
                                imgData.offsetX = offsetX;
                                imgData.offsetY = offsetY;
                            };
                            imgData.img.src = URL.createObjectURL(blob);
                        });
                        __pumpTextFetchQueue();
                    }

                    const { img, offsetX, offsetY, status } = imgData;
                    if (status === 'ready' && img.complete && img.naturalWidth > 0) {
                        // Success: draw new image and update stale cache
                        try {
                            const prevSmooth = ctx.imageSmoothingEnabled;
                            const prevQuality = ctx.imageSmoothingQuality;
                            ctx.imageSmoothingEnabled = true;
                            ctx.imageSmoothingQuality = "high";
                            ctx.drawImage(
                                img,
                                (o.x + offsetX),
                                (o.y + offsetY)
                            );
                            ctx.imageSmoothingEnabled = prevSmooth;
                            ctx.imageSmoothingQuality = prevQuality;
                        } catch (e) { }
                        // Update cache for this object ID
                        if (o.id) textObjCache.set(Number(o.id), { img, offsetX, offsetY });
                    } else if (o.id) {
                        // Fallback: use stale image for this object ID if available to prevent flash
                        const stale = textObjCache.get(Number(o.id));
                        if (stale && stale.img && stale.img.complete) {
                            try {
                                // Draw stale image at current position? Yes, best effort.
                                const prevSmooth = ctx.imageSmoothingEnabled;
                                const prevQuality = ctx.imageSmoothingQuality;
                                ctx.imageSmoothingEnabled = true;
                                ctx.imageSmoothingQuality = "high";
                                ctx.drawImage(
                                    stale.img,
                                    (o.x + (stale.offsetX || 0)),
                                    (o.y + (stale.offsetY || 0))
                                );
                                ctx.imageSmoothingEnabled = prevSmooth;
                                ctx.imageSmoothingQuality = prevQuality;
                            } catch (e) { }
                        }
                    }
                } else {
                    ctx.font = "12px sans-serif";
                    ctx.fillText(o.text, o.x, o.y + 12);
                }
            }

            // Reset alpha for next object
            ctx.globalAlpha = 1.0;
        }

        const TYPE_ABBR = {
            1: 'CLP', 3: 'MSK', 4: 'TXT', 5: 'RCT', 6: 'RCF', 7: 'ELP', 8: 'ELF', 9: 'ARC', 10: 'PTH', 11: 'TRI', 12: 'STP', 13: 'FAN'
        };

        function drawDbgParams(o, labelRects) {
            ctx.save();
            ctx.globalAlpha = 1.0;
            ctx.fillStyle = "#fff";
            ctx.strokeStyle = "#000";
            ctx.lineWidth = 1;
            ctx.font = "bold 12px monospace";

            // Calculate actual top-left of the bounding box
            let lx = o.x, ly = o.y;
            if (o.type === 7 || o.type === 8) { // ellipse
                lx -= (o.rx || 1);
                ly -= (o.ry || (o.rx || 1));
            } else if (o.type === 9) { // arc: use outer radius
                const rO = Math.max(o.rx || 1, o.ry || o.rx || 1);
                lx -= rO;
                ly -= rO;
            } else if ((o.type >= 10 && o.type <= 13) && o.points) { // path/poly
                let minX = Infinity, minY = Infinity;
                for (let i = 0; i < o.points.length; i++) {
                    let px = o.points[i][0], py = o.points[i][1];
                    if (o.mat3 && o.mat3.length === 9) {
                        const p = applyMat3(o.mat3, px, py);
                        px = p.x; py = p.y;
                    }
                    if (px < minX) minX = px;
                    if (py < minY) minY = py;
                }
                lx += (minX === Infinity ? 0 : minX);
                ly += (minY === Infinity ? 0 : minY);
            }

            const abbr = TYPE_ABBR[o.type] || 'UNK';
            const label = `${abbr} #${o.id}: ${Math.round(o.x)},${Math.round(o.y)}${o.w ? ` ${Math.round(o.w)}x${Math.round(o.h)}` : ''}`;

            // Bounding box collision detection
            const metrics = ctx.measureText(label);
            const textW = metrics.width;
            const textH = 14;
            const padding = 4;
            let currentLy = ly;
            let found = false;

            while (!found) {
                const rect = { x: lx - 2, y: currentLy - textH - padding, w: textW + 4, h: textH + padding };
                let collision = false;
                for (const existing of labelRects) {
                    if (rect.x < existing.x + existing.w &&
                        rect.x + rect.w > existing.x &&
                        rect.y < existing.y + existing.h &&
                        rect.y + rect.h > existing.y) {
                        collision = true;
                        break;
                    }
                }
                if (!collision) {
                    labelRects.push(rect);
                    found = true;
                    ly = currentLy;
                } else {
                    currentLy -= (textH + padding + 2); // Move up with a small gap
                }
            }

            ctx.fillStyle = "rgba(0,0,0,0.6)";
            ctx.fillRect(lx - 2, ly - textH - 4, textW + 4, textH + 4);

            ctx.fillStyle = "#fff";
            ctx.strokeText(label, lx, ly - 6);
            ctx.fillText(label, lx, ly - 6);
            ctx.restore();
        }

        // Scene WebSocket
        let viewWs = null;
        let reconnectDelay = 1000;
        const maxReconnectDelay = 10000;
        
        // Client-side perf telemetry (prints ~1Hz; avoid per-frame spam)
        let __wsMsgCount = 0;
        let __wsMsgBytes = 0;
        let __wsParseMsSum = 0;
        let __handleMsSum = 0;
        let __handleErr = 0;
        let __slowHandleCount = 0;
        // Main-thread health
        let __rafCount = 0;
        let __longTaskCount = 0;
        let __longTaskMs = 0;
        let __perfLastLog = (typeof performance !== "undefined" ? performance.now() : Date.now());
        let __perfLastWsCount = 0;
        const __isSafari = /^((?!chrome|android|crios|fxios|edg).)*safari/i.test(navigator.userAgent || "");
        let __lastSceneRenderAt = -Infinity;
        // Last time a full scene snapshot was applied (WebSocket or HTTP fallback).
        let __lastViewPayloadAt = -Infinity;
        let __httpSceneSnapshotInflight = false;

        // Track requestAnimationFrame rate (best-effort)
        (function __trackRaf() {
            function tick() {
                __rafCount++;
                requestAnimationFrame(tick);
            }
            try { requestAnimationFrame(tick); } catch (e) { }
        })();

        // Track Long Tasks (Chrome/Edge) - indicates main thread stalls.
        try {
            if (typeof PerformanceObserver !== "undefined") {
                const obs = new PerformanceObserver((list) => {
                    try {
                        for (const e of list.getEntries()) {
                            __longTaskCount++;
                            __longTaskMs += (e.duration || 0);
                        }
                    } catch (_) { }
                });
                obs.observe({ entryTypes: ["longtask"] });
            }
        } catch (e) { }

        function connectViewWS() {
            const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
            viewWs = new WebSocket(`${protocol}//${location.host}/view`);

            viewWs.onopen = () => {
                console.log("VIEW_WS connected");
                reconnectDelay = 1000; // Reset delay on successful connection
                // Phone connection state comes from snapshots; refresh from /status so the badge updates
                // even if the first snapshot is slow, dropped, or fails JSON.parse.
                fetch("/status")
                    .then((r) => r.json())
                    .then((d) => {
                        if (d && typeof d.connected === "boolean") {
                            updateConnectionStatus(d.connected, false);
                        }
                    })
                    .catch(() => { });
            };

            viewWs.onmessage = (e) => {
                try {
                    const t0 = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    __wsMsgCount++;
                    try { __wsMsgBytes += (e && e.data ? (String(e.data).length || 0) : 0); } catch (_) { }
                    const s = JSON.parse(e.data);
                    const t1 = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    __wsParseMsSum += (t1 - t0);
                    window.__lastSceneSnapshot = s;
                    const nowForThrottle = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    if (__isSafari && !s.isSeeking && (nowForThrottle - __lastSceneRenderAt) < 33) {
                        __lastViewPayloadAt = nowForThrottle;
                        return;
                    }
                    const t2 = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    try {
                        handleSnapshot(s);
                    } catch (err) {
                        __handleErr++;
                        // keep console signal minimal
                        console.warn("handleSnapshot error", err);
                    }
                    const t3 = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    const hms = (t3 - t2);
                    __handleMsSum += hms;
                    if (hms > 16.0) __slowHandleCount++;
                    __lastViewPayloadAt = (typeof performance !== "undefined" ? performance.now() : Date.now());

                    // 1Hz telemetry
                    const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    if ((now - __perfLastLog) >= 1000) {
                        const dt = (now - __perfLastLog) / 1000.0;
                        const wsHz = (__wsMsgCount - __perfLastWsCount) / Math.max(0.001, dt);
                        const msgs = Math.max(1, (__wsMsgCount - __perfLastWsCount));
                        const avgParse = __wsParseMsSum / msgs;
                        const avgHandle = __handleMsSum / msgs;
                        const textInFlight = (typeof __textFetchInFlight !== "undefined") ? __textFetchInFlight : -1;
                        const textQueued = (typeof __textFetchQueue !== "undefined" && __textFetchQueue && __textFetchQueue.length !== undefined) ? __textFetchQueue.length : -1;
                        const rafHz = __rafCount / Math.max(0.001, dt);
                        // Print as a single line (avoid collapsed objects hiding key fields)
                        let lagMs = null;
                        try {
                            if (s && s.serverTime) {
                                lagMs = Math.round(((Date.now() / 1000) - Number(s.serverTime)) * 1000);
                            }
                        } catch (e) { lagMs = null; }
                        const vis = (typeof document !== "undefined") ? (document.visibilityState || "unknown") : "unknown";
                        const hidden = (typeof document !== "undefined") ? (!!document.hidden) : false;
                        // (disabled) VIEW_CLIENT_PERF prints were useful for debugging, but are too noisy in normal use.
                        __perfLastLog = now;
                        __perfLastWsCount = __wsMsgCount;
                        __wsMsgBytes = 0;
                        __wsParseMsSum = 0;
                        __handleMsSum = 0;
                        __slowHandleCount = 0;
                        __handleErr = 0;
                        __rafCount = 0;
                        __longTaskCount = 0;
                        __longTaskMs = 0;
                    }
                } catch (err) {
                    console.error("VIEW_WS parse error", err);
                }
            };

            viewWs.onclose = (e) => {
                console.log(`VIEW_WS closed. Reconnecting in ${reconnectDelay}ms...`, e.reason);
                updateConnectionStatus(false, true); // Show "RECONNECTING" status
                setTimeout(connectViewWS, reconnectDelay);
                reconnectDelay = Math.min(reconnectDelay * 1.5, maxReconnectDelay);
            };

            viewWs.onerror = (err) => {
                console.error("VIEW_WS error:", err);
                viewWs.close(); // Force close on error to trigger reconnect
            };
        }

        function updateConnectionStatus(connected, isReconnecting = false) {
            if (statusBadge) {
                if (isReconnecting) {
                    statusBadge.textContent = "RECONNECTING...";
                    statusBadge.classList.remove("text-sim-accent", "text-red-400");
                    statusBadge.classList.add("text-yellow-500");
                } else {
                    statusBadge.textContent = connected ? "CONNECTED" : "DISCONNECTED";
                    statusBadge.classList.toggle("text-sim-accent", connected);
                    statusBadge.classList.toggle("text-red-400", !connected);
                    statusBadge.classList.remove("text-yellow-500");
                }
            }
            const statusDots = [
                document.getElementById("status-dot"),
                document.getElementById("viewport-status-dot"),
            ].filter(Boolean);
            statusDots.forEach((statusDot) => {
                if (isReconnecting) {
                    statusDot.classList.remove("bg-sim-accent", "bg-red-500", "is-connected");
                    statusDot.classList.add("bg-yellow-500");
                    statusDot.style.boxShadow = "0 0 14px rgba(234,179,8,0.6)";
                } else {
                    statusDot.classList.toggle("is-connected", connected);
                    statusDot.classList.remove("bg-sim-accent");
                    statusDot.classList.toggle("bg-red-500", !connected);
                    statusDot.classList.remove("bg-yellow-500");
                    statusDot.style.boxShadow = connected ? "0 0 14px rgba(52,242,122,0.8)" : "0 0 14px rgba(216,59,54,0.8)";
                }
            });
            if (viewportStatusBadge) {
                viewportStatusBadge.textContent = isReconnecting ? "RECONNECTING..." : (connected ? "CONNECTED" : "DISCONNECTED");
                viewportStatusBadge.classList.toggle("text-yellow-500", isReconnecting);
            }
            if (fpsBadge) fpsBadge.innerText = `${fps} FPS`;
            // if (viewportSpinner) viewportSpinner.classList.toggle("hidden", connected);
        }

        // Start initial connection
        connectViewWS();

        // HTTP fallback: if /view is down, blocked, or stops delivering frames, pull the same JSON via GET.
        setInterval(() => {
            try {
                const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
                const staleMs = 1800;
                const openOk = viewWs && viewWs.readyState === WebSocket.OPEN;
                if (openOk && (now - __lastViewPayloadAt) < staleMs) return;
                if (__httpSceneSnapshotInflight) return;
                __httpSceneSnapshotInflight = true;
                fetch("/api/scene_snapshot")
                    .then((r) => {
                        if (!r.ok) throw new Error(String(r.status));
                        return r.json();
                    })
                    .then((s) => {
                        if (!s || !s.stats) return;
                        window.__lastSceneSnapshot = s;
                        try {
                            const nowForThrottle = (typeof performance !== "undefined" ? performance.now() : Date.now());
                            if (__isSafari && !s.isSeeking && (nowForThrottle - __lastSceneRenderAt) < 33) {
                                __lastViewPayloadAt = nowForThrottle;
                                return;
                            }
                            handleSnapshot(s);
                        } catch (err) {
                            console.warn("handleSnapshot (HTTP fallback) error", err);
                        }
                        __lastViewPayloadAt = (typeof performance !== "undefined" ? performance.now() : Date.now());
                    })
                    .catch(() => { })
                    .finally(() => { __httpSceneSnapshotInflight = false; });
            } catch (_) { __httpSceneSnapshotInflight = false; }
        }, 800);

        // Keep phone connection badge in sync even when snapshot delivery is delayed.
        setInterval(() => {
            fetch("/status")
                .then((r) => r.json())
                .then((d) => {
                    if (d && typeof d.connected === "boolean") {
                        updateConnectionStatus(d.connected, false);
                    }
                })
                .catch(() => { });
        }, 1000);

        // Debug: track snapshot statistics for troubleshooting
        let __snapshotDebug = { received: 0, withObjects: 0, lastOrderLen: 0, lastObjCount: 0 };

        function handleSnapshot(s) {
            if (!s || !s.stats) return;
            __lastSceneRenderAt = (typeof performance !== "undefined" ? performance.now() : Date.now());

            // Update simulator version
            const simVersionEl = document.getElementById("simulator-version");
            if (simVersionEl && s.simulatorVersion) {
                simVersionEl.textContent = `Version ${s.simulatorVersion}`;
            }
            // Update reported FW version if changed (avoid clobbering user input mid-edit)
            const fwInput = document.getElementById("fw-version-input");
            if (fwInput && s.fwVersion && document.activeElement !== fwInput && !fwDirty) {
                if (fwInput.value !== s.fwVersion) fwInput.value = s.fwVersion;
            }

            // Debug: track snapshot reception
            __snapshotDebug.received++;
            __snapshotDebug.lastOrderLen = (s.order || []).length;
            __snapshotDebug.lastObjCount = Object.keys(s.objects || {}).length;
            if (__snapshotDebug.lastObjCount > 0) __snapshotDebug.withObjects++;
            // Log every ~120 snapshots (about 2 seconds at 60fps) if there are objects
            if (__snapshotDebug.received % 120 === 0 && __snapshotDebug.withObjects > 0) {
                console.log(`SNAPSHOT_DEBUG: received=${__snapshotDebug.received} withObjects=${__snapshotDebug.withObjects} lastOrder=${__snapshotDebug.lastOrderLen} lastObjs=${__snapshotDebug.lastObjCount}`);
            }

            // Calculate FPS
            const now = Date.now();
            const framesDelta = (s.stats?.frames || 0) - lastFrameCount;
            const timeDelta = (now - lastFrameTime) / 1000;
            if (timeDelta > 0.5) {
                fps = Math.round(framesDelta / timeDelta);
                lastFrameCount = s.stats?.frames || 0;
                lastFrameTime = now;
            }

            // If we're seeking, avoid heavy redraw. Only keep the replay UI in sync.
            if (s.isSeeking) {
                if (s.playbackIndex !== undefined) updateReplayUiOnly(s.playbackIndex, true);
                return;
            }

            clearCanvas();

            // Draw viewport first (as background)
            const vp = s.viewport || { x: 0, y: 0, w: 640, h: 400 };
            const vpX = vp.x;
            const vpY = vp.y;
            window.__viewVp = { x: Number(vpX) || 0, y: Number(vpY) || 0 };

            drawViewport(vp);

            // Show diagnostic info if no objects are present
            const objectCount = Object.keys(s.objects || {}).length;
            const orderLen = (s.order || []).length;
            drawDiagnosticInfo(s.stats?.connected, objectCount, orderLen);

            // Build per-frame lookup (used by UI selection & checkboxes)
            // IMPORTANT: checkbox lists must be stable; do NOT derive ID order from s.order
            // because focusing moves objects to end of z-order and would reshuffle the UI.
            const usedTextures = new Set();
            const usedFonts = new Set();
            const fullOrder = (s.order || []);
            for (const oid of fullOrder) {
                const o = s.objects ? s.objects[String(oid)] : null;
                if (!o || o.type === 1) continue;
                const ts = getTextureSlotFromObj(o);
                if (ts !== null && Number.isFinite(ts)) usedTextures.add(ts);
                const fs = Number(o.fontSlot ?? -1);
                if (Number.isFinite(fs) && fs >= 0 && o.text) usedFonts.add(fs);
            }

            // Stable IDs by type, ordered by ID descending (original order)
            const typeToIds = {};
            try {
                const allObjs = Object.values(s.objects || {}).filter(o => o && o.type !== 1);
                // Sort by ID descending (original addition order, top-most first by default)
                allObjs.sort((a, b) => Number(b.id) - Number(a.id));
                for (const o of allObjs) {
                    const tn = objTypeName(o.type);
                    if (!typeToIds[tn]) typeToIds[tn] = [];
                    typeToIds[tn].push(Number(o.id));
                }
            } catch (e) { console.error("Ordering error", e); }
            window.__typeToIds = typeToIds;
            window.__usedTextures = usedTextures;
            window.__usedFonts = usedFonts;

            // Compute CLIP regions (same semantics as Tk): CLIP objects come in start/end pairs.
            // Objects between start and end (exclusive) are clipped to the CLIP rect.
            const clipRegions = [];
            try {
                const stack = [];
                for (let i = 0; i < fullOrder.length; i++) {
                    const o = s.objects ? s.objects[String(fullOrder[i])] : null;
                    if (!o) continue;
                    if (o.type === 1) {
                        if (stack.length) {
                            const st = stack.pop();
                            clipRegions.push({ start: st.i, end: i, clip: st.o });
                        } else {
                            stack.push({ i, o });
                        }
                    }
                }
                // Handle unclosed clips (active until end of list)
                while (stack.length) {
                    const st = stack.pop();
                    clipRegions.push({ start: st.i, end: fullOrder.length, clip: st.o });
                }
            } catch (e) { }

            // Pre-render offscreen targets (USER_TEXTURE) into cached canvases for this snapshot.
            try {
                const userTextures = s.userTextures || {};
                const offOrders = s.offscreenOrders || {};
                if (!window.__offscreenCache) window.__offscreenCache = { canvases: new Map(), sigs: new Map() };

                const activeTargets = new Set();
                Object.values(userTextures).forEach(ut => {
                    try {
                        const t = Number(ut.target);
                        if (t >= 0 && t <= 3) activeTargets.add(t);
                    } catch (e) { }
                });

                const buildClipRegions = (orderList) => {
                    const clipRegions = [];
                    try {
                        const stack = [];
                        for (let i = 0; i < orderList.length; i++) {
                            const oid = orderList[i];
                            const o = s.objects ? s.objects[String(oid)] : null;
                            if (!o) continue;
                            if (o.type === 1) {
                                if (stack.length) {
                                    const st = stack.pop();
                                    clipRegions.push({ start: st.i, end: i, clip: st.o });
                                } else {
                                    stack.push({ i, o });
                                }
                            }
                        }
                        while (stack.length) {
                            const st = stack.pop();
                            clipRegions.push({ start: st.i, end: orderList.length, clip: st.o });
                        }
                    } catch (e) { }
                    return clipRegions;
                };

                activeTargets.forEach((t) => {
                    const orderList = offOrders[String(t)] || [];
                    const rootId = (0xFFF1 + Number(t));
                    const drawableOrder = orderList.filter((oid) => {
                        const idn = Number(oid);
                        if (idn === rootId) return false;
                        const o = s.objects ? s.objects[String(idn)] : null;
                        if (!o) return false;
                        // CLIP objects don't render pixels by themselves.
                        if (Number(o.type) === 1) return false;
                        return true;
                    });
                    // Pick any user texture for this target to get dimensions.
                    let ut = null;
                    for (const v of Object.values(userTextures)) {
                        if (Number(v.target) === Number(t)) { ut = v; break; }
                    }
                    if (!ut) return;
                    const w = Math.max(1, Number(ut.w || 1));
                    const h = Math.max(1, Number(ut.h || 1));
                    const transparent = !!ut.transparent;

                    // Ensure we don't keep stale pixels when a target stays allocated but its draw list is empty.
                    if (!drawableOrder || drawableOrder.length === 0) {
                        let emptyCanv = window.__offscreenCache.canvases.get(t);
                        if (!emptyCanv) {
                            emptyCanv = document.createElement("canvas");
                            window.__offscreenCache.canvases.set(t, emptyCanv);
                        }
                        emptyCanv.width = w;
                        emptyCanv.height = h;
                        const ec = emptyCanv.getContext("2d");
                        if (ec) {
                            ec.setTransform(1, 0, 0, 1, 0, 0);
                            ec.clearRect(0, 0, w, h);
                            if (!transparent) {
                                ec.fillStyle = "#282828";
                                ec.fillRect(0, 0, w, h);
                            }
                        }
                        window.__offscreenCache.sigs.set(t, JSON.stringify({
                            v: 4,
                            empty: true,
                            rootId,
                            drawableCount: 0,
                            w,
                            h,
                            transparent
                        }));
                        return;
                    }

                    // IMPORTANT: include texture load readiness in signature.
                    // Otherwise we may cache a black offscreen before images load and never redraw it.
                    const objsSig = orderList.map(oid => s.objects ? s.objects[String(oid)] : null);
                    const computeTextUrl = (o) => {
                        try {
                            if (!o || o.type !== 4 || !o.text) return null;
                            const fs = Number(o.fontSlot ?? -1);
                            if (!Number.isFinite(fs) || fs < 0 || fs === 255) return null;
                            const scale = (o.textScale && o.textScale > 0) ? o.textScale : 1.0;
                            const align = (o.textAlign !== undefined && o.textAlign !== null) ? o.textAlign : 0;
                            return `/text/${fs}?t=${encodeURIComponent(o.text)}&s=${encodeURIComponent(scale)}&a=${encodeURIComponent(align)}&c=${encodeURIComponent((o.color >>> 0))}`;
                        } catch (e) {
                            return null;
                        }
                    };
                    const isUrlReady = (url) => {
                        try {
                            const v = imgCache.get(url);
                            if (!v) return false;
                            // Texture URLs store Image directly.
                            if (v instanceof Image) return !!(v.complete && v.naturalWidth > 0 && v.naturalHeight > 0);
                            // Text URLs store {img, status, ...}
                            if (v.img) return !!(v.status === 'ready' && v.img.complete && v.img.naturalWidth > 0 && v.img.naturalHeight > 0);
                        } catch (e) { }
                        return false;
                    };
                    const imgReadySig = objsSig.map(o => {
                        if (!o) return null;
                        const entries = [];
                        if (o.textureUrl) entries.push([o.textureUrl, isUrlReady(o.textureUrl)]);
                        const textUrl = computeTextUrl(o);
                        if (textUrl) entries.push([textUrl, isUrlReady(textUrl)]);
                        return entries.length ? entries : null;
                    });
                    const forceOffscreenRedraw = !!(s.animators && s.animators.length > 0);
                    const sig = JSON.stringify({
                        v: 4,
                        order: orderList,
                        drawableOrder,
                        objs: objsSig,
                        imgReady: imgReadySig,
                        // Keep offscreen animation responsive even when order is unchanged.
                        animTick: forceOffscreenRedraw ? Number(s.stats?.frames || 0) : 0,
                        w, h, transparent
                    });
                    if (!forceOffscreenRedraw && window.__offscreenCache.sigs.get(t) === sig) return;

                    let canv = window.__offscreenCache.canvases.get(t);
                    if (!canv) {
                        canv = document.createElement("canvas");
                        window.__offscreenCache.canvases.set(t, canv);
                    }
                    canv.width = w;
                    canv.height = h;
                    const c2 = canv.getContext("2d");
                    if (!c2) return;

                    const clipRegions2 = buildClipRegions(orderList);

                    // IMPORTANT: emulate PIL padding for strokes so outlines/gradients don't get clipped at edges.
                    let maxPen = 1;
                    for (let i = 0; i < orderList.length; i++) {
                        const oid = Number(orderList[i]);
                        if (oid === (0xFFF1 + Number(t))) continue;
                        const o = s.objects ? s.objects[String(oid)] : null;
                        if (!o || o.type === 1) continue;
                        const ps = Number(o.penSize || 1);
                        if (Number.isFinite(ps)) maxPen = Math.max(maxPen, ps);
                    }
                    const pad = Math.max(1, Math.ceil(maxPen));

                    const tmp = document.createElement("canvas");
                    tmp.width = w + pad * 2;
                    tmp.height = h + pad * 2;
                    const tc = tmp.getContext("2d");
                    if (!tc) return;

                    // Temp background
                    tc.setTransform(1, 0, 0, 1, 0, 0);
                    tc.clearRect(0, 0, tmp.width, tmp.height);
                    if (!transparent) {
                        tc.fillStyle = "#282828";
                        tc.fillRect(0, 0, tmp.width, tmp.height);
                    }

                    tc.save();
                    tc.translate(pad, pad);
                    window.__offscreenPass = true;
                    if (clipEnabled) {
                        // Expanded clip to include padding (so strokes at edges are preserved)
                        try {
                            tc.beginPath();
                            tc.rect(-pad, -pad, w + pad * 2, h + pad * 2);
                            tc.clip();
                        } catch (e) { }
                    }
                    for (let i = 0; i < orderList.length; i++) {
                        const oid = Number(orderList[i]);
                        if (oid === (0xFFF1 + Number(t))) continue; // offscreen root
                        const o = s.objects ? s.objects[String(oid)] : null;
                        if (!o || o.type === 1) continue;

                        tc.save();
                        if (clipEnabled && clipRegions2.length) {
                            for (const r of clipRegions2) {
                                if (r.start < i && i < r.end) {
                                    const c = r.clip;
                                    const rr = c.rx || 0;
                                    tc.beginPath();
                                    if (rr > 0 && typeof tc.roundRect === "function") tc.roundRect(c.x, c.y, c.w, c.h, rr);
                                    else tc.rect(c.x, c.y, c.w, c.h);
                                    tc.clip();
                                }
                            }
                        }
                        drawObj(o, tc);
                        tc.restore();
                    }
                    window.__offscreenPass = false;
                    tc.restore(); // translate(pad,pad)

                    // Copy padded temp back into fixed-size offscreen canvas
                    c2.setTransform(1, 0, 0, 1, 0, 0);
                    c2.clearRect(0, 0, w, h);
                    c2.drawImage(tmp, -pad, -pad);

                    window.__offscreenCache.sigs.set(t, sig);
                });
            } catch (e) { }

            // Draw objects (offset by viewport origin, just like desktop).
            // When clipping is enabled:
            // - Implicit clip to the SESSION viewport (the grey screen), updated every frame.
            // - Optional nested clip regions from CLIP objects.
            ctx.save();
            ctx.translate(vpX, vpY); // vpX/vpY already include inset
            // Match glasses stroke caps globally too.
            ctx.lineJoin = "miter";
            ctx.lineCap = "butt";
            if (clipEnabled) {
                try {
                    ctx.beginPath();
                    // Clip to full session viewport content area
                    const vw = (vp.w || 640);
                    const vh = (vp.h || 400);
                    if (vw > 0 && vh > 0) ctx.rect(0, 0, vw, vh);
                    ctx.clip();
                } catch (e) { }
            }
            // Focus behavior: checked objects should render on TOP without mutating draw-order.
            // We skip focused objects in the main pass and draw them last in their original order.
            const focusedOidsInOrder = [];
            const focusedSet = selectedObjIds || new Set();

            for (let i = 0; i < fullOrder.length; i++) {
                const oid = fullOrder[i];
                const o = s.objects ? s.objects[String(oid)] : null;
                if (!o || o.type === 1) continue; // CLIP objects don't render

                if (focusedSet.has(Number(o.id))) {
                    focusedOidsInOrder.push({ i, o });
                    continue;
                }

                if (punchTransparentBlackMask(o, clipRegions, i, vp)) continue;

                ctx.save();
                if (clipEnabled && clipRegions.length) {
                    for (const r of clipRegions) {
                        if (r.start < i && i < r.end) {
                            const c = r.clip;
                            const rr = c.rx || 0;
                            ctx.beginPath();
                            if (rr > 0 && typeof ctx.roundRect === "function") ctx.roundRect(c.x, c.y, c.w, c.h, rr);
                            else ctx.rect(c.x, c.y, c.w, c.h);
                            ctx.clip();
                            // Do not break; multiple nested clips should intersect
                        }
                    }
                }
                drawObj(o);
                if (selectedObjIds.has(Number(o.id))) drawSelectionOutline(o);
                ctx.restore();
            }

            // Draw focused objects last (on top), preserving their original clip regions.
            for (const it of focusedOidsInOrder) {
                const i = it.i;
                const o = it.o;
                if (punchTransparentBlackMask(o, clipRegions, i, vp)) continue;
                ctx.save();
                if (clipEnabled && clipRegions.length) {
                    for (const r of clipRegions) {
                        if (r.start < i && i < r.end) {
                            const c = r.clip;
                            const rr = c.rx || 0;
                            ctx.beginPath();
                            if (rr > 0 && typeof ctx.roundRect === "function") ctx.roundRect(c.x, c.y, c.w, c.h, rr);
                            else ctx.rect(c.x, c.y, c.w, c.h);
                            ctx.clip();
                        }
                    }
                }
                drawObj(o);
                if (selectedObjIds.has(Number(o.id))) drawSelectionOutline(o);
                ctx.restore();
            }
            ctx.restore(); // Restore viewport clip/translation

            // Draw debug params on top of everything, UNCLIPPED
            if (dbgParams) {
                const labelRects = [];
                for (let i = 0; i < fullOrder.length; i++) {
                    const o = s.objects ? s.objects[String(fullOrder[i])] : null;
                    if (!o || o.type === 1) continue;
                    drawDbgParams(o, labelRects);
                }
            }

            // --- TIMER & REPLAY UI UPDATES ---
            const delay = s.playbackNextDelay || 0;
            const nextTs = s.playbackNextTs || 0;
            const nextIdx = (s.playbackIndex !== undefined) ? s.playbackIndex + 1 : -1;

            // Sync clocks: Calculate offset between server and client time
            if (s.serverTime) {
                window.__serverTimeOffset = s.serverTime - (Date.now() / 1000);
            }
            const timeOffset = window.__serverTimeOffset || 0;

            if (s.playbackIndex !== undefined) {
                updateReplayUiOnly(s.playbackIndex, !!s.playbackPaused);
            }

            // --- TIMER & CONTROLS UPDATE ---
            // The global timer is now secondary; we prefer the row-level timers in the replay list.
            const globalCountdownEl = document.getElementById("replay-countdown");

            if (globalCountdownEl) {
                if (delay > 0.2 && !s.playbackPaused) {
                    globalCountdownEl.classList.remove("hidden");
                } else {
                    globalCountdownEl.classList.add("hidden");
                }
            }

            // Update Play/Pause button visual state
            const btnPlay = document.getElementById("btn-replay-play");
            const btnPause = document.getElementById("btn-replay-pause");
            if (btnPlay && btnPause) {
                if (s.playbackPaused) {
                    // Show Pause as active
                    btnPlay.classList.replace("bg-sim-accent", "bg-white/10");
                    btnPlay.classList.replace("text-black", "text-white");

                    btnPause.classList.replace("bg-white/10", "bg-sim-accent");
                    btnPause.classList.replace("text-white", "text-black");
                } else {
                    // Show Play as active
                    btnPlay.classList.replace("bg-white/10", "bg-sim-accent");
                    btnPlay.classList.replace("text-white", "text-black");

                    btnPause.classList.replace("bg-sim-accent", "bg-white/10");
                    btnPause.classList.replace("text-black", "text-white");
                }
            }

            const totalEl = document.getElementById("replay-step-total");
            if (totalEl && s.totalCommands !== undefined) totalEl.textContent = s.totalCommands;



            // Sync Speed Slider (if not being touched)
            const speedSlider = document.getElementById("replay-speed-slider");
            if (speedSlider && document.activeElement !== speedSlider && s.playbackSpeed !== undefined) {
                speedSlider.value = s.playbackSpeed;
                const speedText = document.getElementById("replay-speed-text");
                if (speedText) speedText.textContent = Number(s.playbackSpeed).toFixed(1) + "x";
            }


            // Update header status badge
            if (s.stats) {
                // Sync clipping enabled state from server
                if (s.stats.clippingEnabled !== undefined) {
                    const serverClip = !!s.stats.clippingEnabled;
                    if (serverClip !== clipEnabled) {
                        clipEnabled = serverClip;
                        const cb = document.getElementById("clip-enabled");
                        if (cb) cb.checked = clipEnabled;
                    }
                }

                const connected = !!s.stats.connected;
                const activeLog = s.activeLog;
                updateConnectionStatus(connected, false);



                updateRecordingUI(!!s.stats.isRecording);

                // Update filename display if it changed
                if (activeLog && !document.getElementById('replay-filename').textContent.includes(activeLog)) {
                    document.getElementById('replay-filename').textContent = activeLog;
                }

                // Brightness (read-only, controlled by client)
                try {
                    window.__sim_brightness = Number(s.stats.brightness ?? 150);
                } catch (e) {
                    window.__sim_brightness = 150;
                }
                try {
                    window.__sim_battery = Number(s.stats.battery ?? 100);
                } catch (e) {
                    window.__sim_battery = 100;
                }
                try {
                    window.__sim_charging = !!(s.stats.batteryCharging ?? false);
                } catch (e) {
                    window.__sim_charging = false;
                }
                try {
                    window.__sim_ambient_lux = Number(s.stats.ambientLux ?? 150);
                } catch (e) {
                    window.__sim_ambient_lux = 150;
                }
                try {
                    window.__sim_auto_brightness = !!(s.stats.autoBrightnessEnabled ?? false);
                } catch (e) {
                    window.__sim_auto_brightness = false;
                }
                try {
                    window.__sim_powered_on = !!(s.stats.poweredOn ?? true);
                } catch (e) {
                    window.__sim_powered_on = true;
                }
                try { setPowerUi(!!window.__sim_powered_on); } catch (e) { }
                const brightnessValue = Math.max(0, Math.min(255, Math.round(window.__sim_brightness)));
                const brightnessPct = Math.max(0, Math.min(100, (brightnessValue / 255.0) * 100));
                if (brightnessPercent) brightnessPercent.textContent = `${Math.round(brightnessPct)}%`;
                if (brightnessBar) brightnessBar.style.width = `${brightnessPct.toFixed(1)}%`;
                if (brightnessOverlay) {
                    // Brightness is already applied per-pixel in the canvas renderer (see __sim_brightness usage).
                    // A full-viewport black overlay would double-dim AND turn empty/transparent canvas areas black,
                    // which is wrong for glasses (empty pixels must remain see-through). Keep overlay invisible.
                    brightnessOverlay.style.opacity = "0";
                }
                const ambientValue = Math.max(0, Math.min(10000, Math.round(window.__sim_ambient_lux ?? 150)));
                if (ambientLuxVal) ambientLuxVal.textContent = `${ambientValue} lx`;
                if (ambientLux && !ambientLux.dataset.dragging) ambientLux.value = String(ambientValue);
                if (autoBrightnessMode) {
                    const on = !!window.__sim_auto_brightness;
                    autoBrightnessMode.textContent = `AUTO BRIGHTNESS: ${on ? "ON" : "OFF"}`;
                    autoBrightnessMode.classList.toggle("text-sim-accent", on);
                    autoBrightnessMode.classList.toggle("text-zinc-500", !on);
                }

                // Battery Update
                const batteryValue = Math.max(0, Math.min(100, Math.round(window.__sim_battery ?? 100)));
                if (batteryPercent) batteryPercent.textContent = `${batteryValue}%`;
                if (batterySlider && document.activeElement !== batterySlider) batterySlider.value = batteryValue;
                if (batteryCharging && document.activeElement !== batteryCharging) batteryCharging.checked = !!window.__sim_charging;

                // Update metrics
                statFrames.textContent = fps || 0;
                fpsBadge.textContent = `${fps} FPS`;

                const totalObjs = Object.values(s.stats.objects || {}).reduce((sum, v) => sum + v, 0);
                objCount.textContent = totalObjs;
                animCount.textContent = (s.animators || []).length;
                if (objTypesBadge) {
                    const typesN = Object.keys(s.stats.objects || {}).length;
                    objTypesBadge.textContent = `${typesN} / ${totalObjs}`;
                    objTypesBadge.title = "types / total objects";
                }
                if (animatorsBadge) {
                    animatorsBadge.textContent = `${(s.animators || []).length}`;
                }

                // --- EXPENSIVE UI UPDATES (THROTTLED) ---
                const uiDiff = now - lastUiUpdateTime;
                if (window.__uiDirty || uiDiff >= UI_UPDATE_INTERVAL) {
                    window.__uiDirty = false;
                    lastUiUpdateTime = now;

                    // Object types grid (click type -> show ids; check id -> bring to front + highlight)
                    let invHtml = "";
                    const objTypes = typeToIds; // Use the freshly calculated ones in current frame
                    const searchQuery = (document.getElementById('obj-search')?.value || "").toLowerCase();

                    if (Object.keys(objTypes).length > 0) {
                        let filteredCount = 0;
                        for (const [k, ids] of Object.entries(objTypes)) {
                            const v = ids.length;

                            // Filter logic: match type name OR any of the IDs
                            const typeMatches = k.toLowerCase().includes(searchQuery);
                            const idMatches = ids.some(id => String(id).includes(searchQuery));

                            if (searchQuery && !typeMatches && !idMatches) continue;
                            filteredCount++;

                            const isOpen = (window.__expandedObjType === k);
                            const chevron = `
                            <svg class="accordion-chevron" style="width:16px; height:16px; opacity:0.75; transition: transform 0.2s ease; transform:${isOpen ? "rotate(180deg)" : "rotate(0deg)"};" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <path d="M6 9l6 6 6-6"></path>
                            </svg>`;

                            // Build expanded items list
                            let idsHtml = "";
                            if (isOpen && ids && ids.length > 0) {
                                console.log(`Rendering expanded list for ${k} with ${ids.length} items.`);
                                const itemsHtml = ids.map(id => {
                                    try {
                                        // Skip if filtered by search
                                        if (searchQuery && !typeMatches && !String(id).includes(searchQuery)) {
                                            return "";
                                        }
                                        const checked = selectedObjIds.has(Number(id)) ? "checked" : "";
                                        const o = s.objects ? s.objects[String(id)] : null;
                                        if (!o) {
                                            console.warn(`Object ID ${id} listed in types but missing from snapshot objects!`);
                                            return `<div style="color:rgba(255,255,255,0.2); font-size:10px; padding: 4px; border: 1px dashed rgba(255,255,255,0.1); border-radius: 4px;">ID ${id} (not in current snapshot)</div>`;
                                        }
                                        const paramsHtml = renderObjectParams(o);
                                        return `
                                        <div style="display:flex; flex-direction:column;">
                                            <label style="display:flex; align-items:center; justify-content:space-between; gap:10px; cursor:pointer;">
                                                <span style="display:flex; align-items:center; gap:8px;">
                                                    <input type="checkbox" data-obj-id="${id}" ${checked} />
                                                    <span style="color:rgba(229,231,235,0.88); pointer-events:none;">ID ${id}</span>
                                                </span>
                                            </label>
                                            ${paramsHtml}
                                        </div>`;
                                    } catch (err) {
                                        console.error(`Error rendering object ${id}:`, err);
                                        return "";
                                    }
                                }).filter(html => html !== "").join("");

                                if (itemsHtml) {
                                    idsHtml = `<div style="margin-top:10px; padding-left:10px; display:flex; flex-direction:column; gap:6px;">${itemsHtml}</div>`;
                                } else {
                                    idsHtml = `<div style="margin-top:10px; padding-left:10px; font-size:10px; color:rgba(255,255,255,0.3); font-style:italic;">(no matching objects)</div>`;
                                }
                            }
                            invHtml += `
                            <div class="info-item" data-obj-type="${k}" style="display:flex; flex-direction:column; gap:6px; border-bottom: 1px solid rgba(255,255,255,0.03); padding-bottom: 4px;">
                                <div class="info-item-header" style="display:flex; align-items:baseline; justify-content:space-between; gap:12px; cursor:pointer; user-select:none; padding: 4px 0;">
                                    <div class="info-item-title" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${k}</div>
                                    <div style="display:flex; align-items:center; gap:10px;">
                                        <div class="info-item-value" style="white-space:nowrap;">${v}</div>
                                        ${chevron}
                                    </div>
                                </div>
                                ${idsHtml}
                            </div>`;
                        }
                        const sig = JSON.stringify([Object.keys(objTypes), window.__expandedObjType, searchQuery, Array.from(selectedObjIds).sort()]);
                        if (filteredCount === 0) {
                            if (objStatsDomSig !== "empty") {
                                objStats.innerHTML = '<div class="info-empty" style="grid-column: 1/-1;">(no matches)</div>';
                                objStatsDomSig = "empty";
                            }
                        } else if (objStatsDomSig !== sig) {
                            objStats.innerHTML = invHtml;
                            objStatsDomSig = sig;
                        }

                        if (drawOrderList) {
                            if (fullOrder.length <= 1) { // 1 is root screen
                                if (drawOrderDomSig !== "empty") {
                                    drawOrderList.innerHTML = `<div class="info-empty text-[9px] opacity-40">(none)</div>`;
                                    drawOrderDomSig = "empty";
                                }
                            } else {
                                const pillForOid = (oid) => {
                                    if (oid === 0xFFF0) return ""; // root screen
                                    const o = s.objects ? s.objects[String(oid)] : null;
                                    if (!o) return "";
                                    const t = objTypeName(o.type);
                                    const abbrev = t ? t[0].toUpperCase() : "?";
                                    const isSelected = selectedObjIds.has(Number(oid));
                                    const activeClass = isSelected 
                                        ? "bg-sim-accent hover:bg-sim-accent text-black font-semibold ring-2 ring-white/20" 
                                        : "bg-white/10 text-zinc-400 hover:bg-white/20";
                                    return `
                                        <div data-obj-id="${oid}" class="draw-order-pill flex-none px-1.5 py-0.5 rounded text-[10px] cursor-pointer transition-colors ${activeClass}" title="${t} ID ${oid}">
                                            <span class="opacity-60 mr-0.5">${abbrev}</span>${oid}
                                        </div>
                                    `;
                                };

                                // Main screen order
                                let orderHtml = fullOrder.map(pillForOid).join("");

                                // Offscreen orders (only if active)
                                const offOrders = s.offscreenOrders || {};
                                const hasOff = offOrders && Object.keys(offOrders).length > 0;
                                if (hasOff) {
                                    const keys = Object.keys(offOrders).map(x => Number(x)).filter(x => Number.isFinite(x)).sort((a, b) => a - b);
                                    for (const t of keys) {
                                        const olist = offOrders[String(t)] || [];
                                        if (!olist || olist.length <= 1) continue;
                                        const rootId = 0xFFF1 + Number(t);
                                        const isExpanded = window.__expandedOffscreens.has(t);
                                        orderHtml += `
                                            <div class="w-full h-px bg-white/10 my-1"></div>
                                            <div class="w-full flex items-center justify-between gap-2 px-1">
                                                <div class="text-[9px] font-semibold uppercase tracking-wider text-zinc-500">Offscreen ${t}</div>
                                                <div class="flex gap-1">
                                                    <button onclick="toggleOffscreenList(${t}); event.stopPropagation();" id="offscreen-toggle-${t}" class="px-2 py-1 rounded bg-white/5 border border-white/10 hover:bg-white/10 text-[9px] font-semibold uppercase tracking-widest text-zinc-400 hover:text-white transition-colors">
                                                        ${isExpanded ? '▲' : '▼'}
                                                    </button>
                                                    <button onclick="openOffscreenPopup(${t})" class="px-2 py-1 rounded bg-white/5 border border-white/10 hover:bg-white/10 text-[9px] font-semibold uppercase tracking-widest text-zinc-400 hover:text-white transition-colors">
                                                        Open
                                                    </button>
                                                </div>
                                            </div>
                                        `;
                                        // Use global expansion state to prevent immediate closing during snapshot updates
                                        orderHtml += `<div id="offscreen-list-${t}" class="offscreen-list-content" style="display:${isExpanded ? 'flex' : 'none'}; flex-wrap:wrap; gap:6px; margin-top:6px;">`;
                                        orderHtml += olist.map(oid => {
                                            if (Number(oid) === rootId) return "";
                                            return pillForOid(oid);
                                        }).join("");
                                        orderHtml += `</div>`;
                                    }
                                }

                                // Create signature to avoid unnecessary DOM updates (prevents hover flashing)
                                const drawOrderSig = JSON.stringify([fullOrder, Object.keys(offOrders || {}), Array.from(window.__expandedOffscreens || []), Array.from(selectedObjIds).sort()]);
                                if (drawOrderDomSig !== drawOrderSig) {
                                    drawOrderList.innerHTML = orderHtml;
                                    drawOrderDomSig = drawOrderSig;

                                    // Auto-scroll to LAST CHECKED item in draw order list
                                    if (lastCheckedObjId !== null && selectedObjIds.has(lastCheckedObjId) && window.__lastScrolledOid !== lastCheckedObjId) {
                                        const pill = drawOrderList.querySelector(`.draw-order-pill[data-obj-id="${lastCheckedObjId}"]`);
                                        if (pill) {
                                            // Scroll the selected pill into view horizontally
                                            pill.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'center' });
                                            window.__lastScrolledOid = lastCheckedObjId;
                                        }
                                    }

                                    // Setup click handlers for the pills
                                    drawOrderList.querySelectorAll('.draw-order-pill').forEach(pill => {
                                        pill.onclick = (e) => {
                                            const oid = Number(pill.getAttribute('data-obj-id'));
                                            // Toggle selection (like checkbox behavior)
                                            if (selectedObjIds.has(oid)) {
                                                selectedObjIds.delete(oid);
                                                if (lastCheckedObjId === oid) lastCheckedObjId = null;
                                            } else {
                                                selectedObjIds.add(oid);
                                                lastCheckedObjId = oid;
                                            }
                                            if (window.__lastSceneSnapshot) handleSnapshot(window.__lastSceneSnapshot);
                                        };
                                    });
                                }
                            }
                        }
                    } else {
                        objStats.innerHTML = '<div class="info-empty" style="grid-column: 1/-1;">(none)</div>';
                    }

                    // Update Images
                    if (s.images && s.images.length > 0) {
                        let cachedCol = "";
                        let activeCol = "";
                        let cachedBytes = 0;
                        let activeBytes = 0;
                        let npBytes = 0;
                        let cachedCount = 0;
                        let activeCount = 0;
                        let npCount = 0;
                        const usedTextures = (window.__usedTextures && typeof window.__usedTextures.has === "function") ? window.__usedTextures : new Set();
                        s.images.forEach(img => {
                            const canFocus = (img.type === "active" || img.type === "user");
                            let chk = "";
                            if (canFocus) {
                                // checked if all objects using this texture slot are selected
                                const ids = objectsUsingTexture(Number(img.slot), s);
                                const allSel = ids.length > 0 && ids.every(id => selectedObjIds.has(Number(id)));
                                chk = `<input type="checkbox" data-focus-tex="${img.slot}" ${allSel ? "checked" : ""} /> `;
                            }
                            if (img.type === "cached") {
                                cachedCol += `<div data-preview-kind="image" data-preview-slot="${img.slot}" data-preview-size="${img.size || 0}" style="margin-bottom:4px; cursor:pointer;">💾🖼️ Slot ${img.slot}: ${img.size}B</div>`;
                                cachedCount += 1;
                                cachedBytes += (img.size || 0);
                            } else if (img.type === "np") {
                                cachedCol += `<div style="margin-bottom:4px;">Non‑Persistent ⏳: ${img.size}B</div>`;
                                npCount += 1;
                                npBytes += (img.size || 0);
                            } else {
                                // Active column gets the checkbox (so it's visually aligned under Active).
                                activeCol += `<div style="margin-bottom:4px;"><label style="display:flex; align-items:center; gap:8px; cursor:pointer;">${chk}<span style="pointer-events:none;">✓ Active ${img.slot}: ${img.digest} ${img.size ? `(${img.size}B)` : ""}</span></label></div>`;
                                activeCount += 1;
                                activeBytes += (img.size || 0);
                            }
                        });
                        const imgHtml = `
                            <div class="grid grid-cols-2 gap-x-6">
                                <div>
                                    ${cachedCol || '<div class="info-empty">(none)</div>'}
                                </div>
                                <div>
                                    ${activeCol || '<div class="info-empty">(none)</div>'}
                                </div>
                            </div>
                        `;
                        const sig = JSON.stringify([(s.images || []).map(i => [i.slot, i.digest, i.type]), Array.from(selectedObjIds).sort()]);
                        if (imagesDomSig !== sig) {
                            imagesList.innerHTML = imgHtml;
                            imagesDomSig = sig;
                        }
                        // Do NOT double-count active slots into totals (active bytes are duplicates of cached bytes).
                        if (imagesBadge) {
                            imagesBadge.innerHTML =
                                `<span class="text-zinc-500">cached</span> ${cachedCount} <span class="text-zinc-500">${fmtBytes(cachedBytes + npBytes)}</span>` +
                                ` <span class="opacity-30 mx-1">|</span> ` +
                                `<span class="text-zinc-500">active</span> ${activeCount} <span class="text-zinc-500">${fmtBytes(activeBytes)}</span>`;
                        }
                        if (nd2AssetsImagesSummary) {
                            nd2AssetsImagesSummary.textContent = `${cachedCount + npCount} cached / ${fmtBytes(cachedBytes + npBytes)} | ${activeCount} active`;
                        }
                    } else {
                        if (imagesDomSig !== "empty") {
                            imagesList.innerHTML = '<div class="info-empty">(none)</div>';
                            imagesDomSig = "empty";
                        }
                        if (imagesBadge) imagesBadge.textContent = "";
                        if (nd2AssetsImagesSummary) nd2AssetsImagesSummary.textContent = "0";
                        cachedBytes = 0; activeBytes = 0; npBytes = 0;
                        cachedCount = 0; activeCount = 0; npCount = 0;
                    }

                    // Update Fonts
                    if (s.fonts && s.fonts.length > 0) {
                        let cachedCol = "";
                        let activeCol = "";
                        let cachedBytes = 0;
                        let activeBytes = 0;
                        let cachedCount = 0;
                        let activeCount = 0;
                        const usedFonts = (window.__usedFonts && typeof window.__usedFonts.has === "function") ? window.__usedFonts : new Set();
                        s.fonts.forEach(f => {
                            const canFocus = (f.type === "active");
                            let chk = "";
                            if (canFocus) {
                                const ids = objectsUsingFont(Number(f.slot), s);
                                const allSel = ids.length > 0 && ids.every(id => selectedObjIds.has(Number(id)));
                                chk = `<input type="checkbox" data-focus-font="${f.slot}" ${allSel ? "checked" : ""} /> `;
                            }
                            if (f.type === "cached") {
                                cachedCol += `<div data-preview-kind="font" data-preview-slot="${f.slot}" data-preview-size="${f.size || 0}" style="margin-bottom:4px; cursor:pointer;">💾🔤 Slot ${f.slot}: ${f.size || 0}B</div>`;
                                cachedCount += 1;
                                cachedBytes += (f.size || 0);
                            } else {
                                // Active column gets the checkbox (so it's visually aligned under Active).
                                activeCol += `<div style="margin-bottom:4px;"><label style="display:flex; align-items:center; gap:8px; cursor:pointer;">${chk}<span style="pointer-events:none;">✓ Active ${f.slot}: ${f.size || 0}B</span></label></div>`;
                                activeCount += 1;
                                activeBytes += (f.size || 0);
                            }
                        });
                        const fontHtml = `
                            <div class="grid grid-cols-2 gap-x-6">
                                <div>
                                    ${cachedCol || '<div class="info-empty">(none)</div>'}
                                </div>
                                <div>
                                    ${activeCol || '<div class="info-empty">(none)</div>'}
                                </div>
                            </div>
                        `;
                        const sig = JSON.stringify([(s.fonts || []).map(f => [f.slot, f.size, f.type]), Array.from(selectedObjIds).sort()]);
                        if (fontsDomSig !== sig) {
                            fontsList.innerHTML = fontHtml;
                            fontsDomSig = sig;
                        }
                        // Do NOT double-count active slots into totals (active bytes are duplicates of cached bytes).
                        if (fontsBadge) {
                            fontsBadge.innerHTML =
                                `<span class="text-zinc-500">cached</span> ${cachedCount} <span class="text-zinc-500">${fmtBytes(cachedBytes)}</span>` +
                                ` <span class="opacity-30 mx-1">|</span> ` +
                                `<span class="text-zinc-500">active</span> ${activeCount} <span class="text-zinc-500">${fmtBytes(activeBytes)}</span>`;
                        }
                        if (nd2AssetsFontsSummary) {
                            nd2AssetsFontsSummary.textContent = `${cachedCount} cached / ${fmtBytes(cachedBytes)} | ${activeCount} active`;
                        }
                    } else {
                        if (fontsDomSig !== "empty") {
                            fontsList.innerHTML = '<div class="info-empty">(none)</div>';
                            fontsDomSig = "empty";
                        }
                        if (fontsBadge) fontsBadge.textContent = "";
                        if (nd2AssetsFontsSummary) nd2AssetsFontsSummary.textContent = "0";
                    }

                    // Asset Cache badge (X): show *cached* total bytes (unique), not active duplicates.
                    try {
                        if (cacheBadge) {
                            let imgCachedB = 0, fontCachedB = 0, npB = 0;
                            let imgCachedN = 0, fontCachedN = 0;
                            if (s.images && Array.isArray(s.images)) {
                                for (const it of s.images) {
                                    if (!it) continue;
                                    if (it.type === "cached") { imgCachedN++; imgCachedB += (it.size || 0); }
                                    else if (it.type === "np") { npB += (it.size || 0); }
                                }
                            }
                            if (s.fonts && Array.isArray(s.fonts)) {
                                for (const it of s.fonts) {
                                    if (!it) continue;
                                    if (it.type === "cached") { fontCachedN++; fontCachedB += (it.size || 0); }
                                }
                            }
                            const totalB = imgCachedB + fontCachedB + npB;
                            const totalN = imgCachedN + fontCachedN;
                            cacheBadge.textContent = `${totalN} / ${fmtBytes(totalB)}`;
                            cacheBadge.title = "Cached assets count / total cached bytes (active slots not included)";
                        }
                    } catch (e) { }

                    // Update Animators
                    if (s.animators && s.animators.length > 0) {
                        const ANIM_NAMES = {
                            1: "X", 2: "Y", 3: "W", 4: "H", 5: "ALPHA", 6: "R", 7: "G", 8: "B",
                            9: "RX", 10: "RY", 11: "A0", 12: "A1", 13: "SCALE", 14: "SPRITE",
                            28: "G_ALPHA", 29: "G_RED", 30: "G_GREEN", 31: "G_BLUE",
                            32: "G_SIZE", 33: "G_ANGLE", 34: "PEN_SIZE", 24: "MAT_RZ"
                        };
                        let animHtml = "";
                        s.animators.forEach(a => {
                            const tname = ANIM_NAMES[a.type] || `#${a.type}`;
                            const dir = a.direction === 1 ? "→" : "←";
                            const flags = [];
                            if (a.flags & 0x02) flags.push("R");
                            if (a.flags & 0x04) flags.push("B");
                            const flagStr = flags.length > 0 ? ` <span style="color:var(--accent-yellow)">${flags.join("")}</span>` : "";
                            animHtml += `
                        <div class="animator-item">
                            <div class="line1">A${String(a.id).padStart(3, '0')} <span style="color:var(--accent-blue)">${tname}</span> ${dir} obj=${a.obj}${flagStr}</div>
                            <div class="line2">Δ=${a.delta >= 0 ? '+' : ''}${a.delta.toFixed(2)} lim=${a.limit.toFixed(1)}</div>
                        </div>`;
                        });
                        animatorsList.innerHTML = animHtml;
                    } else {
                        animatorsList.innerHTML = '<div class="info-empty">(none)</div>';
                    }

                    // Update Uploads (progress) with sticky history per slot
                    const uploads = s.uploads || [];
                    const cacheMax = s.cacheMax || (48 * 1024 - 16);
                    const nowTs = Date.now();
                    const cachedImageSlots = new Set((s.images || []).filter(it => it && it.type === "cached").map(it => Number(it.slot)));
                    const activeImageSlots = new Set((s.images || []).filter(it => it && it.type === "active").map(it => Number(it.slot)));
                    const cachedFontSlots = new Set((s.fonts || []).filter(it => it && it.type === "cached").map(it => Number(it.slot)));
                    const activeFontSlots = new Set((s.fonts || []).filter(it => it && it.type === "active").map(it => Number(it.slot)));
                    const currentSnapshotKeys = new Set();
                    uploads.forEach(u => {
                        const slotNum = Number(u.slot ?? 0);
                        const typNum = Number(u.type ?? 0);

                        // If a new upload starts, prune older history for that same (type:slot)
                        const isNew = u.recv === 0 || (u.started && !uploadHistory.has(`${typNum}:${slotNum}:${u.started}`));
                        if (isNew) {
                            for (const key of uploadHistory.keys()) {
                                if (key.startsWith(`${typNum}:${slotNum}:`) || key === `${typNum}:${slotNum}`) {
                                    uploadHistory.delete(key);
                                }
                            }
                        }

                        // Slot 127 is mem-only and reused; use started timestamp to avoid overwriting completed entries.
                        const key = (slotNum === 127 && u.started) ? `${typNum}:${slotNum}:${u.started}` : `${typNum}:${slotNum}`;
                        currentSnapshotKeys.add(key);
                        const existing = uploadHistory.get(key);
                        const recvN = Number(u.recv || 0);
                        const totalN = (u.total !== null && u.total !== undefined) ? Number(u.total) : null;
                        const doneN = !!u.done;
                        const startedN = Number(u.started || 0);
                        const unchanged = !!(existing &&
                            existing.slot === slotNum &&
                            existing.type === typNum &&
                            existing.recv === recvN &&
                            existing.total === totalN &&
                            existing.done === doneN &&
                            existing.started === startedN
                        );
                        uploadHistory.set(key, {
                            slot: slotNum,
                            type: typNum,
                            recv: recvN,
                            total: totalN,
                            done: doneN,
                            started: startedN,
                            updatedAt: unchanged ? existing.updatedAt : nowTs
                        });
                    });
                    // Prune uploadHistory: if it's not in the latest snapshot and NOT done, it's dead.
                    for (const key of uploadHistory.keys()) {
                        if (!currentSnapshotKeys.has(key)) {
                            const u = uploadHistory.get(key);
                            if (u && !u.done) {
                                uploadHistory.delete(key);
                            }
                        }
                    }
                    // Drop completed uploads that are no longer active and not present in persistent cache.
                    try {
                        for (const [k, u] of uploadHistory.entries()) {
                            if (!u || !u.done) continue;
                            const typ = Number(u.type);
                            const slot = Number(u.slot);
                            if (typ === 3) { // IMAGE
                                if (!cachedImageSlots.has(slot) && !activeImageSlots.has(slot)) uploadHistory.delete(k);
                            } else if (typ === 4) { // FONT
                                if (!cachedFontSlots.has(slot) && !activeFontSlots.has(slot)) uploadHistory.delete(k);
                            }
                        }
                    } catch (e) { }
                    // prune history size
                    if (uploadHistory.size > MAX_UPLOAD_HISTORY) {
                        const ordered = Array.from(uploadHistory.entries()).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
                        while (ordered.length > MAX_UPLOAD_HISTORY) {
                            const [oldKey] = ordered.shift();
                            uploadHistory.delete(oldKey);
                        }
                    }
                    const historyEntries = Array.from(uploadHistory.values()).sort((a, b) => {
                        if (a.done !== b.done) return a.done ? 1 : -1;
                        return b.updatedAt - a.updatedAt;
                    });
                    if (uploadsBadge) uploadsBadge.textContent = `${historyEntries.length}`;
                    if (uploadsList) {
                        if (historyEntries.length === 0) {
                            const next = '<div class="info-empty">(none)</div>';
                            if (uploadsDomSig !== next) {
                                uploadsList.innerHTML = next;
                                uploadsDomSig = next;
                            }
                        } else {
                            const usedTextures = (window.__usedTextures && typeof window.__usedTextures.has === "function") ? window.__usedTextures : new Set();
                            const usedFonts = (window.__usedFonts && typeof window.__usedFonts.has === "function") ? window.__usedFonts : new Set();
                            const html = historyEntries.map(u => {
                                const denom = (u.total && u.total > 0) ? u.total : cacheMax;
                                let pct = Math.max(0, Math.min(100, (u.recv / Math.max(1, denom)) * 100));
                                if (u.done || (u.total && u.total > 0 && u.recv >= u.total)) pct = 100;
                                const tname = (u.type === 4) ? "FONT" : (u.type === 3) ? "IMAGE" : `T${u.type ?? 0}`;
                                const recvLabel = fmtBytes(u.recv);
                                const totalLabel = u.total ? ` / ${fmtBytes(u.total)}` : "";
                                const status = u.done ? " ✓" : "";
                                const isActive = (u.type === 3 && activeImageSlots.has(u.slot)) || (u.type === 4 && activeFontSlots.has(u.slot));
                                const isCached = (u.type === 3 && cachedImageSlots.has(u.slot)) || (u.type === 4 && cachedFontSlots.has(u.slot));
                                let chk = "";
                                if (u.done || isActive || isCached) {
                                    if (u.type === 3) {
                                        const ids = objectsUsingTexture(Number(u.slot), s);
                                        const allSel = ids.length > 0 && ids.every(id => selectedObjIds.has(Number(id)));
                                        chk = `<input type="checkbox" data-focus-upload="1" data-upload-type="3" data-upload-slot="${u.slot}" ${allSel ? "checked" : ""} /> `;
                                    } else if (u.type === 4) {
                                        const ids = objectsUsingFont(Number(u.slot), s);
                                        const allSel = ids.length > 0 && ids.every(id => selectedObjIds.has(Number(id)));
                                        chk = `<input type="checkbox" data-focus-upload="1" data-upload-type="4" data-upload-slot="${u.slot}" ${allSel ? "checked" : ""} /> `;
                                    }
                                }
                                return `
                            <div style="margin-bottom:10px;">
                                <div style="display:flex; justify-content:space-between; gap:10px; font-size:11px; color: var(--text-secondary);">
                                    <label style="display:flex; align-items:center; gap:8px; cursor:pointer;">${chk}<span style="pointer-events:none;"><span style="color:var(--accent-blue); font-weight:800;">${tname}</span> slot ${u.slot}</span></label>
                                    <div>${recvLabel}${totalLabel}${status}</div>
                                </div>
                                <div style="height:8px; background:#151515; border:1px solid var(--border); border-radius:999px; overflow:hidden;">
                                    <div style="height:100%; width:${pct.toFixed(1)}%; min-width:${u.recv > 0 ? "3px" : "0px"}; background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));"></div>
                                </div>
                            </div>
                        `;
                            }).join("");
                            const sig = JSON.stringify(historyEntries.map(u => [u.type, u.slot, u.recv, u.total, u.done, u.started]));
                            if (uploadsDomSig !== sig) {
                                uploadsList.innerHTML = html;
                                uploadsDomSig = sig;
                            }
                        }
                    } // END THROTTLED UI

                    // Camera preview (refresh when timestamp changes, show no-signal when off)
                    try {
                        const ts = Number(s.stats?.cameraPreviewTs || 0);
                        const cameraEnabled = !!s.stats?.cameraEnabled;
                        const camPreviewEl = document.getElementById("camera-preview");
                        const camNoSignal = document.getElementById("camera-no-signal");

                        if (cameraEnabled && ts > 0) {
                            if (camPreviewEl) {
                                const last = Number(camPreviewEl.dataset.ts || "0");
                                if (ts !== last) {
                                    camPreviewEl.dataset.ts = String(ts);
                                    camPreviewEl.src = `/camera_preview?t=${encodeURIComponent(String(ts))}`;
                                }
                                camPreviewEl.classList.remove("hidden");
                            }
                            if (camNoSignal) camNoSignal.classList.add("opacity-0");
                        } else {
                            // Camera is off - show no-signal, hide preview
                            if (camPreviewEl) {
                                camPreviewEl.classList.add("hidden");
                                camPreviewEl.src = "";
                            }
                            if (camNoSignal) camNoSignal.classList.remove("opacity-0");
                        }

                        // Update camera status label
                        // Update camera status label
                        const camStatusLabel = document.getElementById("camera-status-label");
                        if (camStatusLabel) {
                            if (cameraEnabled) {
                                camStatusLabel.innerHTML = 'ACTIVE <span class="text-xs">●</span>';
                                camStatusLabel.classList.remove("text-zinc-500");
                                camStatusLabel.classList.add("text-sim-accent");
                            } else {
                                camStatusLabel.innerText = 'OFF';
                                camStatusLabel.classList.remove("text-sim-accent");
                                camStatusLabel.classList.add("text-zinc-500");
                            }
                        }
                    } catch (e) { }

                    // Mic EQ + gain (read stats, slider controls gain)
                    try {
                        const lvl = Math.max(0, Math.min(1, Number(s.stats?.micLevel || 0)));
                        const gain = Math.max(0, Math.min(6, Number(s.stats?.micGain || 1)));
                        const micEnabled = !!s.stats?.micEnabled;
                        if (micGainVal) micGainVal.textContent = gain.toFixed(1);
                        if (micLevelLabel) {
                            if (!micEnabled) {
                                micLevelLabel.textContent = "-- dB";
                            } else {
                                const db = lvl > 0 ? (20 * Math.log10(lvl)).toFixed(1) : "-∞";
                                micLevelLabel.textContent = `${db} dB`;
                            }
                        }
                        if (micGain && !micGain.dataset.dragging) micGain.value = String(gain);
                        if (micEq) {
                            const bars = 12;
                            if (micEq.childElementCount !== bars) {
                                micEq.innerHTML = "";
                                for (let i = 0; i < bars; i++) {
                                    const d = document.createElement("div");
                                    d.style.flex = "1";
                                    d.style.borderRadius = "6px";
                                    d.style.background = "rgba(255,255,255,0.10)";
                                    micEq.appendChild(d);
                                }
                            }
                            const children = micEq.children;
                            for (let i = 0; i < children.length; i++) {
                                if (micEnabled && lvl > 0.02) {
                                    const bias = (i + 1) / children.length;
                                    const h = Math.max(6, Math.min(46, (lvl * 46 * (0.5 + bias)) + (Math.random() * 6)));
                                    children[i].style.height = `${h}px`;
                                    children[i].style.background = "linear-gradient(180deg, rgba(127,218,137,0.95), rgba(241,250,140,0.85))";
                                } else {
                                    // Reset to baseline when mic is off or no level
                                    children[i].style.height = "6px";
                                    children[i].style.background = "rgba(255,255,255,0.10)";
                                }
                            }
                        }
                    } catch (e) { }

                    // Update Sensors UI
                    try {
                        updateSensorsUI(s);
                    } catch (e) { }

                } // END if (window.__uiDirty || ...) - THROTTLED BLOCK

            } // END if (s.stats)

        } // END handleSnapshot


        // Logs WebSocket (batch DOM updates; can otherwise starve the main thread)
        let logWs = null;
        const __logQueue = [];
        let __logFlushScheduled = false;

        function __flushLogs() {
            __logFlushScheduled = false;
            const logsDiv = document.getElementById("logs-console");
            const logArea = document.getElementById("log-area");
            if (!logsDiv || !logArea || logArea.classList.contains("hidden")) {
                __logQueue.length = 0;
                return;
            }

            // Only autoscroll if user is already near bottom.
            const nearBottom = (logsDiv.scrollHeight - logsDiv.scrollTop - logsDiv.clientHeight) < 40;
            const frag = document.createDocumentFragment();

            // Drain a bounded amount per frame to prevent long tasks.
            let n = 0;
            while (__logQueue.length && n < 200) {
                const msg = __logQueue.shift();
                const entry = document.createElement("div");
                entry.className = "log-entry";
                entry.textContent = msg;
                frag.appendChild(entry);
                n++;
            }

            logsDiv.appendChild(frag);
            while (logsDiv.childNodes.length > 500) logsDiv.removeChild(logsDiv.firstChild);
            if (nearBottom) logsDiv.scrollTop = logsDiv.scrollHeight;

            if (__logQueue.length) requestAnimationFrame(__flushLogs);
        }

        function startLogWs() {
            if (logWs) return;
            try {
                const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
                logWs = new WebSocket(`${protocol}//${location.host}/view_logs`);
                logWs.onmessage = (e) => {
                    try {
                        const cb = document.getElementById("logs-enabled");
                        const enabled = cb ? !!cb.checked : true;
                        const logArea = document.getElementById("log-area");
                        if (!enabled || !logArea || logArea.classList.contains("hidden")) return;

                        __logQueue.push(e.data);
                        if (!__logFlushScheduled) {
                            __logFlushScheduled = true;
                            requestAnimationFrame(__flushLogs);
                        }
                    } catch (err) { }
                };
                logWs.onclose = () => { logWs = null; };
                logWs.onerror = () => { try { logWs.close(); } catch (e) { } };
            } catch (e) {
                logWs = null;
            }
        }

        function stopLogWs() {
            try { if (logWs) logWs.close(); } catch (e) { }
            logWs = null;
            __logQueue.length = 0;
            __logFlushScheduled = false;
        }

        // Init clipping checkbox from localStorage (defaults to ON)
        try {
            const v = localStorage.getItem("sim_clip_enabled");
            const cb = document.getElementById("clip-enabled");
            if (cb) {
                if (v !== null) cb.checked = (v === "1");
                else cb.checked = false;
            }
        } catch (err) { }

        // Init logs checkbox from localStorage (defaults to ON)
        try {
            const v = localStorage.getItem("sim_show_logs");
            const cb = document.getElementById("logs-enabled");
            if (cb && v !== null) cb.checked = (v === "1");
        } catch (err) { }
        // Apply logs-enabled setting to WS (avoid hidden log processing)
        try { updateLogsEnabled(); } catch (e) { }
        // Init show-blacks checkbox from localStorage (defaults to OFF)
        try {
            const v = localStorage.getItem("sim_show_blacks");
            const cb = document.getElementById("show-blacks");
            if (cb && v !== null) cb.checked = (v === "1");
        } catch (err) { }
        // Init transparent-blacks checkbox from localStorage (defaults to OFF)
        try {
            const v = localStorage.getItem("sim_transparent_blacks");
            const cb = document.getElementById("transparent-blacks");
            if (cb && v !== null) cb.checked = (v === "1");
        } catch (err) { }
        // Init transparent-black clipping checkbox from localStorage (defaults to OFF)
        try {
            const v = localStorage.getItem("sim_transparent_black_clip");
            const checked = v === "1";
            getTransparentBlackClipCheckboxes().forEach((cb) => { cb.checked = checked; });
        } catch (err) { }
        // Init dbg-params checkbox from localStorage
        try {
            const v = localStorage.getItem("sim_dbg_params");
            const cb = document.getElementById("dbg-params");
            if (cb && v !== null) cb.checked = (v === "1");
        } catch (err) { }
        // Init mic-gain from localStorage
        try {
            const v = localStorage.getItem("sim_mic_gain");
            const mg = document.getElementById("mic-gain");
            const out = document.getElementById("mic-gain-val");
            if (mg && v !== null) {
                mg.value = v;
                if (out) out.textContent = Number(v).toFixed(1);
                // Also notify backend immediately on load if we have a saved value
                fetch("/mic_gain", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ micGain: Number(v) }),
                }).catch(() => { });
            }
        } catch (err) { }
        // Init ambient-lux from localStorage
        try {
            const v = localStorage.getItem("sim_ambient_lux");
            const amb = document.getElementById("ambient-lux");
            const out = document.getElementById("ambient-lux-val");
            if (amb && v !== null) {
                amb.value = v;
                if (out) out.textContent = `${Math.round(Number(v))} lx`;
                fetch("/ambient", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ ambientLux: Number(v) }),
                }).catch(() => { });
            }
        } catch (err) { }
        // Init preview HUD opacity from localStorage
        try {
            const v = localStorage.getItem("sim_preview_hud_opacity");
            const slider = document.getElementById("bg-hud-opacity");
            const out = document.getElementById("bg-hud-opacity-val");
            const pct = Math.max(0, Math.min(100, Number(v ?? "100")));
            previewHudOpacity = pct / 100.0;
            if (slider) slider.value = String(Math.round(pct));
            if (out) out.textContent = `${Math.round(pct)}%`;
        } catch (err) { }
        // Init screen background opacity from localStorage
        try {
            const v = localStorage.getItem("sim_screen_bg_opacity");
            const pct = Math.max(0, Math.min(100, Number(v ?? "100")));
            screenBgOpacity = pct / 100.0;
            if (screenBgOpacitySlider) screenBgOpacitySlider.value = String(Math.round(pct));
            if (screenBgOpacityVal) screenBgOpacityVal.textContent = `${Math.round(pct)}%`;
            applyScreenBgOpacity();
        } catch (err) { }
        // Display boundaries intentionally reset on every page load.
        try {
            const cb = document.getElementById("show-display-boundaries");
            if (cb) cb.checked = true;
            updateDisplayBoundaries();
        } catch (err) { }
        try {
            const savedSiteBgMode = localStorage.getItem("sim_site_bg_mode");
            setSiteBgMode(savedSiteBgMode || "night", false);
        } catch (err) {
            setSiteBgMode("night", false);
        }
        updateClipEnabled();
        updateLogsEnabled();
        updateShowBlacks();
        updateTransparentBlacks();
        updateTransparentBlackClip();
        updateDbgParams();
        loadFwVersion();
        try {
            const fwInput = document.getElementById("fw-version-input");
            if (fwInput) {
                fwInput.addEventListener("input", () => {
                    fwDirty = true;
                });
            }
        } catch (err) { }

        // Ensure Lucide icons are created for static buttons
        if (window.lucide) {
            window.lucide.createIcons();
        }

        // Mic gain control
        try {
            const mg = document.getElementById("mic-gain");
            if (mg) {
                mg.addEventListener("input", () => {
                    mg.dataset.dragging = "1";
                    const v = Number(mg.value || "1");
                    const out = document.getElementById("mic-gain-val");
                    if (out) out.textContent = v.toFixed(1);
                });
                mg.addEventListener("change", async () => {
                    const v = Number(mg.value || "1");
                    try {
                        localStorage.setItem("sim_mic_gain", v);
                        await fetch("/mic_gain", {
                            method: "POST",
                            headers: { "Content-Type": "application/json" },
                            body: JSON.stringify({ micGain: v }),
                        });
                    } catch (e) { }
                    mg.dataset.dragging = "";
                });
            }
        } catch (e) { }


        async function forceRender() {
            // "Render Cache" (matches Tk RENDER CACHE): run inspector on server.
            try {
                await fetch("/render_cache", { method: "POST" });
            } catch (e) {
                // ignore
            }
        }

        function updateRecordingUI(isRecording) {
            window.__isRecording = isRecording;
            const recShortcut = document.getElementById("recording-shortcut");
            const shortcutText = document.getElementById("recording-shortcut-text");
            const pulse = document.getElementById("recording-pulse");
            const mainBtn = document.getElementById("btn-recording-main");
            const btnText = document.getElementById("record-btn-text");
            const dotMain = document.getElementById("record-dot-main");

            if (recShortcut) {
                if (isRecording) {
                    recShortcut.classList.remove("bg-zinc-800/40", "border-white/10");
                    recShortcut.classList.add("bg-red-600/90", "border-red-500/50");
                    if (shortcutText) {
                        shortcutText.classList.remove("text-zinc-500");
                        shortcutText.classList.add("text-white");
                        shortcutText.textContent = "RECORDING";
                    }
                    if (pulse) {
                        pulse.classList.remove("hidden", "bg-zinc-600");
                        pulse.classList.add("bg-red-500", "animate-pulse", "shadow-[0_0_12px_rgba(239,68,68,0.8)]");
                    }
                } else {
                    recShortcut.classList.add("bg-zinc-800/40", "border-white/10");
                    recShortcut.classList.remove("bg-red-600/90", "border-red-500/50");
                    if (shortcutText) {
                        shortcutText.classList.remove("text-white");
                        shortcutText.classList.add("text-zinc-500");
                        shortcutText.textContent = "RECORD";
                    }
                    if (pulse) {
                        pulse.classList.remove("hidden", "bg-red-500", "animate-pulse", "shadow-[0_0_12px_rgba(239,68,68,0.8)]");
                        pulse.classList.add("bg-zinc-600");
                    }
                }
            }

            if (mainBtn && btnText) {
                btnText.textContent = isRecording ? "STOP RECORDING" : "START RECORDING";
                btnText.className = isRecording ? "text-white font-semibold" : "text-zinc-300";
                mainBtn.classList.toggle("bg-red-600/20", isRecording);
                mainBtn.classList.toggle("border-red-500/30", isRecording);
                if (dotMain) {
                    dotMain.classList.toggle("animate-pulse", isRecording);
                    if (isRecording) {
                        dotMain.classList.remove("bg-zinc-600");
                        dotMain.classList.add("bg-red-500", "shadow-[0_0_12px_rgba(239,68,68,0.6)]");
                    } else {
                        dotMain.classList.remove("bg-red-500", "shadow-[0_0_12px_rgba(239,68,68,0.6)]");
                        dotMain.classList.add("bg-zinc-600");
                    }
                }
            }
        }

        async function toggleRecording() {
            // Re-entry guard: while a previous toggle's fetch is still in flight,
            // ignore additional clicks AND ignore snapshot-stream pushes that would
            // otherwise clobber the optimistic UI mid-flight (see updateRecordingUI).
            if (window.__recordingToggling) {
                console.log("Toggle Recording ignored: previous toggle still pending");
                return;
            }
            const btnText = document.getElementById("record-btn-text");
            const shortcutText = document.getElementById("recording-shortcut-text");

            const currentState = !!window.__isRecording;
            const targetState = !currentState;

            console.log("Toggle Recording Clicked - State:", currentState, "->", targetState);
            window.__recordingToggling = true;

            try {
                // Immediate visual feedback
                if (btnText) btnText.textContent = targetState ? "STARTING..." : "STOPPING...";
                if (shortcutText) shortcutText.textContent = targetState ? "..." : "...";

                const url = currentState ? "/api/record/stop" : "/api/record/start";
                const res = await fetch(url, { method: "POST" });
                let data = null;
                try { data = await res.json(); } catch (_) { data = null; }

                if (res.ok && data && data.status === 'ok') {
                    // Update state immediately if successful
                    updateRecordingUI(targetState);
                } else {
                    // Revert UI if server rejected
                    console.error("Recording toggle rejected by server:", res.status, data);
                    updateRecordingUI(currentState);
                }
                window.__uiDirty = true;
            } catch (err) {
                console.error("Recording toggle failed:", err);
                updateRecordingUI(currentState);
            } finally {
                window.__recordingToggling = false;
            }
        }
    </script>
    <script>
        async function clearScene() {
            await fetch("/clear", { method: "POST" });
        }

        async function playAll() {
            try {
                // Use the process-based replay engine (same decode/render path as real-time).
                // Reset to start (-1) then play.
                const res = await fetch('/api/replay/play', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ index: -1 }),
                });
                const data = await res.json();
                if (data.status === 'ok') {
                    // Start polling for playback position
                    startPlaybackPolling();
                }
            } catch (err) { }
        }

        // Icons (SVG) - keep simple and high-contrast.
        // Play-from-zero: "play + pause" (filled so it never disappears).
        const __ICON_PLAY_FROM_ZERO = `<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
            <polygon points="5,7 13,12 5,17"></polygon>
            <rect x="14.5" y="7" width="1.2" height="10" rx="0.6"></rect>
            <rect x="17" y="7" width="2.6" height="10" rx="0.6"></rect>
            <rect x="20.4" y="7" width="2.6" height="10" rx="0.6"></rect>
        </svg>`;
        const __ICON_PAUSE = `<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
            <rect x="7" y="7" width="3.2" height="10" rx="0.7"></rect>
            <rect x="13.8" y="7" width="3.2" height="10" rx="0.7"></rect>
        </svg>`;
        const __ICON_RESUME = `<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
            <polygon points="7,7 17,12 7,17"></polygon>
        </svg>`;

        function __setReplayPauseIcon(paused) {
            try {
                const btn = document.getElementById("btn-replay-pause");
                const icon = document.getElementById("replay-pause-icon");
                if (!btn || !icon) return;
                btn.title = paused ? "Resume" : "Pause";
                icon.innerHTML = paused ? __ICON_RESUME : __ICON_PAUSE;
                // Ensure contrast: when the pause button is in the yellow state, icon must be black.
                if (btn.classList.contains("bg-sim-accent")) {
                    icon.classList.remove("text-white");
                    icon.classList.add("text-black");
                } else {
                    icon.classList.remove("text-black");
                    icon.classList.add("text-white");
                }
            } catch (e) { }
        }

        function __setReplayPlayIcon() {
            try {
                const icon = document.getElementById("replay-play-icon");
                if (!icon) return;
                icon.innerHTML = __ICON_PLAY_FROM_ZERO;
            } catch (e) { }
        }

        async function resumePlayback() {
            try {
                // No index => continue from current step (server keeps current index)
                await fetch('/api/replay/play', { method: 'POST' });
                if (window.__lastSceneSnapshot && window.__lastSceneSnapshot.playbackIndex !== undefined) {
                    updateReplayUiOnly(window.__lastSceneSnapshot.playbackIndex, false);
                }
            } catch (err) { }
        }

        async function pausePlayback() {
            try {
                await fetch('/api/replay/pause', { method: 'POST' });
                if (window.__lastSceneSnapshot && window.__lastSceneSnapshot.playbackIndex !== undefined) {
                    updateReplayUiOnly(window.__lastSceneSnapshot.playbackIndex, true);
                }
            } catch (err) { }
        }

        async function togglePauseResume() {
            const paused = !!(window.__lastSceneSnapshot && window.__lastSceneSnapshot.playbackPaused);
            if (paused) return await resumePlayback();
            return await pausePlayback();
        }

        async function stepFwd() {
            try {
                const res = await fetch('/api/replay/step?direction=1', { method: 'POST' });
                const data = await res.json();
                if (data.status === 'ok' && data.index !== undefined) {
                    if (window.__lastSceneSnapshot) {
                        window.__lastSceneSnapshot.playbackIndex = data.index;
                        window.__lastSceneSnapshot.playbackPaused = true;
                    }
                    updateReplayUiOnly(data.index, true);
                }
            } catch (err) { }
        }

        async function stepBack() {
            try {
                const res = await fetch('/api/replay/step?direction=-1', { method: 'POST' });
                const data = await res.json();
                if (data.status === 'ok' && data.index !== undefined) {
                    if (window.__lastSceneSnapshot) {
                        window.__lastSceneSnapshot.playbackIndex = data.index;
                        window.__lastSceneSnapshot.playbackPaused = true;
                    }
                    updateReplayUiOnly(data.index, true);
                }
            } catch (err) { }
        }

        async function setReplaySpeed(value) {
            const val = parseFloat(value);
            let speed = 1.0;

            // Map slider -10..10 to 0.1..10.0
            if (val === 0) {
                speed = 1.0;
            } else if (val > 0) {
                // 1 -> 2x, 10 -> 10x
                speed = 1.0 + val;
            } else {
                // -1 -> 0.9x, -9 -> 0.1x, -10 -> 0.1x
                const divisor = 1.0 + Math.abs(val);
                speed = 1.0 / divisor;
            }

            const textEl = document.getElementById("replay-speed-text");
            if (textEl) {
                if (speed < 1.0) {
                    const div = 1.0 / speed;
                    textEl.textContent = "1/" + div.toFixed(0) + "x";
                } else {
                    textEl.textContent = speed.toFixed(1) + "x";
                }
            }

            try {
                await fetch('/api/replay/speed', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ speed: speed })
                });
            } catch (err) { }
        }

        async function toggleScreen() {
            try {
                const resp = await fetch("/screen_toggle", { method: "POST" });
                const data = await resp.json();
                const btn = document.getElementById("btn-screen-toggle");
                const icon = btn ? btn.querySelector("span") : null;
                if (data && data.status === "ok") {
                    if (data.display_on) {
                        if (icon) {
                            icon.classList.remove("text-zinc-500");
                            icon.classList.add("text-sim-accent");
                        }
                    } else {
                        if (icon) {
                            icon.classList.remove("text-sim-accent");
                            icon.classList.add("text-zinc-500");
                        }
                    }
                }
            } catch (e) { }
        }

        function toggleSystemSection() {
            const content = document.getElementById('sys-content');
            const chevron = document.getElementById('sys-chevron');
            if (content && chevron) {
                content.classList.toggle('hidden');
                chevron.classList.toggle('rotate-180');
            }
        }

        function toggleCenterSystemSection() {
            const content = document.getElementById('center-sys-content');
            const chevron = document.getElementById('center-sys-chevron');
            if (content && chevron) {
                content.classList.toggle('hidden');
                chevron.classList.toggle('rotate-180');
            }
        }

        let playbackPolling = null;
        function startPlaybackPolling() {
            if (playbackPolling) return;
            playbackPolling = setInterval(async () => {
                // We'll use the snapshot to get current playback index if we add it to the state
                // For now, just poll /sim_state or a specialized endpoint
                const res = await fetch('/sim_state'); // handleSnapshot already called by main loop
            }, 500);
        }

        async function injectInput(dev, event) {
            await fetch(`/input?dev=${dev}&event=${event}`, { method: "POST" });
        }

        (function initTouchShortcuts() {
            const keyMap = {
                ' ':        { dev: 1, event: 5, btn: 'btn-tap' },
                'ArrowDown':{ dev: 1, event: 5, btn: 'btn-tap' },
                'ArrowLeft':{ dev: 1, event: 3, btn: 'btn-back' },
                'ArrowRight':{ dev: 1, event: 4, btn: 'btn-forward' },
                'l':        { dev: 1, event: 6, btn: 'btn-long-tap' },
                'L':        { dev: 1, event: 6, btn: 'btn-long-tap' },
                'ArrowUp':  { dev: 1, event: 6, btn: 'btn-long-tap' },
            };
            document.addEventListener('keydown', (e) => {
                if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
                const mapping = keyMap[e.key];
                if (!mapping) return;
                e.preventDefault();
                injectInput(mapping.dev, mapping.event);
                const btn = document.getElementById(mapping.btn);
                if (btn) {
                    btn.classList.add('!bg-sim-accent');
                    setTimeout(() => btn.classList.remove('!bg-sim-accent'), 120);
                }
            });
        })();

        function updateSensorsUI(s) {
            const enabled = !!s.stats?.quatEnabled;
            const badge = document.getElementById("sensors-status-badge");
            const sliders = ["yaw-slider", "pitch-slider", "roll-slider"];
            const btn = document.getElementById("btn-reset-sensors");
            const axisResetButtons = document.querySelectorAll("[data-sensor-axis-reset]");

            // Each time sensors transition disabled→enabled (app connect + QUAT enable), push the
            // neutral (0,0,0) quaternion so the simulator starts at yaw/pitch/roll = 0 instead of
            // the identity-quat default which maps to pitch=-90. Reset the flag on disable so the
            // next reconnect also gets initialized.
            if (enabled && !window.__sim_sensors_initialized) {
                window.__sim_sensors_initialized = true;
                try { sendSensorData(); } catch (e) { }
            } else if (!enabled && window.__sim_sensors_initialized) {
                window.__sim_sensors_initialized = false;
            }

            if (badge) {
                if (enabled) {
                    badge.innerHTML = 'ENABLED <span class="text-xs text-sim-accent">●</span>';
                    badge.classList.remove("text-zinc-500", "text-red-500");
                    badge.classList.add("text-sim-accent");
                } else {
                    badge.innerText = 'DISABLED';
                    badge.classList.remove("text-sim-accent", "text-red-500");
                    badge.classList.add("text-zinc-500");
                }
            }

            sliders.forEach(id => {
                const el = document.getElementById(id);
                if (el) el.disabled = !enabled;
            });
            axisResetButtons.forEach((el) => { el.disabled = !enabled; });
            if (btn) btn.disabled = !enabled;
        }

        let sensorThrottle = false;
        function normalizeQuaternion(q) {
            const n = Math.hypot(q.x, q.y, q.z, q.w);
            if (!Number.isFinite(n) || n === 0) return { x: 0, y: 0, z: 0, w: 1 };
            return { x: q.x / n, y: q.y / n, z: q.z / n, w: q.w / n };
        }

        function quaternionFromEulerCore(yawDeg, pitchDeg, rollDeg) {
            // Keep this exactly aligned with core Quaternion.fromEuler(yaw, pitch, roll):
            // q = Quaternion(yaw,pitch,roll) then qFinal = qRot2 * q * qRot1
            const rollRad = rollDeg * Math.PI / 180;
            const pitchRad = pitchDeg * Math.PI / 180;
            const yawRad = yawDeg * Math.PI / 180;

            const sinPsi = Math.sin(yawRad * 0.5);
            const cosPsi = Math.cos(yawRad * 0.5);
            const sinTheta = Math.sin(pitchRad * 0.5);
            const cosTheta = Math.cos(pitchRad * 0.5);
            const sinPhi = Math.sin(rollRad * 0.5);
            const cosPhi = Math.cos(rollRad * 0.5);

            const q = {
                x: sinPhi * cosTheta * cosPsi - cosPhi * sinTheta * sinPsi,
                y: cosPhi * sinTheta * cosPsi + sinPhi * cosTheta * sinPsi,
                z: cosPhi * cosTheta * sinPsi - sinPhi * sinTheta * cosPsi,
                w: cosPhi * cosTheta * cosPsi + sinPhi * sinTheta * sinPsi
            };

            const sqrt05 = Math.sqrt(0.5);
            const qRot1 = { x: -0.5, y: -0.5, z: 0.5, w: 0.5 };
            const qRot2 = { x: -sqrt05, y: -sqrt05, z: 0.0, w: 0.0 };
            const qTemp = multiplyQuaternions(q, qRot1);
            return normalizeQuaternion(multiplyQuaternions(qRot2, qTemp));
        }

        function sendSensorData() {
            const yaw = parseFloat(document.getElementById('yaw-slider').value || 0);
            const pitch = parseFloat(document.getElementById('pitch-slider').value || 0);
            const roll = parseFloat(document.getElementById('roll-slider').value || 0);

            document.getElementById('yaw-val').textContent = yaw + "°";
            document.getElementById('pitch-val').textContent = pitch + "°";
            document.getElementById('roll-val').textContent = roll + "°";
            updateMars360Background(yaw, pitch, roll);

            if (sensorThrottle) return;
            sensorThrottle = true;
            setTimeout(() => { sensorThrottle = false; }, 50); // 20Hz cap

            const qFinal = quaternionFromEulerCore(yaw, pitch, roll);

            fetch('/sensors', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(qFinal)
            }).catch(() => { });
        }

        function multiplyQuaternions(a, b) {
            return {
                x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
                y: a.w * b.y + a.y * b.w + a.z * b.x - a.x * b.z,
                z: a.w * b.z + a.z * b.w + a.x * b.y - a.y * b.x,
                w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
            };
        }

        function resetSensors() {
            document.getElementById('yaw-slider').value = 0;
            document.getElementById('pitch-slider').value = 0;
            document.getElementById('roll-slider').value = 0;
            sendSensorData();
        }

        function resetSensorAxis(axis) {
            const allowed = new Set(["yaw", "pitch", "roll"]);
            if (!allowed.has(axis)) return;
            const slider = document.getElementById(`${axis}-slider`);
            if (!slider || slider.disabled) return;
            slider.value = 0;
            sendSensorData();
        }

        async function openCacheFolder() {
            try {
                await fetch("/open_cache", { method: "POST" });
            } catch (e) {
                // ignore
            }
        }

        // -----------------------------------------------------------------------------------------
        // Cache Preview Modal (Images/Fonts cached slots)
        // -----------------------------------------------------------------------------------------
        function openCachePreview(kind, slot, sizeBytes) {
            const modal = document.getElementById("cache-preview-modal");
            if (!modal) return;
            const title = document.getElementById("cache-preview-title");
            const meta = document.getElementById("cache-preview-meta");
            const img = document.getElementById("cache-preview-img");
            if (title) title.textContent = (kind === "font" ? "Font Cache Preview" : "Image Cache Preview");
            if (meta) meta.textContent = `${kind} slot ${slot} • ${fmtBytes(Number(sizeBytes || 0))}`;
            if (img) {
                img.src = `/api/cache/preview/${encodeURIComponent(kind)}/${encodeURIComponent(String(slot))}?t=${Date.now()}`;
            }
            modal.classList.remove("hidden");
        }

        function closeCachePreview() {
            const modal = document.getElementById("cache-preview-modal");
            if (!modal) return;
            const img = document.getElementById("cache-preview-img");
            if (img) img.src = "";
            modal.classList.add("hidden");
        }

        // --- REPLAY LOGIC ---
        function getReplayPanel() { return document.getElementById('replay-panel'); }
        function getLogPicker() { return document.getElementById('log-picker-modal'); }

        async function showReplayPicker() {
            const picker = getLogPicker();
            if (picker) picker.classList.remove('hidden');
            const list = document.getElementById('log-files-list');
            list.innerHTML = '<div class="text-zinc-500 font-mono text-xs animate-pulse">Scanning logs...</div>';

            try {
                const res = await fetch('/api/command_logs');
                const files = await res.json();

                if (files.length === 0) {
                    list.innerHTML = '<div class="text-zinc-500 font-mono text-xs italic">No command logs found in logs/ folder.</div>';
                    return;
                }

                list.innerHTML = '';
                files.forEach(f => {
                    const row = document.createElement('div');
                    row.className = 'w-full flex justify-between items-center px-4 py-3 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 transition group text-left';

                    const date = new Date(f.modified * 1000).toLocaleString();
                    const isProtected = !!f.protected;

                    row.innerHTML = `
                        <div class="flex flex-col gap-1 overflow-hidden mr-3 flex-1 cursor-pointer" onclick="loadLogFile('${f.path}', '${f.name}')">
                            <div class="flex items-center gap-2">
                                <span class="text-white font-semibold text-xs truncate group-hover:text-sim-accent transition">${f.name}</span>
                                ${isProtected ? '<span class="text-[9px] bg-sim-accent/20 text-sim-accent px-1.5 py-0.5 rounded uppercase font-semibold tracking-wider">Protected</span>' : ''}
                            </div>
                            <span class="text-[10px] text-zinc-500 font-mono">${date} • ${(f.size / 1024).toFixed(1)} KB • ${f.commands} CMDs</span>
                        </div>
                        <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
                            <button onclick="event.stopPropagation(); revealLog('${f.name}')" class="p-2 rounded-lg hover:bg-white/10 text-zinc-500 hover:text-white transition" title="Show in Folder">
                                <i data-lucide="folder" class="w-3 h-3"></i>
                            </button>
                            <button onclick="event.stopPropagation(); toggleProtect('${f.name}', ${!isProtected})" class="p-2 rounded-lg hover:bg-white/10 ${isProtected ? 'text-sim-accent' : 'text-zinc-500 hover:text-white'} transition" title="${isProtected ? 'Unprotect' : 'Protect'}">
                                <i data-lucide="shield" class="w-3 h-3"></i>
                            </button>
                            <button onclick="event.stopPropagation(); renameLog('${f.name}')" class="p-2 rounded-lg hover:bg-white/10 text-zinc-500 hover:text-white transition" title="Rename">
                                <i data-lucide="pencil" class="w-3 h-3"></i>
                            </button>
                            ${!isProtected ? `
                            <button onclick="event.stopPropagation(); deleteLog('${f.name}')" class="p-2 rounded-lg hover:bg-red-500/20 text-zinc-500 hover:text-red-400 transition" title="Delete">
                                <i data-lucide="trash-2" class="w-3 h-3"></i>
                            </button>
                            ` : ''}
                            <button onclick="loadLogFile('${f.path}', '${f.name}')" class="ml-2 px-3 py-1.5 rounded-lg bg-white/10 hover:bg-sim-accent hover:text-black text-white text-[10px] font-semibold transition">
                                LOAD
                            </button>
                        </div>
                    `;
                    list.appendChild(row);
                });
                lucide.createIcons();
            } catch (e) {
                list.innerHTML = `<div class="text-red-400 font-mono text-xs">Error: ${e.message}</div>`;
            }
        }

        async function renameLog(oldName) {
            const newName = prompt("Enter new name:", oldName);
            if (newName && newName !== oldName) {
                try {
                    const res = await fetch('/api/logs/rename', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ old_name: oldName, new_name: newName })
                    });
                    if ((await res.json()).status === 'ok') showReplayPicker();
                } catch (e) { alert("Rename failed"); }
            }
        }

        async function deleteLog(name) {
            if (confirm(`Delete ${name}?`)) {
                try {
                    const res = await fetch('/api/logs/delete', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ name })
                    });
                    if ((await res.json()).status === 'ok') showReplayPicker();
                } catch (e) { alert("Delete failed"); }
            }
        }

        async function toggleProtect(name, protect) {
            try {
                const res = await fetch('/api/logs/protect', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name, protect })
                });
                if ((await res.json()).status === 'ok') showReplayPicker();
            } catch (e) { alert("Protect failed"); }
        }

        async function revealLog(name) {
            try {
                await fetch('/api/logs/reveal', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name })
                });
            } catch (e) { }
        }

        function hideLogPicker() {
            const picker = getLogPicker();
            if (picker) picker.classList.add('hidden');
        }

        async function loadLogFile(path, name) {
            hideLogPicker();
            const panel = getReplayPanel();
            if (panel) {
                panel.classList.remove('hidden');
                setTimeout(() => panel.classList.remove('translate-x-full'), 10);
            }

            document.getElementById('replay-filename').textContent = name;
            const list = document.getElementById('replay-list');
            list.innerHTML = `
                <div class="flex flex-col items-center justify-center p-20 space-y-4">
                    <div class="relative w-16 h-16">
                        <div class="absolute inset-0 border-4 border-white/5 rounded-full"></div>
                        <div class="absolute inset-0 border-t-4 border-sim-accent rounded-full animate-spin"></div>
                    </div>
                    <div class="text-sim-accent font-semibold uppercase tracking-[0.3em] text-[10px] animate-pulse">Analyzing Sequence...</div>
                    <div class="text-zinc-500 font-mono text-[9px] opacity-60">Exploding ZLIB packets & caching bytes</div>
                </div>
            `;

            try {
                const res = await fetch('/api/replay/load', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path })
                });
                const data = await res.json();
                if (data.status === 'ok') {
                    renderCommands(data.commands);
                } else {
                    list.innerHTML = `<div class="text-red-400 font-mono text-xs p-4">Load Error: ${data.error}</div>`;
                }
            } catch (e) {
                list.innerHTML = `<div class="text-red-400 font-mono text-xs p-4">Error: ${e.message}</div>`;
            }
        }


        function renderCommandThumb(cmd) {
            const op = cmd.op;
            if (op === 0x10) { // ADD_OBJECT
                let icon = "?";
                const t = cmd.item_type || "";
                if (t === "TEXT") icon = "T";
                else if (t.includes("RECT")) icon = "□";
                else if (t.includes("ELLIPSE")) icon = "○";
                else if (t === "IMAGE") icon = "img";
                return `<div class="w-6 h-6 flex-shrink-0 rounded bg-white/10 flex items-center justify-center text-[10px] font-semibold text-sim-accent/80">${icon}</div>`;
            }
            if (op === 0x12) { // SET_COLOR
                const c = cmd.color_hex;
                if (c !== undefined) {
                    const r = (c >> 16) & 0xFF;
                    const g = (c >> 8) & 0xFF;
                    const b = c & 0xFF;
                    const a = ((c >> 24) & 0xFF) / 255;
                    return `<div class="w-4 h-4 rounded-full border border-white/20 flex-shrink-0" style="background:rgba(${r},${g},${b},${a})"></div>`;
                }
            }
            // Generic icon for opcodes
            const icons = {
                0x11: "X",
                0x13: "XY",
                0x16: "T+",
                0x20: "UL",
                0x1D: "G",
                0x1B: "GR"
            };
            const icon = icons[op] || cmd.op_name.substring(0, 2);
            return `<div class="w-6 h-6 flex-shrink-0 rounded bg-white/5 flex items-center justify-center text-[8px] font-mono text-zinc-500">${icon}</div>`;
        }


        function renderCommands(cmds) {
            const list = document.getElementById('replay-list');
            if (!list) return;
            window.__lastPlaybackIdx = -1; // Reset highlight scroll
            window.__replayRowToRawIdx = [];
            window.__replayRawToRowIdx = {};
            const total = Array.isArray(cmds) ? cmds.length : 0;
            // Virtualized list: keep only visible rows in the DOM (prevents long tasks on load).
            // IMPORTANT: renderCommands() can be called multiple times (load/refresh/shortcut).
            // Always tear down any previous virtualization instance + listeners, otherwise an old
            // instance can repaint stale rows while you scroll (visual mismatch).
            try {
                if (window.__replayVirt && typeof window.__replayVirt.destroy === "function") {
                    window.__replayVirt.destroy();
                }
            } catch (e) { }
            list.innerHTML = '';
            list.scrollTop = 0;
            window.__replayCmds = cmds || [];
            window.__replayVirt = null;

            const totalEl = document.getElementById('replay-step-total');
            if (totalEl) totalEl.textContent = String(total);
            const currentEl = document.getElementById("replay-step-current");
            if (currentEl) currentEl.textContent = "0";

            // Precompute row <-> raw index mapping without touching the DOM.
            for (let i = 0; i < total; i++) {
                const cmd = cmds[i];
                const rawIdx = (function () {
                    const p = cmd.parent_idx;
                    if (p !== undefined && p !== null) {
                        const pn = Number(p);
                        if (Number.isFinite(pn)) return pn;
                    }
                    const ci = cmd.idx;
                    if (ci !== undefined && ci !== null) {
                        if (typeof ci === "number" && Number.isFinite(ci)) return ci;
                        if (typeof ci === "string") {
                            const m = ci.match(/^(\d+)\s*>/);
                            if (m) return Number(m[1]);
                            const cn = Number(ci);
                            if (Number.isFinite(cn)) return cn;
                        }
                    }
                    return i;
                })();
                window.__replayRowToRawIdx[i] = rawIdx;
                if (window.__replayRawToRowIdx[rawIdx] === undefined) window.__replayRawToRowIdx[rawIdx] = i;
            }

            const inner = document.createElement('div');
            inner.style.position = 'relative';
            inner.style.width = '100%';
            list.appendChild(inner);

            const cs = getComputedStyle(list);
            const padTop = parseFloat(cs.paddingTop || "0") || 0;
            const padBottom = parseFloat(cs.paddingBottom || "0") || 0;

            // Fixed row sizing for fast virtualization (content is truncated via overflow).
            // Slightly taller to support larger fonts for readability.
            const ROW_H = 98;
            const GAP = 12;
            const PITCH = ROW_H + GAP;
            inner.style.height = `${Math.max(0, total * PITCH)}px`;

            function buildRow(i) {
                const cmd = cmds[i];
                const isVirtual = !!cmd.is_virtual;
                const isContainer = !!cmd.is_container;
                const displayIdx = (cmd.idx !== undefined) ? cmd.idx : i;
                const opLabel = isContainer ? 'BLOCK' : cmd.op_name;
                const thumb = renderCommandThumb(cmd);

                const row = document.createElement('div');
                row.setAttribute('data-idx', String(i));
                row.setAttribute('data-raw-idx', String(window.__replayRowToRawIdx[i]));

                let rowClasses = 'replay-row p-3 rounded-2xl border flex items-center justify-between group transition-all duration-200 cursor-pointer ';
                if (isContainer) rowClasses += 'bg-sim-accent/5 border-sim-accent/20 border-dashed opacity-80';
                else if (isVirtual) rowClasses += 'ml-8 bg-white/5 border-white/5 pl-4 border-l-2 border-l-sim-accent/30';
                else rowClasses += 'bg-white/5 border-white/5 hover:bg-white/10';
                row.className = rowClasses;

                row.style.position = 'absolute';
                row.style.left = '0';
                row.style.right = '0';
                row.style.top = `${i * PITCH}px`;
                row.style.height = `${ROW_H}px`;
                row.style.overflow = 'hidden';
                row.onclick = () => playCommand(i);

                // Keep markup lightweight (no lucide icons / no row timers), but make it human-readable.
                const extraBadges = [];
                try {
                    if (cmd.object_id !== undefined && cmd.object_id !== null && Number(cmd.object_id) !== 0xFFFF) {
                        extraBadges.push(`<span class="px-1.5 py-0.5 rounded bg-white/5 border border-white/10 text-[11px] font-mono text-zinc-400">ID ${Number(cmd.object_id)}</span>`);
                    }
                } catch (e) { }
                try {
                    if (cmd.item_type) {
                        extraBadges.push(`<span class="px-1.5 py-0.5 rounded bg-sim-accent/10 border border-sim-accent/20 text-[11px] font-semibold text-sim-accent uppercase tracking-tighter">${String(cmd.item_type)}</span>`);
                    }
                } catch (e) { }
                try {
                    const x = Number(cmd.x), y = Number(cmd.y);
                    if (Number.isFinite(x) && Number.isFinite(y)) {
                        extraBadges.push(`<span class="px-1.5 py-0.5 rounded bg-blue-500/10 border border-blue-500/20 text-[11px] font-mono text-blue-400">XY ${x.toFixed(1)},${y.toFixed(1)}</span>`);
                    }
                } catch (e) { }
                try {
                    const w = Number(cmd.w), h = Number(cmd.h);
                    if (Number.isFinite(w) && Number.isFinite(h)) {
                        extraBadges.push(`<span class="px-1.5 py-0.5 rounded bg-purple-500/10 border border-purple-500/20 text-[11px] font-mono text-purple-400">SIZE ${w.toFixed(1)}×${h.toFixed(1)}</span>`);
                    }
                } catch (e) { }

                let colorPill = "";
                try {
                    if (cmd.color_hex !== undefined && cmd.color_hex !== null) {
                        const c = Number(cmd.color_hex) >>> 0;
                        const r = (c >> 16) & 0xFF;
                        const g = (c >> 8) & 0xFF;
                        const b = (c >> 0) & 0xFF;
                        const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
                        colorPill = `<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-white/5 border border-white/10">
                            <span class="w-2.5 h-2.5 rounded-full border border-white/20" style="background:${hex}"></span>
                            <span class="text-[11px] font-mono text-zinc-400">${hex}</span>
                        </span>`;
                    }
                } catch (e) { colorPill = ""; }

                const title = (cmd.explanation || cmd.op_name || "").toString();
                const meta = [];
                try { if (cmd.pkt_id !== undefined) meta.push(`pkt:${cmd.pkt_id}`); } catch (e) { }
                try { if (cmd.zlib) meta.push("zlib"); } catch (e) { }
                try {
                    if (cmd.decode_ts && cmd.ts) {
                        const dt = (Number(cmd.ts) - Number(cmd.decode_ts)) * 1000.0;
                        if (Number.isFinite(dt)) meta.push(`Δ${dt.toFixed(1)}ms`);
                    }
                } catch (e) { }

                row.innerHTML = `
                    <div class="flex items-center gap-3 overflow-hidden flex-1">
                        <span class="text-[10px] font-mono text-zinc-500 w-12 flex-shrink-0 text-right pr-2 mr-1 border-r border-white/10 italic">${displayIdx}</span>
                        ${thumb}
                        <div class="flex-1 overflow-hidden">
                            <div class="flex items-center gap-2 min-w-0">
                                <div class="text-[14px] font-semibold uppercase tracking-widest text-sim-accent opacity-80 truncate min-w-0">${title}</div>
                                ${colorPill}
                            </div>
                            <div class="flex items-center gap-2 mt-1 min-w-0 whitespace-nowrap overflow-hidden">
                                <div class="text-[12px] text-zinc-500 font-mono opacity-50 truncate">${isContainer ? 'Compressed Sequence' : ('OP: ' + opLabel)}</div>
                                <div class="flex items-center gap-1 min-w-0 overflow-hidden">
                                    ${extraBadges.slice(0, 4).join("")}
                                </div>
                            </div>
                            <div class="text-[10px] text-zinc-600 font-mono mt-0.5 opacity-40 truncate">${meta.join(" ")}</div>
                        </div>
                    </div>
                    <div class="flex items-center gap-2 ml-2">
                        ${!isContainer ? `<button onclick="event.stopPropagation(); playCommand(${i})"
                            class="px-2 py-1 rounded-full hover:bg-sim-accent hover:text-black transition text-zinc-500 flex items-center justify-center group-hover:bg-white/10 group-hover:text-white text-[11px] font-semibold"
                            title="Play this step and pause">▶</button>` : ''}
                    </div>
                `;
                return row;
            }

            const virt = {
                list,
                inner,
                total,
                padTop,
                padBottom,
                rowH: ROW_H,
                pitch: PITCH,
                overscan: 8,
                rendered: new Map(),
                scheduled: false,
                currentRawIdx: -1,
                programmaticScroll: false,
                lastUserScrollAt: 0,
                lastAutoFollowAt: 0,
                _followScheduled: false,
                _pendingFollowRowIdx: null,
                // "Comfortably visible" (not hugging edges) so the current row is easy to see.
                isRowComfortable: function (rowIdx) {
                    const i = Number(rowIdx);
                    if (!Number.isFinite(i) || i < 0) return false;
                    const scrollY = Math.max(0, this.list.scrollTop - this.padTop);
                    const viewH = Math.max(0, this.list.clientHeight - this.padTop - this.padBottom);
                    const top = i * this.pitch;
                    const bottom = top + this.rowH;
                    // IMPORTANT: With our current row height vs viewport height, a tight "band" test can
                    // *never* be satisfied reliably (rounding makes it fail by a few px).
                    // For auto-follow, "comfortable" should mean: the whole row is visible (not clipped).
                    const margin = 2; // small slack to avoid edge rounding
                    return (top >= (scrollY + margin)) && (bottom <= (scrollY + viewH - margin));
                },
                render: function (force) {
                    const scrollY = Math.max(0, this.list.scrollTop - this.padTop);
                    const viewH = Math.max(0, this.list.clientHeight - this.padTop - this.padBottom);
                    let start = Math.floor(scrollY / this.pitch) - this.overscan;
                    let end = Math.ceil((scrollY + viewH) / this.pitch) + this.overscan;
                    if (start < 0) start = 0;
                    if (end > this.total) end = this.total;

                    // Remove out-of-range nodes
                    for (const [idx, node] of this.rendered.entries()) {
                        if (idx < start || idx >= end) {
                            try { node.remove(); } catch (e) { }
                            this.rendered.delete(idx);
                        }
                    }
                    // Add missing nodes and update state
                    for (let i = start; i < end; i++) {
                        let node = this.rendered.get(i);
                        if (!node) {
                            node = buildRow(i);
                            this.rendered.set(i, node);
                            this.inner.appendChild(node);
                        }
                        const raw = Number(window.__replayRowToRawIdx[i]);
                        node.classList.remove('is-completed', 'is-current-step');
                        if (Number.isFinite(raw) && Number.isFinite(this.currentRawIdx)) {
                            if (raw <= this.currentRawIdx) node.classList.add('is-completed');
                            if (raw === this.currentRawIdx) node.classList.add('is-current-step');
                        }
                    }
                },
                ensureVisible: function (rowIdx) {
                    const i = Number(rowIdx);
                    if (!Number.isFinite(i) || i < 0) return;
                    const top = i * this.pitch;
                    const bottom = top + this.rowH;
                    const scrollY = Math.max(0, this.list.scrollTop - this.padTop);
                    const viewH = Math.max(0, this.list.clientHeight - this.padTop - this.padBottom);
                    let nextScrollY = null;
                    // Keep the focused row slightly below the top so it's easy to see context.
                    const anchor = 0.18; // 18% from top
                    const desiredTop = Math.max(0, top - viewH * anchor);
                    if (top < scrollY) nextScrollY = desiredTop;
                    else if (bottom > scrollY + viewH) nextScrollY = desiredTop;
                    if (nextScrollY === null) return;

                    const self = this;
                    self.programmaticScroll = true;
                    try {
                        self._dbgLastProgrammaticAt = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
                    } catch (e) { }
                    // Clamp to scrollable range (use real scrollHeight so padding is handled correctly).
                    const maxScrollTop = Math.max(0, self.list.scrollHeight - self.list.clientHeight);
                    const desiredScrollTop = nextScrollY + self.padTop;
                    const clamped = Math.max(0, Math.min(maxScrollTop, desiredScrollTop));
                    self.list.scrollTop = clamped;
                    requestAnimationFrame(() => {
                        if (window.__replayVirt === self) self.programmaticScroll = false;
                    });
                }
            };

            const __onScroll = () => {
                // Guard: ignore events from an old instance after a new load.
                if (window.__replayVirt !== virt) return;
                // Record only user-driven scrolling; ignore programmatic scrolls from ensureVisible().
                try {
                    if (!virt.programmaticScroll) {
                        const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
                        virt.lastUserScrollAt = now;
                    }
                } catch (e) { }
                if (virt.scheduled) return;
                virt.scheduled = true;
                requestAnimationFrame(() => {
                    if (window.__replayVirt !== virt) return;
                    virt.scheduled = false;
                    virt.render(false);
                });
            };
            const __onResize = () => { __onScroll(); };
            list.addEventListener('scroll', __onScroll, { passive: true });
            window.addEventListener('resize', __onResize);
            virt.destroy = () => {
                try { list.removeEventListener('scroll', __onScroll); } catch (e) { }
                try { window.removeEventListener('resize', __onResize); } catch (e) { }
                try { virt.rendered.clear(); } catch (e) { }
                try { if (virt.inner) virt.inner.innerHTML = ""; } catch (e) { }
            };

            window.__replayVirt = virt;
            virt.render(true);
        }

        async function loadCurrentLog() {
            try {
                const stat = await fetch("/status").then(r => r.json());
                if (stat.activeLog) {
                    const path = `logs / ${stat.activeLog}`;
                    const res = await fetch('/api/replay/load', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ path: path })
                    }).then(r => r.json());
                    if (res.status === 'ok') {
                        document.getElementById('replay-filename').textContent = stat.activeLog;
                        renderCommands(res.commands);
                        const panel = getReplayPanel();
                        panel.classList.remove('hidden');
                        panel.classList.remove('minimized'); // Ensure it's unminimized on load
                        setTimeout(() => panel.classList.remove('translate-x-full'), 10);
                    }
                }
            } catch (err) { console.error("Shortcut failed", err); }
        }

        function toggleReplayMinimize() {
            const panel = getReplayPanel();
            if (panel) {
                panel.classList.toggle('minimized');
                lucide.createIcons(); // Ensure icons are rendered correctly if state changed
            }
        }

        async function closeReplay() {
            try {
                hideReplayPanel();
                await fetch('/api/replay/close', { method: 'POST' });
                // Clear UI cache
                try {
                    if (window.__replayVirt && typeof window.__replayVirt.destroy === "function") {
                        window.__replayVirt.destroy();
                    }
                } catch (e) { }
                window.__replayVirt = null;
                window.__replayCmds = null;
                window.__replayRowToRawIdx = null;
                window.__replayRawToRowIdx = null;
                const list = document.getElementById('replay-list');
                if (list) list.innerHTML = '';
            } catch (err) { }
        }

        function hideReplayPanel() {
            const panel = getReplayPanel();
            if (panel) {
                panel.classList.add('translate-x-full');
                setTimeout(() => panel.classList.add('hidden'), 300);
            }
        }

        function clearAllSelections() {
            selectedObjIds.clear();
            postFocus([], false);
            // handleSnapshot will trigger UI update on next tick
        }

        async function playCommand(idx) {
            try {
                // By user request, "play" on a specific row should seek to it and pause.
                const rawIdx = _replay_raw_idx_from_row(idx);
                const res = await fetch('/api/replay/seek?index=' + encodeURIComponent(String(rawIdx)), {
                    method: 'POST'
                });
                try {
                    const data = await res.json();
                    if (data && data.status === 'ok' && data.index !== undefined) {
                        if (!window.__lastSceneSnapshot) window.__lastSceneSnapshot = {};
                        window.__lastSceneSnapshot.playbackIndex = data.index;
                        window.__lastSceneSnapshot.playbackPaused = true;
                        handleSnapshot(window.__lastSceneSnapshot);
                    }
                } catch (e) { }
            } catch (e) { console.error("Play failed", e); }
        }

        async function playAllCommands() {
            try {
                // Always start from zero (user request).
                console.log("UI_PLAY_FROM_ZERO: click");
                const res = await fetch('/api/replay/play', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ index: -1 }),
                });
                let data = null;
                try { data = await res.json(); } catch (e) { }
                console.log("UI_PLAY_FROM_ZERO: resp", res.status, data);
            } catch (e) { console.error("Play all failed", e); }
        }

    </script>

    <!-- Replay Panel -->
    <div id="replay-panel"
        class="fixed inset-y-0 right-0 w-[640px] bg-sim-bg border-l border-white/10 shadow-2xl z-50 transform translate-x-full transition-transform duration-300 flex flex-col hidden"
        style="background-color: rgba(15, 15, 15, 0.99);">
        <div class="p-6 border-b border-white/10 flex justify-between items-center bg-black/40 replay-panel-header"
            onclick="if(this.parentElement.classList.contains('minimized')) toggleReplayMinimize()">
            <div class="flex-1 overflow-hidden">
                <h2 class="text-[10px] font-semibold uppercase tracking-[0.35em] text-white/50 mb-1">Command Replay</h2>
                <p id="replay-filename" class="text-[11px] text-white font-mono truncate max-w-[200px] font-semibold">(no
                    file)</p>
            </div>
            <div class="flex items-center gap-1">
                <button onclick="event.stopPropagation(); toggleReplayMinimize()" title="Minimize"
                    class="p-2 rounded-xl hover:bg-white/10 text-zinc-500 hover:text-white transition">
                    <i data-lucide="minus" class="w-5 h-5"></i>
                </button>
                <button onclick="event.stopPropagation(); closeReplay()" title="Close Replay"
                    class="p-2 rounded-xl hover:bg-white/10 text-zinc-500 hover:text-white transition">
                    <i data-lucide="x" class="w-5 h-5"></i>
                </button>
            </div>
        </div>

        <div class="p-6 border-b border-white/10 flex flex-col gap-4 bg-white/5">
            <div class="flex items-center justify-between">
                <div class="flex gap-2">
                    <button id="btn-replay-play" onclick="playAllCommands()" title="Play All"
                        class="p-2.5 rounded-2xl bg-sim-accent text-black hover:bg-white transition flex items-center gap-2">
                        <span id="replay-play-icon" class="text-black"></span>
                    </button>
                    <button id="btn-replay-pause" onclick="togglePauseResume()" title="Pause"
                        class="p-2.5 rounded-2xl bg-white/10 text-white hover:bg-white/20 transition">
                        <!-- icon swapped in JS based on paused state -->
                        <span id="replay-pause-icon" class="text-white"></span>
                    </button>
                    <button onclick="stepBack()" title="Step Backward"
                        class="p-2.5 rounded-2xl bg-white/10 text-white hover:bg-white/20 transition">
                        <i data-lucide="skip-back" class="w-4 h-4"></i>
                    </button>
                    <button onclick="stepFwd()" title="Step Forward"
                        class="p-2.5 rounded-2xl bg-white/10 text-white hover:bg-white/20 transition">
                        <i data-lucide="skip-forward" class="w-4 h-4"></i>
                    </button>
                </div>
                <!-- Step Counter & Countdown -->
                <div class="flex items-center gap-3">
                    <div id="replay-countdown" class="relative w-7 h-7 hidden">
                        <svg class="w-full h-full transform -rotate-90">
                            <circle cx="14" cy="14" r="11" stroke="currentColor" stroke-width="2" fill="transparent"
                                class="text-white/10" />
                            <circle id="replay-countdown-circle" cx="14" cy="14" r="11" stroke="currentColor"
                                stroke-width="2.5" fill="transparent" class="text-sim-accent" stroke-dasharray="69.1"
                                stroke-dashoffset="0" stroke-linecap="round" />
                        </svg>
                        <div id="replay-countdown-text"
                            class="absolute inset-0 flex items-center justify-center text-[8px] font-mono text-white/50">
                            0.5s</div>
                    </div>

                    <div
                        class="text-[10px] font-mono text-zinc-500 bg-black/40 px-3 py-1.5 rounded-full border border-white/5">
                        <span id="replay-step-current" class="text-sim-accent font-semibold">0</span>
                        <span class="opacity-30 mx-1">/</span>
                        <span id="replay-step-total">0</span>
                    </div>
                </div>
            </div>

            <!-- Speed Factor Control -->
            <div class="flex flex-col gap-2 mt-2 px-1">
                <div class="flex justify-between items-center">
                    <div class="text-[9px] font-semibold uppercase tracking-widest text-zinc-500">Speed Factor</div>
                    <div id="replay-speed-text" class="text-[11px] font-semibold font-mono text-white shadow-sim">1.0x</div>
                </div>
                <div class="relative flex items-center group">
                    <input id="replay-speed-slider" type="range" min="-10" max="10" step="1" value="0"
                        oninput="setReplaySpeed(this.value)"
                        class="flex-1 h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-sim-accent group-hover:bg-white/20 transition-all">
                </div>
                <div class="flex justify-between px-0.5 text-[8px] font-mono text-zinc-600 uppercase">
                    <span>Slower</span>
                    <span>Normal</span>
                    <span>Faster</span>
                </div>
            </div>
        </div>

        <div id="replay-list" class="flex-1 overflow-y-auto p-5 space-y-3 custom-scrollbar">
            <!-- Command rows go here -->
        </div>
    </div>

    <!-- Log File Picker Modal -->
    <div id="log-picker-modal"
        class="fixed inset-0 bg-black/80 backdrop-blur-md z-[60] flex items-center justify-center hidden p-6">
        <div class="bg-sim-bg border border-white/10 rounded-[32px] w-full max-w-4xl overflow-hidden shadow-2xl">
            <div class="p-8 border-b border-white/10 flex justify-between items-center bg-white/5">
                <h3 class="text-xs font-semibold uppercase tracking-[0.35em] text-zinc-400">Select Command Log</h3>
                <button onclick="hideLogPicker()"
                    class="p-2 rounded-xl text-zinc-500 hover:text-white hover:bg-white/10 transition"><i
                        data-lucide="x"></i></button>
            </div>
            <div id="log-files-list" class="p-8 space-y-3 max-h-[60vh] overflow-y-auto custom-scrollbar">
                <!-- Log files go here -->
            </div>
        </div>
    </div>

    <!-- Cache Preview Modal -->
    <div id="cache-preview-modal"
        class="fixed inset-0 bg-black/80 backdrop-blur-md z-[70] flex items-center justify-center hidden p-6">
        <div class="bg-sim-bg border border-white/10 rounded-[28px] w-full max-w-5xl overflow-hidden shadow-2xl">
            <div class="p-6 border-b border-white/10 flex justify-between items-center bg-white/5">
                <div class="min-w-0">
                    <div id="cache-preview-title" class="text-[11px] font-semibold uppercase tracking-[0.35em] text-zinc-400">
                        Cache Preview
                    </div>
                    <div id="cache-preview-meta" class="text-[11px] font-mono text-zinc-500 mt-1 truncate"></div>
                </div>
                <div class="flex items-center gap-2">
                    <button onclick="forceRender()" title="Render Cache"
                        class="px-4 py-2 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-[10px] font-semibold uppercase tracking-widest text-sim-accent">
                        Render
                    </button>
                    <button onclick="openCacheFolder()" title="Open Cache Folder"
                        class="px-4 py-2 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-[10px] font-semibold uppercase tracking-widest text-blue-300">
                        Folder
                    </button>
                    <button onclick="closeCachePreview()" title="Close"
                        class="px-4 py-2 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 transition text-[10px] font-semibold uppercase tracking-widest text-zinc-300">
                        Close
                    </button>
                </div>
            </div>
            <div class="p-6 bg-black/10">
                <div class="rounded-2xl border border-white/10 bg-black/30 p-4 overflow-auto max-h-[70vh]">
                    <img id="cache-preview-img" src="" alt="cache preview" class="max-w-full h-auto mx-auto" />
                </div>
            </div>
        </div>
    </div>

    <style>
        .custom-scrollbar::-webkit-scrollbar {
            width: 6px;
        }

        .custom-scrollbar::-webkit-scrollbar-track {
            background: transparent;
        }

        .custom-scrollbar::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
        }

        .custom-scrollbar::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.2);
        }
    </style>

    <script>
        // Final initialization - runs after all scripts are loaded
        window.addEventListener('load', async () => {
            console.log("Dashboard Loaded - Performing final sync...");
            try {
                const resp = await fetch("/sim_state");
                const s = await resp.json();
                if (s) {
                    console.log("Initial state sync:", s);
                    if (typeof setPowerUi === 'function') setPowerUi(!!s.poweredOn);
                    if (typeof updateRecordingUI === 'function') updateRecordingUI(!!s.isRecording);
                }
            } catch (e) {
                console.error("Final sync failed:", e);
            }
        });
    </script>
</body>

</html>
