<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ brand }} Dashboard</title>
    <link rel="stylesheet" href="/static/css/style.css">
    <link rel="stylesheet" href="/static/features/personality_picker.css">
    <link rel="stylesheet" href="/static/features/toasts.css">
    <link rel="stylesheet" href="/static/features/welcome.css">
    <link rel="stylesheet" href="/static/features/locked_tabs.css">
    <link rel="stylesheet" href="/static/features/audio_wizard.css">
    <link rel="stylesheet" href="/static/features/license.css">
    <link rel="stylesheet" href="/static/features/mac_audio_modal.css">
    <link rel="stylesheet" href="/static/features/midi.css">
    <link rel="stylesheet" href="/static/features/dmx_discover.css">
    <link rel="stylesheet" href="/static/features/govee.css">
    <link rel="stylesheet" href="/static/features/fixtures.css">
    <link rel="stylesheet" href="/static/features/dmx.css">
    <link rel="stylesheet" href="/static/features/feedback.css">
    <script>
      // Tier is server-rendered so JS modules can gate CTAs without a
      // round-trip. "starter" | "pro" | "venue".
      window.CONDUCTOR_TIER = {{ tier_name | tojson }};
      // KL-76: early-access mode forces telemetry opt-in. Server-
      // rendered so the modal can render the locked-toggle variant
      // on first paint without waiting on a fetch.
      window.CONDUCTOR_EARLY_ACCESS = {{ early_access_mode | tojson }};
      // KL-89: PostHog anonymous ID exposed to JS so upgrade CTAs can
      // pass it to the LemonSqueezy checkout via custom_data, closing
      // the funnel attribution loop (in-app cta_clicked → LS-relayed
      // checkout.completed both share the same distinct_id).
      window.CONDUCTOR_ANON_ID = {{ anon_id | tojson }};
    </script>
</head>
<body x-data>
    <aside class="shell-sidebar">
        <div class="shell-brand">
            <span class="brand-word" title="{{ brand }}">{{ brand }}</span>
            {% if early_access_mode %}
            {# KL-177: persistent early-access marker. Pill is non-dismissible
               by design — its job is to make EA status impossible to forget.
               Click jumps to the Help tab where the Feedback flow lives, so
               a user noticing the pill has a one-click path to "send feedback
               about what I'm seeing." #}
            <button type="button" class="early-access-pill"
                    aria-label="Early Access build — click to send feedback"
                    title="Early Access build — click to open Help / Feedback"
                    x-on:click="$store.tabs.activate('help')">
                EARLY ACCESS
            </button>
            {% endif %}
        </div>
        {% if tier_name == "starter" %}
        <a class="shell-tier-badge" href="https://conductor.lighting/pricing.html" data-upgrade-surface="sidebar_badge" target="_blank" rel="noopener" title="Upgrade to Pro">
            <span class="tier-badge-label">Starter</span>
            <span class="tier-badge-teaser" id="tier-badge-teaser" aria-hidden="true"></span>
            <span class="tier-badge-cta">Upgrade ↗</span>
        </a>
        {% endif %}
        <nav class="tab-rail" role="tablist" aria-label="Dashboard sections">
            <button class="tab-btn" role="tab" data-tab="show" id="tab-btn-show"
                    :class="{ 'active': $store.tabs.active === 'show' }"
                    :aria-selected="$store.tabs.active === 'show' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('show')">
                <span class="tab-glyph" aria-hidden="true">▶</span><span class="tab-label">Show</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="log" id="tab-btn-log"
                    :class="{ 'active': $store.tabs.active === 'log' }"
                    :aria-selected="$store.tabs.active === 'log' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('log')">
                <span class="tab-glyph" aria-hidden="true">≡</span><span class="tab-label">Log</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="dmx" id="tab-btn-dmx"
                    :class="{ 'active': $store.tabs.active === 'dmx' }"
                    :aria-selected="$store.tabs.active === 'dmx' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('dmx')">
                <span class="tab-glyph" aria-hidden="true">⚡</span><span class="tab-label">DMX Settings</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="fixtures" id="tab-btn-fixtures"
                    :class="{ 'active': $store.tabs.active === 'fixtures' }"
                    :aria-selected="$store.tabs.active === 'fixtures' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('fixtures')">
                <span class="tab-glyph" aria-hidden="true">◆</span><span class="tab-label">DMX Fixtures</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="govee" id="tab-btn-govee"
                    :class="{ 'active': $store.tabs.active === 'govee' }"
                    :aria-selected="$store.tabs.active === 'govee' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('govee')">
                <span class="tab-glyph" aria-hidden="true">✦</span><span class="tab-label">Govee</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="director" id="tab-btn-director"
                    :class="{ 'active': $store.tabs.active === 'director' }"
                    :aria-selected="$store.tabs.active === 'director' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('director')">
                <span class="tab-glyph" aria-hidden="true">✧</span><span class="tab-label">Director AI</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="midi" id="tab-btn-midi"
                    :class="{ 'active': $store.tabs.active === 'midi' }"
                    :aria-selected="$store.tabs.active === 'midi' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('midi')">
                <span class="tab-glyph" aria-hidden="true">♫</span><span class="tab-label">MIDI</span>
            </button>
            <button class="tab-btn" role="tab" data-tab="settings" id="tab-btn-settings"
                    :class="{ 'active': $store.tabs.active === 'settings' }"
                    :aria-selected="$store.tabs.active === 'settings' ? 'true' : 'false'"
                    x-on:click="$store.tabs.activate('settings')">
                <span class="tab-glyph" aria-hidden="true">⚙</span><span class="tab-label">Settings</span>
            </button>
        </nav>
        <button class="shell-updates tab-btn" role="tab" data-tab="updates" id="tab-btn-updates" data-update-state="loading" title="App Updates"
                :class="{ 'active': $store.tabs.active === 'updates' }"
                :aria-selected="$store.tabs.active === 'updates' ? 'true' : 'false'"
                x-on:click="$store.tabs.activate('updates')">
            <span class="shell-updates-row">
                <span class="tab-glyph" aria-hidden="true">⇅</span><span class="tab-label">App Updates</span>
            </span>
            <span class="shell-updates-status" id="updates-tab-status">Checking…</span>
        </button>
        <button class="shell-help tab-btn" role="tab" data-tab="help" id="tab-btn-help" title="Help &amp; feedback"
                :class="{ 'active': $store.tabs.active === 'help' }"
                :aria-selected="$store.tabs.active === 'help' ? 'true' : 'false'"
                x-on:click="$store.tabs.activate('help')">
            <span class="tab-glyph" aria-hidden="true">?</span><span class="tab-label">Help</span>
        </button>
        <button class="shell-about tab-btn" role="tab" data-tab="about" id="tab-btn-about" title="About {{ brand }}"
                :class="{ 'active': $store.tabs.active === 'about' }"
                :aria-selected="$store.tabs.active === 'about' ? 'true' : 'false'"
                x-on:click="$store.tabs.activate('about')">
            <span class="tab-glyph" aria-hidden="true">i</span><span class="tab-label">About</span>
        </button>
    </aside>

    <!-- Toast stack — server notifications + Mac-audio onboarding share this -->
    <div class="toast-stack" id="toast-stack" aria-live="polite" x-data>
        <template x-for="t in $store.toasts.visible" :key="t.key">
            <div class="toast"
                 :class="['level-' + $store.toasts.severityOf(t.level), { 'is-expanded': t.expanded, 'is-dismissing': t.dismissing }]"
                 x-on:click="$store.toasts.toggleExpanded(t.key)"
                 x-on:mouseenter="$store.toasts.pauseTimer(t.key)"
                 x-on:mouseleave="$store.toasts.resumeTimer(t.key)">
                <div class="toast-body">
                    <div class="toast-title" x-text="t.title || ''"></div>
                    <div class="toast-detail" x-show="!!t.detail" x-text="t.detail || ''"></div>
                    <div class="toast-actions" x-show="t.actions && t.actions.length">
                        <template x-for="a in t.actions" :key="a.id">
                            <span style="display: contents">
                                <a x-show="!!a.href" class="action-btn"
                                   :href="a.href || '#'"
                                   :target="a.target || ''"
                                   rel="noopener noreferrer"
                                   :data-action="a.id"
                                   x-text="a.label"
                                   x-on:click.stop="$store.toasts.runAction(t.key, a.id)"></a>
                                <button x-show="!a.href" class="action-btn" type="button"
                                        :data-action="a.id"
                                        x-text="a.label"
                                        x-on:click.stop="$store.toasts.runAction(t.key, a.id)"></button>
                            </span>
                        </template>
                    </div>
                </div>
                <button x-show="t.dismissible !== false" class="toast-dismiss"
                        title="Dismiss" aria-label="Dismiss"
                        x-on:click.stop="$store.toasts.dismiss(t.key)">&times;</button>
            </div>
        </template>
        <button x-show="$store.toasts.overflowCount > 0 && !$store.toasts.overflowExpanded"
                class="toast-overflow-more"
                type="button"
                x-on:click="$store.toasts.toggleOverflow()"
                x-text="'+' + $store.toasts.overflowCount + ' more'"></button>
    </div>

    <main class="shell-main">
        <!-- Floating live-show controls, anchored to the main area, always reachable -->
        <div class="show-quick-actions" role="group" aria-label="Live show controls">
            <button class="action-btn engine-playpause" id="btn-engine-playpause" title="Play/Pause: freezes the show, fixtures hold their current state" data-state="stopped">▶ PLAY</button>
            <button class="action-btn engine-stop" id="btn-engine-stop" title="Emergency stop: instant blackout + pan/tilt home + Govee black">■ STOP</button>
            <span class="qa-divider" aria-hidden="true"></span>
            <button class="action-btn danger" id="btn-blackout" title="Black out all fixtures">BLACKOUT</button>
            <button class="action-btn flash" id="btn-flash" title="Full white flash">FLASH</button>
        </div>

        <div class="shell-panels">
        <div class="tab-panel" data-tab-panel="show" role="tabpanel" aria-labelledby="tab-btn-show" :hidden="$store.tabs.active !== 'show'">
            <!-- Director hero — full-width focal point of the Show tab.
                 KL-56: cartoon DJ slot bleeds past the panel's left edge,
                 mood word stays centered, footer collapses the "AI DIRECTOR"
                 label + backend + status onto a single divider-separated
                 line so the mood word owns the visual hierarchy. -->
            <section class="director-panel">
                <!-- Lumi clipped to the panel's rounded shape — the panel
                     bottom edge acts as the "bar," cutting her off at
                     hip/waist level. Her sized to be tall enough that
                     the clip lands at her midsection instead of at her
                     ankles. -->
                <div class="director-cartoon-clip">
                    <!-- Static image — always rendered for emotions that
                         only ship a PNG. For video-enabled emotions (KL-61)
                         this img is hidden via the `hidden` attribute and
                         the <video> below takes over. -->
                    <img class="director-cartoon"
                         id="director-cartoon"
                         src="/static/images/director/lumi-default.png"
                         alt="Lumi, the Conductør AI Director">
                    <!-- Video element (KL-61) — swapped in by setCartoon
                         when an emotion's map entry has a `video` field.
                         The poster doubles as the loading splash and the
                         still-image fallback if the MP4 fails to load.
                         KL-194: two stacked video elements in absolute
                         alignment. setCartoon ping-pongs between them and
                         crossfades via the .is-active class so emotion
                         transitions are smooth instead of an abrupt cut.
                         Neither uses the `hidden` attribute (which would
                         force display:none and skip the opacity transition);
                         visibility is opacity-driven via .is-active. -->
                    <video class="director-cartoon director-cartoon-video"
                           id="director-cartoon-video"
                           muted loop playsinline preload="metadata"
                           aria-hidden="true"></video>
                    <video class="director-cartoon director-cartoon-video"
                           id="director-cartoon-video-b"
                           muted loop playsinline preload="metadata"
                           aria-hidden="true"></video>
                </div>
                <!-- KL-64: band meters, top-right. Compact 1-letter HUD. -->
                <div class="hud-bands" aria-hidden="true">
                    <div class="hud-band" id="band-bass">
                        <span class="hud-band-glyph">B</span>
                        <div class="hud-band-bar"><div class="hud-band-fill" id="fill-bass"></div></div>
                    </div>
                    <div class="hud-band" id="band-mids">
                        <span class="hud-band-glyph">M</span>
                        <div class="hud-band-bar"><div class="hud-band-fill" id="fill-mids"></div></div>
                    </div>
                    <div class="hud-band" id="band-highs">
                        <span class="hud-band-glyph">H</span>
                        <div class="hud-band-bar"><div class="hud-band-fill" id="fill-highs"></div></div>
                    </div>
                </div>
                <!-- KL-67: hardware-status row — DMX + Govee at-a-glance
                     health. Top-LEFT, mirroring the band meters' top-right
                     placement. JS renders the badges + dot strip inside
                     based on the state push. -->
                <div class="hud-systems" id="hud-systems" aria-label="Hardware status"></div>
                <!-- KL-64: stats, bottom-right. Faint, glanceable. -->
                <div class="hud-stats" aria-hidden="true">
                    <div class="hud-stat"><span class="hud-stat-label">LULL</span><span class="hud-stat-value" id="stat-lull">0%</span></div>
                    <div class="hud-stat"><span class="hud-stat-label">BUILDUP</span><span class="hud-stat-value" id="stat-buildup">0.00</span></div>
                    <div class="hud-stat"><span class="hud-stat-label">CONV</span><span class="hud-stat-value" id="stat-convergence">0.00</span></div>
                    <div class="hud-stat"><span class="hud-stat-label">UPTIME</span><span class="hud-stat-value" id="stat-uptime">0:00</span></div>
                </div>
                <div class="director-mood-row">
                    <div class="director-mood" id="director-mood">READY</div>
                </div>
                <div class="director-note" id="director-note">Waiting for first direction...</div>
                <!-- KL-64: meta row moves to bottom-LEFT (under Lumi's
                     clipped feet area) and now hosts the beat-pulse glyph
                     and thinking indicator. -->
                <div class="director-meta">
                    <span class="director-beat-pulse" id="director-beat-pulse" aria-hidden="true"></span>
                    <span class="director-meta-label">LUMI</span>
                    <span class="director-thinking" id="director-thinking" aria-hidden="true">
                        <span class="dot"></span><span class="dot"></span><span class="dot"></span>
                    </span>
                    <span class="director-meta-sep">·</span>
                    <span id="director-backend">--</span>
                    <span class="director-meta-sep">·</span>
                    <span id="director-status">--</span>
                </div>

                <!-- KL-64: 15s decision-cycle countdown bar. Hairline strip
                     across the panel bottom that fills as time elapses since
                     Lumi's last decision; resets on the next decision. -->
                <div class="director-cycle-bar" aria-hidden="true">
                    <div class="director-cycle-fill" id="director-cycle-fill"></div>
                </div>
                <!-- KL-64: full-panel cyan flash overlay for superbeats. -->
                <div class="director-superbeat-flash" id="director-superbeat-flash" aria-hidden="true"></div>

                <!-- KL-110: Lumi personality picker, in-panel. Sits at the
                     bottom of the director panel above the cycle bar so
                     "vibe" reads as a property of Lumi, not a separate
                     control card. JS populates the chips at runtime. -->
                <section class="personality-picker" id="personality-picker" x-data>
                    <div class="personality-picker-label">SELECT LUMI'S VIBE</div>
                    <div class="personality-chips" id="personality-chips" role="radiogroup" aria-label="Lumi personality">
                        <template x-for="p in $store.personalities.items" :key="p.id">
                            <button type="button"
                                    role="radio"
                                    class="personality-chip"
                                    :class="{ 'active': p.id === $store.personalities.activeId, 'locked': $store.personalities.isLocked(p.id) }"
                                    :aria-checked="p.id === $store.personalities.activeId"
                                    :aria-label="p.name + ': ' + p.voice"
                                    :title="p.voice"
                                    :style="'--chip-accent: ' + p.accent"
                                    x-on:click="$store.personalities.setActive(p.id)"
                                    x-on:mouseenter="$dispatch('show-fingerprint', { id: p.id, anchor: $el })"
                                    x-on:focus="$dispatch('show-fingerprint', { id: p.id, anchor: $el })"
                                    x-on:mouseleave="$dispatch('hide-fingerprint')"
                                    x-on:blur="$dispatch('hide-fingerprint')">
                                <span class="personality-chip-glyph" x-text="p.glyph"></span>
                                <span class="personality-chip-name" x-text="p.name"></span>
                            </button>
                        </template>
                    </div>
                    <!-- KL-117: fingerprint popover. Hovering a chip dispatches
                         show-fingerprint, which the picker listener uses to
                         populate this with 4 mini-bars (Strobe / Movement /
                         Color / Restraint) positioned just above the chip. -->
                    <div class="personality-fingerprint-popover"
                         id="personality-fingerprint-popover" hidden></div>
                </section>
            </section>

            <!-- Lumi Console (KL-65) — vertical lighting-board faders
                 that visualize every param Lumi adjusts. Read-only;
                 values driven by the WebSocket state push. Three
                 grouped clusters mirror real LD console layouts:
                 ENGINE (4 character knobs), COLOR (3 bipolar hue
                 shifts with center detents), WASH (PAR speed solo).
                 Ghost-touch animation from KL-63 still applies. -->
            <section class="controls lumi-console" aria-label="Lumi Control Monitor">
                <!-- KL-201: header chip + subtitle so people don't try to grab
                     the (read-only, disabled) faders thinking the rig's broken.
                     Chip reads from across the room, subtitle reads up close. -->
                <div class="lumi-console-header">
                    <h2>Lumi Control Monitor</h2>
                    <span class="lumi-console-chip" aria-hidden="true">
                        <span class="lumi-console-chip-dot"></span>
                        <span class="lumi-console-chip-text">LIVE &middot; LUMI'S DRIVING</span>
                    </span>
                </div>
                <p class="lumi-console-subtitle">Live mirror of Lumi's decisions. Watch, don't touch.</p>
                <div class="console-banks">
                    <div class="console-group" data-group="engine">
                        <div class="console-group-label">ENGINE</div>
                        <div class="fader-bank">
                            <div class="fader-column" id="slider-strobe">
                                <div class="fader-label">STROBE</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="0" max="200" value="100" id="ctrl-strobe" disabled>
                                </div>
                                <div class="fader-readout" id="val-strobe">1.00</div>
                            </div>
                            <div class="fader-column" id="slider-movement">
                                <div class="fader-label">MOVE</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="30" max="250" value="100" id="ctrl-movement" disabled>
                                </div>
                                <div class="fader-readout" id="val-movement">1.00</div>
                            </div>
                            <div class="fader-column" id="slider-release">
                                <div class="fader-label">RELEASE</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="2" max="15" value="5" id="ctrl-release" disabled>
                                </div>
                                <div class="fader-readout" id="val-release">0.05</div>
                            </div>
                            <div class="fader-column" id="slider-superbeat">
                                <div class="fader-label">SBEAT</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="40" max="90" value="65" id="ctrl-superbeat" disabled>
                                </div>
                                <div class="fader-readout" id="val-superbeat">0.65</div>
                            </div>
                        </div>
                    </div>
                    <div class="console-group" data-group="color">
                        <div class="console-group-label">COLOR</div>
                        <div class="fader-bank">
                            <div class="fader-column fader-bipolar" id="slider-hue-bass">
                                <div class="fader-label">BASS</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="-50" max="50" value="0" id="ctrl-hue-bass" disabled>
                                </div>
                                <div class="fader-readout" id="val-hue-bass">0.00</div>
                            </div>
                            <div class="fader-column fader-bipolar" id="slider-hue-mids">
                                <div class="fader-label">MIDS</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="-50" max="50" value="0" id="ctrl-hue-mids" disabled>
                                </div>
                                <div class="fader-readout" id="val-hue-mids">0.00</div>
                            </div>
                            <div class="fader-column fader-bipolar" id="slider-hue-highs">
                                <div class="fader-label">HIGHS</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="-50" max="50" value="0" id="ctrl-hue-highs" disabled>
                                </div>
                                <div class="fader-readout" id="val-hue-highs">0.00</div>
                            </div>
                        </div>
                    </div>
                    <div class="console-group" data-group="wash">
                        <div class="console-group-label">WASH</div>
                        <div class="fader-bank">
                            <div class="fader-column" id="slider-par-wash">
                                <div class="fader-label">PAR</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="3" max="40" value="8" id="ctrl-par-wash" disabled>
                                </div>
                                <div class="fader-readout" id="val-par-wash">0.0008</div>
                            </div>
                        </div>
                    </div>
                    <!-- KL-136: optical knobs (zoom/iris/frost/CCT) for fixtures
                         with the channels. Inert on rigs that don't have them. -->
                    <div class="console-group" data-group="optics">
                        <div class="console-group-label">OPTICS</div>
                        <div class="fader-bank">
                            <div class="fader-column" id="slider-zoom">
                                <div class="fader-label">ZOOM</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="0" max="100" value="0" id="ctrl-zoom" disabled>
                                </div>
                                <div class="fader-readout" id="val-zoom">0.00</div>
                            </div>
                            <div class="fader-column" id="slider-iris">
                                <div class="fader-label">IRIS</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="0" max="100" value="50" id="ctrl-iris" disabled>
                                </div>
                                <div class="fader-readout" id="val-iris">0.50</div>
                            </div>
                            <div class="fader-column" id="slider-frost">
                                <div class="fader-label">FROST</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="0" max="100" value="0" id="ctrl-frost" disabled>
                                </div>
                                <div class="fader-readout" id="val-frost">0.00</div>
                            </div>
                            <div class="fader-column fader-bipolar" id="slider-cct">
                                <div class="fader-label">CCT</div>
                                <div class="fader-track-wrap">
                                    <input type="range" min="-100" max="100" value="0" id="ctrl-cct" disabled>
                                </div>
                                <div class="fader-readout" id="val-cct">0.00</div>
                            </div>
                        </div>
                    </div>
                </div>
            </section>

        </div><!-- /tab-panel: show -->

        <div class="tab-panel" data-tab-panel="log" role="tabpanel" aria-labelledby="tab-btn-log" :hidden="$store.tabs.active !== 'log'">
            <section class="log-panel">
                <div class="log-header">
                    <h2>Event Log</h2>
                    <button class="action-btn log-copy-btn" id="btn-log-copy" title="Copy log to clipboard" aria-label="Copy log to clipboard">
                        <span class="log-copy-icon" aria-hidden="true">⎘</span>
                        <span class="log-copy-label">Copy</span>
                    </button>
                </div>
                <div class="log-entries" id="log-entries">
                    <!-- Populated by JS — newest on top -->
                </div>
            </section>
        </div><!-- /tab-panel: log -->

        <div class="tab-panel" data-tab-panel="fixtures" role="tabpanel" aria-labelledby="tab-btn-fixtures" :hidden="$store.tabs.active !== 'fixtures'">
            <section class="fixtures-panel">
                <div class="fixtures-header">
                    <h2>Fixtures</h2>
                    <div class="fixtures-header-actions">
                        <input type="search" id="fixture-filter" class="fixture-filter" placeholder="Filter by name, template, or role…" spellcheck="false"
                               x-model="$store.fixtures.filter">
                        <button class="action-btn" id="btn-fixture-discover" title="Walk the rig over RDM and pre-fill discovered fixtures"
                                x-on:click="$store.dmxDiscover.open()">Autodiscover Fixtures</button>
                        <button class="action-btn" id="btn-fixture-add"
                                x-on:click="$store.fixtures.openAdd()">+ Add fixture</button>
                    </div>
                </div>
                <div class="restart-banner" id="fixtures-restart-banner" hidden
                     :hidden="!$store.fixtures.restartPending">
                    <strong>Restart required</strong>
                    <span>Your edits are saved to config.yaml. Quit and relaunch Conductør for them to take effect.</span>
                </div>
                <!-- KL-154: visual universe map. One strip per universe in use.
                     Click a fixture block → opens Edit. Click a gap → opens Add
                     pre-filled with that start address. -->
                <section class="universe-map" x-show="$store.fixtures.universesInUse.length > 0"
                         aria-label="DMX universe map">
                    <div class="universe-map-header">
                        <span class="universe-map-title">Channel map</span>
                        <span class="universe-map-legend">
                            <span class="universe-legend-swatch role-bass_mover"></span> bass
                            <span class="universe-legend-swatch role-mid_wash"></span> mids
                            <span class="universe-legend-swatch role-high_accent"></span> highs
                            <span class="universe-legend-swatch role-ambient"></span> ambient
                        </span>
                    </div>
                    <template x-for="u in $store.fixtures.universesInUse" :key="u">
                        <div class="universe-strip" :class="{ 'has-collisions': $store.fixtures.universeCollisionCount(u) > 0 }">
                            <span class="universe-label" x-text="`U${u}`"></span>
                            <div class="universe-bar-stack">
                            <div class="universe-channels">
                                <template x-for="(seg, i) in $store.fixtures.buildUniverseSegments(u)" :key="i">
                                    <button type="button"
                                            class="universe-segment"
                                            :class="seg.type === 'fixture'
                                                    ? ('universe-block role-' + seg.role + (seg.overlap ? ' is-overlap' : ''))
                                                    : 'universe-gap'"
                                            :style="`flex-grow: ${seg.channels}`"
                                            :title="seg.type === 'fixture'
                                                    ? (seg.overlap
                                                        ? `⚠ ${seg.name} (ch ${seg.start}-${seg.end}) overlaps another fixture. Re-address to fix.`
                                                        : `${seg.name} (ch ${seg.start}-${seg.end}, ${seg.channels} channels, role: ${seg.role})`)
                                                    : `${seg.channels} free channels (${seg.start}-${seg.end}). Click to add a fixture here.`"
                                            x-on:click="seg.type === 'fixture'
                                                        ? $store.fixtures.openEdit(seg.index)
                                                        : $store.fixtures.openAddAtGap(u, seg.start)">
                                        <span class="universe-segment-label" x-show="seg.type === 'fixture' && seg.channels >= 8"
                                              x-text="seg.overlap ? `⚠ ${seg.name}` : seg.name"></span>
                                        <!-- KL-175: collision callouts live inside the segment
                                             so they anchor to the visual center of the actual
                                             rendered block — flex-basis:auto distorts pure
                                             address-to-pixel mapping when buttons have text. -->
                                        <template x-for="(b, bi) in (seg.bubbles || [])" :key="b.id">
                                            <span class="collision-bubble"
                                                  :style="`top: calc(100% + 0.5rem + ${bi} * 1.65rem)`"
                                                  :title="b.detail"
                                                  x-on:click.stop="$store.fixtures.openEdit(seg.index)">
                                                <span class="collision-bubble-range" x-text="b.rangeText"></span>
                                                <span class="collision-bubble-label" x-text="b.label"></span>
                                            </span>
                                        </template>
                                    </button>
                                </template>
                            </div>
                            <!-- KL-196: tick scale below the channels bar.
                                 Universe-agnostic — same ticks for every
                                 universe. Endpoints at 1 and 512 anchor
                                 to bar edges; majors every 50 carry a
                                 number label; minors every 10 are
                                 unlabeled visual reference. -->
                            <div class="universe-ticks" aria-hidden="true">
                                <template x-for="(t, ti) in $store.fixtures.universeTicks" :key="ti">
                                    <span :class="`universe-tick universe-tick-${t.kind}`"
                                          :style="`left: ${t.pos}%`">
                                        <span class="universe-tick-line"></span>
                                        <span class="universe-tick-label" x-show="t.label" x-text="t.label"></span>
                                    </span>
                                </template>
                            </div>
                            </div>
                            <span class="universe-collisions-pill"
                                  x-show="$store.fixtures.universeCollisionCount(u) > 0"
                                  x-text="`⚠ ${$store.fixtures.universeCollisionCount(u)} collision${$store.fixtures.universeCollisionCount(u) === 1 ? '' : 's'}`"></span>
                        </div>
                    </template>
                </section>
                <div class="fixture-list" id="fixture-list">
                    <template x-if="$store.fixtures.showEmpty">
                        <div class="fixtures-empty">No fixtures yet. Click + Add fixture to stamp your first one.</div>
                    </template>
                    <template x-if="$store.fixtures.showFilteredEmpty">
                        <div class="fixtures-empty" x-text="`No fixtures match \"${$store.fixtures.filter}\".`"></div>
                    </template>
                    <template x-for="row in $store.fixtures.visibleFixtures" :key="row.idx">
                        <!-- KL-157: role-* class drives the left-border color stripe so
                             a fixture's row visually links to its block on the universe
                             map. Same color tokens as .universe-block .role-* — single
                             source of truth in features/fixtures.css. errored state
                             overrides the role stripe (red beats role color). -->
                        <div class="fixture-row"
                             :class="['role-' + (row.fixture.role || 'ambient'), { 'errored': row.errors.length > 0 }]"
                             :data-idx="row.idx"
                             x-on:click="$store.fixtures.openEdit(row.idx)">
                            <div class="fx-name" x-text="row.fixture.name || '(unnamed)'"></div>
                            <div class="fx-badge"
                                 :class="{ 'fx-custom': row.isCustom }"
                                 x-text="row.badgeText"></div>
                            <div class="fx-addr" x-text="row.addrText"></div>
                            <div class="fx-role" x-text="row.fixture.role || 'ambient'"></div>
                            <button class="fx-delete"
                                    x-on:click.stop="$store.fixtures.deleteFixture(row.idx, row.fixture.name || '(unnamed)')">Delete</button>
                            <template x-if="row.errors.length > 0">
                                <div class="fx-error-line" x-text="row.errorText"></div>
                            </template>
                        </div>
                    </template>
                </div>
            </section>

            <!-- Edit Fixture Modal -->
            <div class="modal-overlay" id="fixture-edit-modal" hidden
                 :hidden="!$store.fixtures.editShown"
                 x-on:click.self="$store.fixtures.closeEdit()">
                <div class="modal-dialog modal-wide" role="dialog" aria-labelledby="fixture-edit-title" aria-modal="true">
                    <button class="modal-close" id="btn-fixture-edit-close" aria-label="Close"
                            x-on:click="$store.fixtures.closeEdit()">&times;</button>
                    <h3 id="fixture-edit-title">Edit fixture</h3>
                    <p class="modal-intro" id="fx-edit-origin" x-text="$store.fixtures.editOriginText"></p>

                    <div class="form-row">
                        <label for="fx-edit-name">Name</label>
                        <input type="text" id="fx-edit-name" spellcheck="false"
                               x-model="$store.fixtures.editForm.name">
                    </div>

                    <div class="form-row form-row-inline">
                        <div>
                            <label for="fx-edit-universe">Universe</label>
                            <input type="number" id="fx-edit-universe" min="0"
                                   x-model.number="$store.fixtures.editForm.universe">
                        </div>
                        <div>
                            <label for="fx-edit-address">Address</label>
                            <input type="number" id="fx-edit-address" min="1" max="512"
                                   :class="{'address-conflict': $store.fixtures.editAddressOverlap}"
                                   x-model.number="$store.fixtures.editForm.address">
                        </div>
                        <div>
                            <!-- KL-149: lock-by-default role with override button.
                                 Inline-row layout matches the surrounding form-row-inline
                                 cells. -->
                            <label for="fx-edit-role" x-text="$store.fixtures.editRoleLabel"></label>
                            <div class="role-row">
                                <select id="fx-edit-role"
                                        x-model="$store.fixtures.editForm.role"
                                        :disabled="$store.fixtures.editForm.roleLocked">
                                    <option value="bass_mover">bass_mover</option>
                                    <option value="mid_wash">mid_wash</option>
                                    <option value="high_accent">high_accent</option>
                                    <option value="ambient">ambient</option>
                                </select>
                                <button type="button" class="action-btn role-override-btn"
                                        x-show="$store.fixtures.showEditRoleOverrideButton"
                                        x-on:click="$store.fixtures.unlockEditRole()">Override</button>
                            </div>
                        </div>
                    </div>
                    <div class="form-hint role-override-warn"
                         x-show="!!$store.fixtures.editingEntry?.stamped_from && !$store.fixtures.editForm.roleLocked">
                        Overriding the role of a fixture can break show immersion.
                    </div>
                    <!-- KL-155: overlap warning under the address row. -->
                    <div class="form-hint setting-warning" x-show="$store.fixtures.editAddressOverlap"
                         x-text="$store.fixtures.editAddressOverlap
                                 ? `Overlaps with ${$store.fixtures.editAddressOverlap.name} (channels ${$store.fixtures.editAddressOverlap.start}–${$store.fixtures.editAddressOverlap.end}). Pick another address.`
                                 : ''"
                         style="margin-top: -0.4rem;"></div>

                    <div class="form-row">
                        <label>Channel map</label>
                        <div class="channel-editor" id="fx-edit-channels">
                            <template x-for="(ch, idx) in $store.fixtures.editChannels" :key="idx">
                                <div class="channel-row">
                                    <span class="ch-num" x-text="idx + 1"></span>
                                    <input type="text" class="ch-type" list="channel-type-suggestions"
                                           x-model="ch.type" placeholder="channel type">
                                    <input type="number" class="ch-default"
                                           x-model="ch.default" placeholder="default" min="0" max="255">
                                    <!-- KL-146: invert checkbox, only meaningful for direction-flippable
                                         types (zoom/iris/frost/cct/dimmer). Hidden via x-show otherwise. -->
                                    <label class="ch-invert" x-show="$store.fixtures.isInvertibleChannelType(ch.type)"
                                           title="Flip the channel direction (e.g., for fixtures where iris-high = closed instead of open)">
                                        <input type="checkbox" x-model="ch.invert">
                                        <span>Invert</span>
                                    </label>
                                    <button class="ch-delete" type="button" aria-label="Remove channel"
                                            x-on:click="$store.fixtures.removeChannelRow(idx)">&times;</button>
                                    <div class="ch-hint" x-text="$store.fixtures.channelTypeHintFor(ch.type)"></div>
                                </div>
                            </template>
                        </div>
                        <button class="action-btn channel-add" id="btn-channel-add" type="button"
                            x-on:click="$store.fixtures.addChannelRow()">+ Add channel</button>
                    </div>

                    <datalist id="channel-type-suggestions">
                        <template x-for="t in $store.fixtures.channelTypes" :key="t.type">
                            <option :value="t.type"></option>
                        </template>
                    </datalist>

                    <div class="modal-actions">
                        <button class="action-btn modal-primary" id="btn-fixture-edit-save"
                                :disabled="$store.fixtures.editBusy || !!$store.fixtures.editAddressOverlap"
                                x-on:click="$store.fixtures.saveEdit()">Save changes</button>
                        <button class="action-btn" id="btn-fixture-restamp" hidden
                                :hidden="!$store.fixtures.showRestamp"
                                x-on:click="$store.fixtures.restamp()">Re-stamp from template</button>
                        <button class="action-btn" id="btn-fixture-rdm-repatch" hidden title="Push the current address to this fixture over RDM"
                                :hidden="!$store.fixtures.showRdmRepatch"
                                :disabled="$store.fixtures.rdmRepatchBusy"
                                x-text="$store.fixtures.rdmRepatchLabel"
                                x-on:click="$store.fixtures.rdmRepatch()">Push address (RDM)</button>
                        <button class="action-btn" id="btn-fixture-edit-cancel"
                                x-on:click="$store.fixtures.closeEdit()">Cancel</button>
                    </div>
                    <div class="modal-warning" id="fx-edit-error" hidden
                         :hidden="!$store.fixtures.editError"
                         x-text="$store.fixtures.editError"></div>
                </div>
            </div>

            <!-- Autodiscover Fixtures Modal (RDM) -->
            <div class="modal-overlay" id="fixture-discover-modal" hidden
                 :hidden="!$store.dmxDiscover.shown"
                 x-on:click.self="$store.dmxDiscover.close()">
                <div class="modal-dialog modal-wide" role="dialog" aria-labelledby="fixture-discover-title" aria-modal="true">
                    <button class="modal-close" id="btn-fixture-discover-close" aria-label="Close"
                            x-on:click="$store.dmxDiscover.close()">&times;</button>
                    <h3 id="fixture-discover-title">Autodiscover Fixtures</h3>
                    <p class="modal-intro" id="fx-discover-intro">Walks your rig over RDM and pre-fills any fixtures that respond.</p>
                    <div class="fx-discover-status" id="fx-discover-status" hidden
                         :hidden="!$store.dmxDiscover.showStatus"
                         :class="{ 'is-busy': $store.dmxDiscover.statusBusy }"
                         x-text="$store.dmxDiscover.statusText"></div>
                    <!-- KL-162: destructive-action warning. Only renders when at
                         least one kept-and-not-keep-existing row has overlap
                         candidates. The single warning surface is the contract:
                         clicking Add commits the overwrites listed below. -->
                    <div class="fx-discover-overlap-banner modal-warning"
                         x-show="$store.dmxDiscover.hasOverwriteOverlaps">
                        ⚠ <strong>This will overwrite existing fixtures.</strong>
                        Some discovered fixtures use the same DMX addresses as fixtures already in
                        your config — clicking <strong>Add</strong> will replace
                        <span x-text="$store.dmxDiscover.overwriteCount"></span>
                        existing entr<span x-text="$store.dmxDiscover.overwriteCount === 1 ? 'y' : 'ies'"></span>.
                        Toggle <em>Keep existing</em> on any row below to skip its overwrite.
                    </div>
                    <div class="fx-discover-results" id="fx-discover-results" hidden
                         :hidden="!$store.dmxDiscover.showResults">
                        <div class="fx-discover-row fx-discover-header">
                            <span class="fx-d-keep">Add</span>
                            <span class="fx-d-info">Fixture</span>
                            <span class="fx-d-addr">Patch</span>
                            <span class="fx-d-template">Profile</span>
                            <span class="fx-d-role">Role</span>
                        </div>
                        <template x-for="(f, idx) in $store.dmxDiscover.results" :key="f.uid">
                            <div class="fx-discover-row"
                                 :class="{ 'fx-discover-row-overwrite': f._keep && !f._keep_existing && (f.overlap_candidates || []).length > 0 }"
                                 :data-idx="idx">
                                <label class="fx-d-keep"><input type="checkbox" x-model="f._keep"></label>
                                <div class="fx-d-info">
                                    <div class="fx-d-name">
                                        <strong x-text="f.manufacturer || 'Unknown manufacturer'"></strong>
                                        <span x-text="' ' + (f.model || '')"></span>
                                    </div>
                                    <div class="fx-d-meta">
                                        UID <span x-text="f.uid"></span><span x-text="$store.dmxDiscover.fixturePersonalityLabel(f)"></span><span x-text="$store.dmxDiscover.fixtureSourceLabel(f)"></span>
                                    </div>
                                    <!-- KL-162: per-row overwrite detail + opt-out toggle.
                                         Visible only when this row is being kept AND has
                                         at least one overlap candidate. Toggling "Keep
                                         existing" flips _keep_existing → row contributes
                                         no replace_indices on submit, discovered fixture
                                         is skipped entirely. -->
                                    <div class="fx-d-overwrite"
                                         x-show="f._keep && (f.overlap_candidates || []).length > 0">
                                        <span class="fx-d-overwrite-warning"
                                              x-show="!f._keep_existing">
                                            ⚠ Will replace:
                                            <template x-for="(c, ci) in f.overlap_candidates" :key="c.index">
                                                <span>
                                                    <strong x-text="c.name"></strong>
                                                    <span x-text="`(ch ${c.address}–${c.address + c.channels - 1})`"></span><span x-show="ci < f.overlap_candidates.length - 1">, </span>
                                                </span>
                                            </template>
                                        </span>
                                        <span class="fx-d-overwrite-skipped"
                                              x-show="f._keep_existing">
                                            Existing fixtures kept; this discovered entry will be skipped.
                                        </span>
                                        <label class="fx-d-overwrite-toggle">
                                            <input type="checkbox" x-model="f._keep_existing">
                                            Keep existing instead
                                        </label>
                                    </div>
                                </div>
                                <div class="fx-d-addr" x-text="$store.dmxDiscover.fixturePatchText(f)"></div>
                                <select class="fx-d-template" x-model="f._template_id">
                                    <option value="">Unknown — pick a profile</option>
                                    <template x-for="t in $store.dmxDiscover.templates" :key="t.id">
                                        <option :value="t.id" x-text="$store.dmxDiscover.templateOptionLabel(t)"></option>
                                    </template>
                                </select>
                                <select class="fx-d-role" x-model="f._role">
                                    <template x-for="r in $store.dmxDiscover.roles" :key="r">
                                        <option :value="r" x-text="r"></option>
                                    </template>
                                </select>
                            </div>
                        </template>
                    </div>
                    <div class="fx-discover-empty" id="fx-discover-empty" hidden
                         :hidden="!$store.dmxDiscover.showEmpty">
                        <p>No fixtures responded.</p>
                        <p class="form-hint">RDM requires fixture-side support. Many sub-$150 LED pars and dimmer packs don't implement it. Add those manually with <strong>+ Add fixture</strong>. Mid/pro fixtures (most moving heads, higher-end LED bars) usually do — check that the fixture is powered on, addressed on the same universe, and that your Art-Net node has RDM enabled.</p>
                        <!-- KL-159: collapsible deeper diagnostic. Native <details> for a11y +
                             zero JS. Default-collapsed so the empty state stays digestible;
                             users who want to dig get the DMXking-specific note + FAQ link. -->
                        <details class="fx-discover-troubleshoot">
                            <summary>Why didn't anything show up?</summary>
                            <p>
                                The most common cause after fixture-side support is the Art-Net node's RDM settings.
                                <strong>DMXking</strong> nodes (eDMX1/eDMX2/eDMX MAX) ship with
                                <strong>Discovery Period = 0s</strong>, which disables their autonomous discovery.
                                Conductør now triggers discovery on demand so the scan should still work,
                                but raising it to 30s in the eDMX MAX configuration tool makes future
                                scans noticeably faster and helps when fixtures are flaky.
                            </p>
                            <p>
                                Other usual suspects: fixture is powered off, RDM is disabled in the node
                                config, the node is on a different subnet, or your firewall is blocking
                                UDP 6454. Daisy-chain cables that work fine for plain DMX can still
                                drop RDM responses if they're long or non-DMX-rated.
                            </p>
                            <p>
                                <a href="https://conductor.lighting/faq.html#rdm-discovery"
                                   target="_blank" rel="noopener noreferrer">Full RDM troubleshooting guide →</a>
                            </p>
                        </details>
                    </div>
                    <div class="modal-actions">
                        <button class="action-btn modal-primary" id="btn-fixture-discover-run"
                                :hidden="!$store.dmxDiscover.showRunBtn"
                                :disabled="$store.dmxDiscover.runBtnDisabled"
                                x-text="$store.dmxDiscover.runBtnLabel"
                                x-on:click="$store.dmxDiscover.runScan()">Start scan</button>
                        <button class="action-btn modal-primary" id="btn-fixture-discover-add" hidden
                                :hidden="!$store.dmxDiscover.showAddBtn"
                                :disabled="$store.dmxDiscover.addBtnDisabled"
                                x-text="$store.dmxDiscover.addBtnLabel"
                                x-on:click="$store.dmxDiscover.addSelected()">Add 0 fixtures</button>
                        <button class="action-btn" id="btn-fixture-discover-cancel"
                                x-on:click="$store.dmxDiscover.close()">Close</button>
                    </div>
                    <div class="modal-warning" id="fx-discover-error" hidden
                         :hidden="!$store.dmxDiscover.showError"
                         x-text="$store.dmxDiscover.errorText"></div>
                </div>
            </div>

            <!-- Add Fixture Modal -->
            <div class="modal-overlay" id="fixture-add-modal" hidden
                 :hidden="!$store.fixtures.addShown"
                 x-on:click.self="$store.fixtures.closeAdd()">
                <div class="modal-dialog modal-wide" role="dialog" aria-labelledby="fixture-add-title" aria-modal="true">
                    <button class="modal-close" id="btn-fixture-add-close" aria-label="Close"
                            x-on:click="$store.fixtures.closeAdd()">&times;</button>
                    <h3 id="fixture-add-title">Add a fixture</h3>
                    <p class="modal-intro">Stamp from a template to pre-fill the channel map, or start from Custom and build from scratch.</p>

                    <div class="form-row">
                        <label for="fx-template">Template</label>
                        <input type="search" id="fx-template-filter" class="fixture-filter" placeholder="Filter templates by name, manufacturer, or category…" spellcheck="false"
                               x-model="$store.fixtures.templateFilter"
                               :disabled="$store.fixtures.catalogLoading">
                        <select id="fx-template" x-model="$store.fixtures.addForm.templateId"
                                :disabled="$store.fixtures.catalogLoading"
                                x-on:change="$store.fixtures.refreshSuggestedAddress(); $store.fixtures.applyDefaultRoleForTemplate()">
                            <option value="" x-text="$store.fixtures.catalogLoading ? 'Loading templates…' : 'Custom / Blank (1ch dimmer)'"></option>
                            <template x-for="t in $store.fixtures.addTemplateOptions" :key="t.id">
                                <option :value="t.id" x-text="`${t.name} (${t.manufacturer || '—'}, ${t.channels}ch)`"></option>
                            </template>
                        </select>
                        <div class="form-hint" x-show="$store.fixtures.catalogLoading">
                            <span class="catalog-spinner" aria-hidden="true"></span>
                            Loading <span x-text="$store.fixtures.catalogTemplates.length || ''"></span> fixture templates…
                        </div>
                        <div class="form-hint" x-show="!$store.fixtures.catalogLoading && $store.fixtures.addTemplateOverflowCount > 0"
                             x-text="`Showing first 100 of ${$store.fixtures.catalogTemplates.length}. Type to filter the rest.`"></div>
                        <div class="form-hint" id="fx-template-hint" x-text="$store.fixtures.addTemplateHint"></div>
                    </div>

                    <div class="form-row">
                        <label for="fx-name">Name</label>
                        <input type="text" id="fx-name" placeholder="e.g. Front Left Mover" spellcheck="false"
                               x-model="$store.fixtures.addForm.name">
                        <!-- KL-153: name placeholder hint, only shown when count > 1 -->
                        <div class="form-hint" x-show="($store.fixtures.addForm.count || 1) > 1"
                             style="margin-top: -0.4rem;">
                            Use <code>{n}</code> in the name for sequential numbering (e.g. "Front Wash {n}" → "Front Wash 1", "Front Wash 2"). Otherwise " 1", " 2"… is appended automatically.
                        </div>
                    </div>

                    <div class="form-row form-row-inline">
                        <div>
                            <label for="fx-count">Count</label>
                            <input type="number" id="fx-count" min="1" max="32"
                                   x-model.number="$store.fixtures.addForm.count"
                                   x-on:change="$store.fixtures.refreshSuggestedAddress()">
                        </div>
                        <div>
                            <label for="fx-universe">Universe</label>
                            <input type="number" id="fx-universe" min="0"
                                   x-model.number="$store.fixtures.addForm.universe"
                                   x-on:change="$store.fixtures.refreshSuggestedAddress()">
                        </div>
                        <div>
                            <label for="fx-start-address">Start address</label>
                            <input type="number" id="fx-start-address" placeholder="auto" min="1" max="512"
                                   :class="{'address-conflict': $store.fixtures.addAddressOverlap}"
                                   x-model="$store.fixtures.addForm.startAddress"
                                   x-on:input="$store.fixtures.markStartAddressManual()">
                        </div>
                    </div>
                    <!-- KL-152/KL-153/KL-155: hint copy under the address row. States:
                         (a) overlap → red, names the conflicting fixture, blocks submit
                         (b) universe full → red error, blocks submit
                         (c) auto-suggested → "Auto-suggested next free slot."
                         (d) bulk preview → "5 × 5ch = 25 channels, addresses 1–25"
                         (e) user typed clean address w/ count=1 → no hint -->
                    <div class="form-hint setting-warning" x-show="$store.fixtures.addAddressOverlap"
                         x-text="$store.fixtures.addAddressOverlap
                                 ? `Overlaps with ${$store.fixtures.addAddressOverlap.name} (channels ${$store.fixtures.addAddressOverlap.start}–${$store.fixtures.addAddressOverlap.end}). Pick another address.`
                                 : ''"
                         style="margin-top: -0.4rem;"></div>
                    <div class="form-hint" x-show="!$store.fixtures.addAddressOverlap && $store.fixtures.addForm.startAddressAuto"
                         style="margin-top: -0.4rem;">
                        Auto-suggested next free slot. Edit to override.
                    </div>
                    <div class="form-hint setting-warning" x-show="!$store.fixtures.addAddressOverlap && $store.fixtures.addForm.universeFull"
                         x-text="`Universe ${$store.fixtures.addForm.universe} is full — no room for this fixture. Try another universe.`"
                         style="margin-top: -0.4rem;"></div>
                    <div class="form-hint" x-show="!$store.fixtures.addAddressOverlap && !$store.fixtures.addForm.universeFull && $store.fixtures.bulkBlockPreview"
                         x-text="$store.fixtures.bulkBlockPreview"
                         style="margin-top: -0.4rem;"></div>

                    <!-- KL-149: role row with lock-by-default UX. Templated
                         fixtures show "Default role" + locked dropdown +
                         Override button. Custom fixtures show plain "Role"
                         + editable dropdown (no lock to apply). -->
                    <div class="form-row">
                        <label for="fx-role" x-text="$store.fixtures.addRoleLabel"></label>
                        <div class="role-row">
                            <select id="fx-role"
                                    x-model="$store.fixtures.addForm.role"
                                    :disabled="$store.fixtures.addForm.roleLocked">
                                <option value="bass_mover">bass_mover</option>
                                <option value="mid_wash">mid_wash</option>
                                <option value="high_accent">high_accent</option>
                                <option value="ambient">ambient</option>
                            </select>
                            <button type="button" class="action-btn role-override-btn"
                                    x-show="$store.fixtures.showAddRoleOverrideButton"
                                    x-on:click="$store.fixtures.unlockAddRole()">Override</button>
                        </div>
                        <div class="form-hint role-override-warn"
                             x-show="!!$store.fixtures.addForm.templateId && !$store.fixtures.addForm.roleLocked">
                            Overriding the role of a fixture can break show immersion.
                        </div>
                    </div>

                    <div class="modal-actions">
                        <button class="action-btn modal-primary" id="btn-fixture-save"
                                :disabled="$store.fixtures.addBusy || $store.fixtures.addForm.universeFull || !!$store.fixtures.addAddressOverlap"
                                x-on:click="$store.fixtures.saveAdd()">Save</button>
                        <button class="action-btn" id="btn-fixture-cancel"
                                x-on:click="$store.fixtures.closeAdd()">Cancel</button>
                    </div>
                    <div class="modal-warning" id="fx-error" hidden
                         :hidden="!$store.fixtures.addError"
                         x-text="$store.fixtures.addError"></div>
                </div>
            </div>
        </div><!-- /tab-panel: fixtures -->

        <div class="tab-panel" data-tab-panel="dmx" role="tabpanel" aria-labelledby="tab-btn-dmx" :hidden="$store.tabs.active !== 'dmx'">
            <section class="dmx-panel">
                <h2>DMX</h2>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Enable DMX output</div>
                        <div class="setting-status" id="dmx-enabled-status" x-text="$store.dmx.enabledStatusText">Loading…</div>
                    </div>
                    <div class="setting-controls">
                        <label class="toggle">
                            <input type="checkbox" id="dmx-enabled-toggle"
                                   x-model="$store.dmx.enabled"
                                   x-on:change="$store.dmx.setEnabled($el.checked)">
                            <span>Enabled</span>
                        </label>
                    </div>
                    <div class="setting-hint">
                        Turn off if you're running a Govee-only rig. Skips Art-Net /
                        sACN socket bind and the startup ArtPoll probe.
                    </div>
                </div>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Protocol</div>
                        <div class="setting-status">How Conductør talks to your DMX hardware.</div>
                    </div>
                    <div class="setting-controls">
                        <select id="dmx-protocol-select" class="setting-select" x-model="$store.dmx.protocol">
                            <option value="artnet">Art-Net (recommended)</option>
                            <option value="sacn">sACN</option>
                            <option value="both">Both</option>
                        </select>
                        <button class="action-btn" id="btn-save-dmx-protocol"
                                x-on:click="$store.dmx.saveProtocol()">Save</button>
                    </div>
                    <div class="setting-hint">
                        Art-Net covers every consumer DMX-over-IP box. sACN is for venues with managed networks.
                    </div>
                </div>

                <div class="dmx-nodes">
                    <div class="dmx-nodes-header">
                        <h3>Art-Net nodes</h3>
                        <button class="action-btn" id="btn-dmx-node-add"
                                x-on:click="$store.dmx.openAdd()">+ Add node</button>
                    </div>
                    <p class="dmx-nodes-hint">
                        Optional — skip this if you've got one Art-Net box on the LAN;
                        we'll just broadcast to everything. Populate this if you're a
                        venue running separate universes on distinct IPs.
                    </p>
                    <div class="dmx-node-list" id="dmx-node-list">
                        <template x-if="$store.dmx.showEmpty">
                            <div class="dmx-nodes-empty">No nodes configured — broadcasting to LAN.</div>
                        </template>
                        <template x-for="(node, idx) in $store.dmx.nodes" :key="idx">
                            <div class="dmx-node-row">
                                <div class="dmx-node-ip"><strong x-text="node.ip"></strong></div>
                                <div class="dmx-node-universe" x-text="'Universe ' + node.universe"></div>
                                <div class="dmx-node-actions">
                                    <button class="action-btn"
                                            x-on:click="$store.dmx.openEdit(idx)">Edit</button>
                                    <button class="action-btn danger"
                                            x-on:click="$store.dmx.deleteNode(idx)">Delete</button>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </section>

            <div class="modal-overlay" id="dmx-node-modal" hidden
                 :hidden="!$store.dmx.modalShown"
                 x-on:click.self="$store.dmx.closeModal()">
                <div class="modal-dialog" role="dialog" aria-labelledby="dmx-node-title" aria-modal="true">
                    <button class="modal-close" id="btn-dmx-node-close" aria-label="Close"
                            x-on:click="$store.dmx.closeModal()">&times;</button>
                    <h3 id="dmx-node-title" x-text="$store.dmx.modalTitle">Add Art-Net node</h3>
                    <div class="form-row">
                        <label for="dmx-node-ip">IP address</label>
                        <input type="text" id="dmx-node-ip" placeholder="192.168.1.100" spellcheck="false"
                               x-model="$store.dmx.form.ip">
                    </div>
                    <div class="form-row">
                        <label for="dmx-node-universe">Universe</label>
                        <input type="number" id="dmx-node-universe" min="0" max="32767"
                               x-model.number="$store.dmx.form.universe">
                    </div>
                    <div class="modal-actions">
                        <button class="action-btn modal-primary" id="btn-dmx-node-save"
                                x-on:click="$store.dmx.saveNode()">Save</button>
                        <button class="action-btn" id="btn-dmx-node-cancel"
                                x-on:click="$store.dmx.closeModal()">Cancel</button>
                    </div>
                    <div class="modal-warning" id="dmx-node-error" hidden
                         :hidden="!$store.dmx.formError"
                         x-text="$store.dmx.formError"></div>
                </div>
            </div>
        </div><!-- /tab-panel: dmx -->

        <div class="tab-panel" data-tab-panel="govee" role="tabpanel" aria-labelledby="tab-btn-govee" :hidden="$store.tabs.active !== 'govee'"
             x-on:click="$store.govee.openHelpId = ''">
            <section class="govee-panel">
                <h2>Govee</h2>

                <!-- LAN Control primer. Most onboarding failures are users
                     who haven't toggled this on in the Govee Home app yet,
                     so it gets top billing. -->
                <div class="govee-lan-callout" id="govee-lan-callout">
                    <div class="govee-lan-head">
                        <div class="govee-lan-title">Enable LAN Control first</div>
                        <button type="button" class="govee-lan-toggle" id="btn-govee-lan-toggle" aria-expanded="false" aria-controls="govee-lan-body"
                                :aria-expanded="$store.govee.lanExpanded ? 'true' : 'false'"
                                x-text="$store.govee.lanToggleLabel"
                                x-on:click="$store.govee.toggleLan()">Show me how</button>
                    </div>
                    <p class="govee-lan-summary">
                        Conductør controls your lights directly over your Wi-Fi — no cloud
                        roundtrip. You'll need to flip <strong>LAN Control</strong> on in the
                        Govee Home app for each light.
                    </p>
                    <p class="govee-lan-help">
                        Can't find it? <a href="https://conductor.lighting/faq.html#govee-setup" target="_blank" rel="noopener noreferrer">See the full walkthrough →</a>
                    </p>
                    <div class="govee-lan-body" id="govee-lan-body" hidden
                         :hidden="!$store.govee.lanExpanded">
                        <ol class="govee-lan-steps">
                            <li>Open <strong>Govee Home</strong> on your phone.</li>
                            <li>Tap a light → gear icon → <strong>LAN Control</strong>.</li>
                            <li>Flip it on. Repeat for every light you want Conductør to drive.</li>
                        </ol>
                        <p class="govee-lan-faq-link">
                            Walkthrough with screenshots: <a href="https://conductor.lighting/faq.html#govee-setup" target="_blank" rel="noopener noreferrer">conductor.lighting/faq#govee-setup</a>
                        </p>
                    </div>
                </div>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Enable Govee lights</div>
                        <div class="setting-status" id="govee-enabled-status" x-text="$store.govee.enabledStatusText">Loading…</div>
                    </div>
                    <div class="setting-controls">
                        <label class="toggle">
                            <input type="checkbox" id="govee-enabled-toggle"
                                   x-model="$store.govee.enabled"
                                   x-on:change="$store.govee.setEnabled($el.checked)">
                            <span>Enabled</span>
                        </label>
                    </div>
                    <div class="setting-hint">
                        Govee Wi-Fi smart bulbs and strips. Leave off if you only use DMX.
                    </div>
                </div>

                <div class="govee-devices">
                    <div class="govee-devices-header">
                        <h3>Devices</h3>
                        <div class="govee-devices-actions" id="govee-toolbar-actions" hidden
                             :hidden="!$store.govee.showToolbar">
                            <button class="action-btn" id="btn-govee-find"
                                    :disabled="$store.govee.findBusy"
                                    x-on:click="$store.govee.findMyLights('toolbar')">
                                <span class="inline-spinner" x-show="$store.govee.findBusy" aria-hidden="true"></span>
                                <span x-text="$store.govee.findBusy ? 'Scanning…' : 'Find my lights'">Find my lights</span>
                            </button>
                            <button class="action-btn" id="btn-govee-device-add"
                                    x-on:click="$store.govee.openAdd()">+ Add manually</button>
                            <button class="action-btn" id="btn-govee-recheck" title="Re-scan the LAN now"
                                    :disabled="$store.govee.recheckBusy"
                                    x-on:click="$store.govee.manualRecheck()">
                                <span class="inline-spinner" x-show="$store.govee.recheckBusy" aria-hidden="true"></span>
                                <span x-text="$store.govee.recheckBusy ? 'Rechecking…' : 'Recheck'">Recheck</span>
                            </button>
                        </div>
                    </div>

                    <!-- Empty-state hero. Shows when no devices are configured.
                         Hidden the moment the list has anything in it, at which
                         point the compact toolbar above takes over. -->
                    <div class="govee-empty-hero" id="govee-empty-hero" hidden
                         :hidden="!$store.govee.showEmptyHero">
                        <div class="govee-empty-title">No lights yet.</div>
                        <p class="govee-empty-lede">
                            Make sure LAN Control is on (above), then hit the button.
                            Conductør will auto-add every light it finds and turn Govee on.
                        </p>
                        <button class="action-btn modal-primary govee-empty-cta" id="btn-govee-find-hero"
                                :disabled="$store.govee.findBusy"
                                x-on:click="$store.govee.findMyLights('hero')">
                            <span class="inline-spinner" x-show="$store.govee.findBusy" aria-hidden="true"></span>
                            <span x-text="$store.govee.findBusy ? 'Scanning…' : 'Find my lights'">Find my lights</span>
                        </button>
                        <button class="govee-empty-secondary" id="btn-govee-device-add-hero"
                                x-on:click="$store.govee.openAdd()">or add one manually</button>
                        <div class="govee-empty-status" id="govee-empty-status" hidden
                             :hidden="!$store.govee.emptyHeroStatusText"
                             :class="'govee-empty-status ' + ($store.govee.emptyHeroStatusKind || '')"
                             x-text="$store.govee.emptyHeroStatusText"></div>
                    </div>

                    <div class="govee-scan-result" id="govee-scan-result" hidden></div>
                    <div class="govee-device-list" id="govee-device-list" hidden
                         :hidden="!$store.govee.showDeviceList">
                        <template x-for="(d, idx) in $store.govee.devices" :key="d.ble_id || d.sku || idx">
                            <div class="govee-device-row"
                                 :data-ble-id="d.ble_id || ''"
                                 :data-sku="d.sku || ''">
                                <span class="govee-status-dot"
                                      :class="{ 'pulsing': $store.govee.statusInflight }"
                                      :data-status="$store.govee.statusFor(d).status"
                                      :title="$store.govee.statusFor(d).title"
                                      :aria-label="'Status: ' + $store.govee.statusFor(d).label"></span>
                                <div class="govee-device-name"><strong x-text="d.name"></strong></div>
                                <div class="govee-device-meta">
                                    <template x-if="d.product_name">
                                        <span><span x-text="d.product_name"></span> · </span>
                                    </template>
                                    <span x-text="d.sku"></span>
                                    <template x-if="d.ble_id">
                                        <span> · <span x-text="d.ble_id"></span></span>
                                    </template>
                                </div>
                                <div class="govee-device-actions">
                                    <button class="action-btn"
                                            x-on:click="$store.govee.openEdit(idx)">Edit</button>
                                    <button class="action-btn danger"
                                            x-on:click="$store.govee.deleteDevice(idx)">Delete</button>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </section>

            <div class="modal-overlay" id="govee-device-modal" hidden
                 :hidden="!$store.govee.modalShown">
                <div class="modal-dialog" role="dialog" aria-labelledby="govee-device-title" aria-modal="true">
                    <button class="modal-close" id="btn-govee-device-close" aria-label="Close"
                            x-on:click="$store.govee.closeModal()">&times;</button>
                    <h3 id="govee-device-title" x-text="$store.govee.modalTitle">Add Govee device</h3>
                    <div class="form-row">
                        <label for="govee-device-sku">
                            SKU
                            <button type="button" class="help-bubble" data-help="help-govee-sku" aria-label="What's a SKU?"
                                    :aria-expanded="$store.govee.openHelpId === 'help-govee-sku' ? 'true' : 'false'"
                                    x-on:click.stop="$store.govee.toggleHelp('help-govee-sku')">?</button>
                        </label>
                        <input type="text" id="govee-device-sku" placeholder="H6160" spellcheck="false"
                               x-model="$store.govee.form.sku">
                        <div class="form-hint">The model code printed on the device or shown in the Govee Home app.</div>
                        <div class="help-popover" id="help-govee-sku" hidden
                             :hidden="$store.govee.openHelpId !== 'help-govee-sku'"
                             x-on:click.stop="">
                            <p>Govee model code, starts with H. Check the Govee Home app → device settings → <em>Device Info</em>, or the sticker on the underside of the light. <a href="https://conductor.lighting/faq.html#govee-setup" target="_blank" rel="noopener noreferrer">Walkthrough →</a></p>
                        </div>
                    </div>
                    <div class="form-row">
                        <label for="govee-device-name">Name</label>
                        <input type="text" id="govee-device-name" placeholder="Couch strip" spellcheck="false"
                               x-model="$store.govee.form.name">
                        <div class="form-hint">Anything you like — just how it shows up in the list.</div>
                    </div>
                    <div class="form-row">
                        <label for="govee-device-ble">
                            BT MAC (optional)
                            <button type="button" class="help-bubble" data-help="help-govee-ble" aria-label="Where do I find the BT MAC?"
                                    :aria-expanded="$store.govee.openHelpId === 'help-govee-ble' ? 'true' : 'false'"
                                    x-on:click.stop="$store.govee.toggleHelp('help-govee-ble')">?</button>
                        </label>
                        <input type="text" id="govee-device-ble" placeholder="A4:C1:38:XX:XX:XX" spellcheck="false" maxlength="17"
                               :value="$store.govee.form.ble_id"
                               x-on:input="$store.govee.formatBleField($el)">
                        <div class="form-hint">Auto-filled when you use <em>Find my lights</em>. Only needed manually if you own two identical SKUs or the light naps its Wi-Fi and needs a BLE wake.</div>
                        <div class="help-popover" id="help-govee-ble" hidden
                             :hidden="$store.govee.openHelpId !== 'help-govee-ble'"
                             x-on:click.stop="">
                            <p>Govee Home app → tap the light → gear icon → <strong>Device Info</strong>. Scroll to the bottom — the BT MAC sits next to the Product Model and looks like <code>A4:C1:38:XX:XX:XX</code>. Paste it in — colons get added automatically as you type. <a href="https://conductor.lighting/faq.html#govee-setup" target="_blank" rel="noopener noreferrer">Walkthrough →</a></p>
                        </div>
                    </div>
                    <div class="modal-actions">
                        <button class="action-btn modal-primary" id="btn-govee-device-save"
                            x-on:click="$store.govee.saveDevice()">Save</button>
                        <button class="action-btn" id="btn-govee-device-cancel"
                            x-on:click="$store.govee.closeModal()">Cancel</button>
                    </div>
                    <div class="modal-warning" id="govee-device-error" hidden
                         :hidden="!$store.govee.formError"
                         x-text="$store.govee.formError"></div>
                </div>
            </div>
        </div><!-- /tab-panel: govee -->

        <div class="tab-panel" data-tab-panel="director" role="tabpanel" aria-labelledby="tab-btn-director" :hidden="$store.tabs.active !== 'director'">
            <section class="director-config">
                <h2>AI Director</h2>
                <p class="director-subtitle">Meet <strong>Lumi</strong> — she watches the room and nudges the show every ~15 seconds.</p>

                <!-- Status banner — single live read of what the engine is actually doing. -->
                <div class="director-status-banner" id="director-status-banner" data-state="loading"
                     :data-state="$store.director.bannerState">
                    <span class="director-status-dot"></span>
                    <span class="director-status-text" x-text="$store.director.bannerText">Loading…</span>
                </div>

                <!-- Top: enable toggle. Auto-saves on change. -->
                <div class="setting-row director-enable-row">
                    <div class="setting-label">
                        <div class="setting-name">Enable AI Director</div>
                    </div>
                    <div class="setting-controls">
                        <label class="toggle">
                            <input type="checkbox" id="director-enable-toggle"
                                   x-model="$store.director.enabled"
                                   x-on:change="$store.director.toggleEnable($el.checked)">
                            <span>On</span>
                        </label>
                        <!-- KL-101: paused pill — visible whenever
                             paused_reason is set. Distinct from the
                             toggle so user intent stays preserved. -->
                        <span class="director-paused-pill" id="director-paused-pill" hidden
                              :hidden="!$store.director.showPausedPill">
                            <span class="director-paused-label">Paused — <span id="director-paused-reason-text" x-text="$store.director.pausedReasonText"></span></span>
                            <button type="button" class="action-btn" id="director-paused-resume"
                                    x-on:click="$store.director.resumeFromPause()">Resume</button>
                        </span>
                    </div>
                    <div class="setting-hint">
                        Lumi gives the show subtle nudges over time —
                        release speed, color hue, transient emphasis — based
                        on what's playing. Updates every ~15 seconds.
                    </div>
                </div>

                <!-- Mode cards. Hidden when Director is disabled.
                     KL-180: relay card is gated by `relay_ui_enabled`. The
                     DOM stays in this template (no code deleted) so flipping
                     the flag back on later is a one-line change. When the
                     flag is OFF (default), only the BYOK card renders and the
                     "How should it run?" chooser collapses to a single
                     "Set up the AI Director" panel. -->
                <div class="director-cards" id="director-cards" hidden
                     :hidden="!$store.director.showCards">
                    {% if relay_ui_enabled %}
                    <h3 class="director-cards-heading">How should it run?</h3>

                    <label class="director-card" for="director-mode-relay">
                        <input type="radio" name="director-mode" id="director-mode-relay" value="relay"
                               :checked="$store.director.isRelayMode"
                               x-on:change="$el.checked && $store.director.selectMode('relay')">
                        <div class="director-card-body">
                            <div class="director-card-head">
                                <span class="director-card-title">Use Conductør AI</span>
                                <span class="director-card-badge">Recommended</span>
                            </div>
                            <p class="director-card-desc">
                                Included with your license. No setup, no extra
                                keys, no calls billed to you.
                            </p>
                        </div>
                    </label>
                    {% else %}
                    <h3 class="director-cards-heading">Set up the AI Director</h3>
                    {% endif %}

                    <label class="director-card" for="director-mode-byok">
                        <input type="radio" name="director-mode" id="director-mode-byok" value="byok"
                               :checked="$store.director.isByokMode"
                               x-on:change="$el.checked && $store.director.selectMode('byok')">
                        <div class="director-card-body">
                            <div class="director-card-head">
                                <span class="director-card-title">{% if relay_ui_enabled %}Use my own Anthropic key (BYOK){% else %}Connect your Anthropic API key{% endif %}</span>
                            </div>
                            <p class="director-card-desc">
                                {% if relay_ui_enabled %}Calls billed by Anthropic. Useful for custom
                                models or to bypass the license-included quota.{% else %}Your license unlocks the AI Director; you bring your
                                own Anthropic API key so calls are billed directly to
                                you. Most users find a few dollars of credit lasts a
                                long time.{% endif %}
                            </p>
                            {% if not relay_ui_enabled %}
                            <!-- KL-182: empty-state explainer. Rendered only when
                                 no key is set yet (active_path === 'needs_byok_key').
                                 Mirrors the post-activation modal so users who
                                 dismiss the modal still get the 3-step walkthrough
                                 here. Disappears once a key is saved. -->
                            <div class="director-byok-empty-state"
                                 :hidden="$store.director.activePath !== 'needs_byok_key'"
                                 hidden>
                                <div class="director-byok-empty-title">Add your Anthropic API key to start</div>
                                <ol class="byok-setup-steps">
                                    <li>Sign up at <a href="https://console.anthropic.com" target="_blank" rel="noopener">console.anthropic.com</a> and add a payment method (or pre-pay credit).</li>
                                    <li>Open <strong>Settings → API Keys</strong> and click <strong>Create Key</strong>. Copy the value &mdash; you only see it once.</li>
                                    <li>Paste it below and we'll validate it for you.</li>
                                </ol>
                            </div>
                            {% endif %}
                            <div class="director-byok-fields" id="director-byok-fields" hidden
                                 :hidden="!$store.director.showByokFields">
                                <div class="director-byok-current" id="director-byok-current" hidden
                                     :hidden="!$store.director.showByokCurrent">
                                    Currently set: <code id="director-byok-mask" x-text="$store.director.apiKeyMasked"></code>
                                    <button type="button" class="action-btn" id="director-byok-clear"
                                            x-on:click="$store.director.clearByokKey()">Clear</button>
                                </div>
                                <div class="director-byok-input-row">
                                    <input type="password" id="director-byok-input"
                                           class="setting-select" autocomplete="off"
                                           spellcheck="false" placeholder="sk-ant-…"
                                           x-model="$store.director.byokInputValue"
                                           :type="$store.director.byokInputType"
                                           :placeholder="$store.director.byokPlaceholder"
                                           x-on:input="$store.director.onByokInput()">
                                    <button type="button" class="icon-btn" id="director-byok-toggle-visibility"
                                            title="Show/hide" aria-label="Toggle visibility"
                                            :class="{ 'active': $store.director.byokInputType === 'text' }"
                                            x-on:click="$store.director.toggleByokVisibility()">
                                        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
                                    </button>
                                </div>
                                <div class="director-byok-status" id="director-byok-status"
                                     :class="$store.director.byokStatusKind"
                                     x-text="$store.director.byokStatusText"></div>
                                <div class="director-byok-hint">
                                    Get a key at <a href="https://console.anthropic.com" target="_blank" rel="noopener noreferrer">console.anthropic.com</a>.
                                    Stored locally in <code>~/.conductor/secrets.json</code>; never sent anywhere except Anthropic.
                                    Need step-by-step? <a href="https://conductor.lighting/byok.html" target="_blank" rel="noopener noreferrer">Read the full setup walkthrough</a> &mdash; cost numbers, spend caps, FAQ.
                                </div>

                                <details class="director-byok-advanced">
                                    <summary>Advanced: model override</summary>
                                    <div class="director-byok-advanced-body">
                                        <div class="director-byok-input-row">
                                            <input type="text" id="director-byok-model"
                                                   class="setting-select" autocomplete="off"
                                                   spellcheck="false"
                                                   placeholder="claude-haiku-4-5-20251001 (default)"
                                                   x-model="$store.director.modelInputValue"
                                                   x-on:input="$store.director.onModelInput()">
                                            <span class="director-byok-model-status" id="director-byok-model-status"
                                                  :class="$store.director.modelStatusKind"
                                                  x-text="$store.director.modelStatusText"></span>
                                        </div>
                                        <div class="director-byok-hint">
                                            Pass any Anthropic model ID — Haiku for fast/cheap, Sonnet for
                                            more nuance, Opus if you want to go big. Leave blank for the
                                            default.{% if relay_ui_enabled %} <strong>Only honored on BYOK</strong>; the relay path
                                            ignores this and uses the hardcoded default.{% endif %}
                                        </div>
                                    </div>
                                </details>
                            </div>
                        </div>
                    </label>
                </div>

                <!-- KL-101: idle auto-pause settings + marathon mode.
                     Hidden when the director is off. -->
                <div class="director-extras" id="director-extras" hidden
                     :hidden="!$store.director.showExtras">
                    <div class="setting-row director-idle-row">
                        <div class="setting-label">
                            <div class="setting-name">Auto-pause when idle</div>
                        </div>
                        <div class="setting-controls">
                            <select id="director-idle-threshold-select" class="setting-select"
                                    x-model.number="$store.director.idleThresholdSeconds"
                                    :disabled="$store.director.idleThresholdEnvOverride !== null"
                                    x-on:change="$store.director.saveIdleThreshold($el.value)">
                                <option value="0">Off</option>
                                <option value="1800">30 min</option>
                                <option value="5400">90 min (default)</option>
                                <option value="7200">2 hours</option>
                                <option value="14400">4 hours</option>
                            </select>
                        </div>
                        <div class="setting-hint">
                            Pauses Lumi if the dashboard sees no activity for this long. Click anything to wake her up. Lights keep working — only AI calls pause.
                        </div>
                    </div>

                    <div class="setting-row director-marathon-row">
                        <div class="setting-label">
                            <div class="setting-name">Marathon mode</div>
                        </div>
                        <div class="setting-controls">
                            <button type="button" class="action-btn" id="director-marathon-btn"
                                    :hidden="!$store.director.showMarathonBtn"
                                    x-on:click="$store.director.toggleMarathon(true)">
                                I'm doing a marathon set (12hr)
                            </button>
                            <span class="director-marathon-active" id="director-marathon-active" hidden
                                  :hidden="!$store.director.showMarathonActive">
                                <span id="director-marathon-remaining" x-text="$store.director.marathonRemainingText">12h 0m</span> remaining
                                <button type="button" class="action-btn" id="director-marathon-cancel"
                                        x-on:click="$store.director.toggleMarathon(false)">Cancel</button>
                            </span>
                        </div>
                        <div class="setting-hint">
                            Suppress idle auto-pause for 12 hours. For long sets where you can't keep clicking the dashboard.
                        </div>
                    </div>
                </div>
            </section>
        </div><!-- /tab-panel: director -->

        <div class="tab-panel" data-tab-panel="midi" role="tabpanel" aria-labelledby="tab-btn-midi" :hidden="$store.tabs.active !== 'midi'">
            <section class="midi-config">
                <h2>MIDI</h2>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Enable</div>
                        <div class="setting-status" id="midi-cfg-status" x-text="$store.midi.cfgStatusText">Loading…</div>
                    </div>
                    <div class="setting-controls">
                        <label class="toggle">
                            <input type="checkbox" id="midi-cfg-enabled" x-model="$store.midi.enabled">
                            <span>MIDI control + feedback</span>
                        </label>
                    </div>
                    <div class="setting-hint">
                        Disable if you're not using a MIDI controller. This stops the app from probing ports at startup.
                    </div>
                </div>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Input port</div>
                        <div class="setting-status">Controller you use to drive the show.</div>
                    </div>
                    <div class="setting-controls">
                        <select id="midi-input-select" class="setting-select"
                                x-model="$store.midi.inputPort">
                            <template x-for="opt in $store.midi.inputPortOptions" :key="opt.value">
                                <option :value="opt.value" x-text="opt.label"></option>
                            </template>
                        </select>
                        <button class="action-btn" id="btn-midi-refresh"
                                x-on:click="$store.midi.refreshConfig()">Refresh ports</button>
                    </div>
                </div>

                <div class="setting-row">
                    <div class="setting-label">
                        <div class="setting-name">Output port</div>
                        <div class="setting-status">Controller that gets LED / motor feedback.</div>
                    </div>
                    <div class="setting-controls">
                        <select id="midi-output-select" class="setting-select"
                                x-model="$store.midi.outputPort">
                            <template x-for="opt in $store.midi.outputPortOptions" :key="opt.value">
                                <option :value="opt.value" x-text="opt.label"></option>
                            </template>
                        </select>
                    </div>
                </div>

                <div class="setting-controls" style="justify-content: flex-end; margin-top: 0.5rem;">
                    <button class="action-btn modal-primary" id="btn-save-midi-cfg"
                            x-on:click="$store.midi.saveConfig()">Save MIDI Config</button>
                </div>
                <div class="setting-result" id="midi-cfg-result" hidden
                     :hidden="!$store.midi.showSaveResult"
                     :class="$store.midi.saveResultKind"
                     x-text="$store.midi.saveResultText"></div>

                <!-- ─── KL-71: live mapping table ─────────────────────
                     Action ↔ MIDI control bindings. Learn captures the
                     next note/CC the controller sends and persists it
                     to ~/.conductor/midi_map.json. Recent-message
                     ticker is the "we're hearing you" diagnostic so
                     users can confirm the controller is talking
                     before binding anything. -->
                <div class="midi-map">
                    <div class="midi-map-head">
                        <h3>Control mapping</h3>
                        <div class="midi-conn" id="midi-conn">
                            <span class="midi-conn-dot" id="midi-conn-dot" data-state="off" :data-state="$store.midi.connState"></span>
                            <span class="midi-conn-text" id="midi-conn-text" x-text="$store.midi.connText">Not connected</span>
                        </div>
                    </div>
                    <p class="midi-map-help">
                        Click <strong>Learn</strong> next to an action, then press the pad or move the fader on your MIDI controller you want to bind. Bindings save instantly and survive restarts.
                    </p>

                    <div class="midi-recent" id="midi-recent" aria-label="Recent MIDI messages">
                        <span class="midi-recent-label">Recent</span>
                        <div class="midi-recent-strip" id="midi-recent-strip" x-html="$store.midi.recentHtml">
                            <span class="midi-recent-empty">Press a button or move a fader on your controller to test…</span>
                        </div>
                    </div>

                    <div class="midi-map-table" id="midi-map-table" role="table">
                        <template x-for="row in $store.midi.tableRows" :key="row.actionId">
                            <div class="midi-map-row" :data-action="row.actionId">
                                <div class="midi-map-action">
                                    <span class="midi-map-action-name" x-text="row.label"></span>
                                    <span class="midi-map-action-tip" x-text="row.tooltip"></span>
                                </div>
                                <div class="midi-map-binding"
                                     :data-state="row.bindingState"
                                     x-text="row.bindingText"></div>
                                <div class="midi-map-actions">
                                    <button class="midi-map-btn"
                                            :data-armed="row.isArmed"
                                            x-text="row.learnLabel"
                                            x-on:click="$store.midi.learn(row.actionId)"></button>
                                    <button class="midi-map-btn midi-map-btn-clear"
                                            x-show="row.hasBinding"
                                            x-on:click="$store.midi.clearBinding(row.actionId)">×</button>
                                </div>
                            </div>
                        </template>
                    </div>

                    <div class="midi-map-footer">
                        <button class="action-btn" id="btn-midi-restore"
                                x-on:click="$store.midi.restoreDefaults()">Restore defaults</button>
                    </div>
                </div>
            </section>
        </div><!-- /tab-panel: midi -->

        <div class="tab-panel" data-tab-panel="settings" role="tabpanel" aria-labelledby="tab-btn-settings" :hidden="$store.tabs.active !== 'settings'">
        <!-- Settings -->
        <section class="settings">
            <h2>Settings</h2>
            <div class="setting-row license-row" id="license-row" data-state="loading"
                 :data-state="$store.license.displayState">
                <div class="setting-label">
                    <div class="setting-name">License</div>
                    <div class="setting-status" id="license-status" x-text="$store.license.statusText">Loading…</div>
                </div>

                <div class="license-unlicensed" hidden
                     :hidden="!('unlicensed' === $store.license.state || $store.license.showError)">
                    <div class="setting-controls">
                        <input type="text" id="license-key-input" class="license-key-input"
                               placeholder="Paste your license key" autocomplete="off"
                               spellcheck="false"
                               x-model="$store.license.inputValue"
                               :disabled="$store.license.activateBusy"
                               x-on:keydown.enter.prevent="$store.license.activate()">
                        <button class="action-btn" id="btn-license-activate"
                                :disabled="$store.license.activateBusy"
                                x-text="$store.license.activateLabel"
                                x-on:click="$store.license.activate()">Activate</button>
                    </div>
                    <div class="setting-hint">
                        Got an email after checkout? Paste the key from there.
                        <a href="https://conductor.lighting/pricing.html" data-upgrade-surface="license_row_see_plans" target="_blank" rel="noopener">
                            Don't have one yet? See plans →
                        </a>
                    </div>
                    <div class="license-error" id="license-error" hidden
                         :hidden="!$store.license.showError"
                         x-text="$store.license.errorMessage"></div>
                </div>

                <div class="license-active" hidden
                     :hidden="$store.license.state !== 'active'">
                    <div class="license-active-header">
                        <span class="license-check" aria-hidden="true">✓</span>
                        <span class="license-tier-pill" id="license-tier-pill" x-text="$store.license.tierLabel">Pro</span>
                        <span class="license-active-pill">Active</span>
                    </div>
                    <div class="license-meta">
                        <div class="license-meta-row">
                            <span class="license-meta-label">Key</span>
                            <span class="license-meta-value" id="license-key-masked" x-text="$store.license.keyMasked">••••••••</span>
                        </div>
                        <div class="license-meta-row" id="license-expires-row" hidden
                            :hidden="!$store.license.showExpires">
                            <span class="license-meta-label">Expires</span>
                            <span class="license-meta-value" id="license-expires-value" x-text="$store.license.expiresFormatted">—</span>
                        </div>
                        <div class="license-meta-row">
                            <span class="license-meta-label">Activated on</span>
                            <span class="license-meta-value" id="license-fingerprint" x-text="$store.license.fingerprintText">this machine</span>
                        </div>
                    </div>
                    <button class="license-deactivate" id="btn-license-deactivate" type="button"
                            :disabled="$store.license.deactivateBusy"
                            x-text="$store.license.deactivateLabel"
                            x-on:click="$store.license.deactivate()">
                        Deactivate this device
                    </button>
                </div>

                <div class="license-override" hidden
                     :hidden="$store.license.state !== 'override'">
                    <div class="license-override-header">
                        <span class="license-override-glyph" aria-hidden="true">⚠</span>
                        <span class="license-override-title">Tier override active</span>
                    </div>
                    <div class="license-override-body" id="license-override-body">
                        Running as <strong id="license-override-tier" x-text="$store.license.overrideTierLabel">Pro</strong> via dev override.
                        No license on file.
                    </div>
                    <div class="license-override-hint" id="license-override-hint"
                         x-text="$store.license.overrideHint">
                        Unset the override to see the real activation flow.
                    </div>
                </div>

                <div class="license-restart-hint" id="license-restart-hint" hidden
                     :hidden="!$store.license.showRestartHint">
                    Some hardware features (DMX, sACN) may require a restart to fully activate.
                </div>
            </div>
            <div class="setting-row">
                <div class="setting-label">
                    <div class="setting-name">Audio Input</div>
                    <div class="setting-status" id="audio-device-status"
                         :class="$store.audioInput.statusLevel"
                         x-text="$store.audioInput.statusText">Loading…</div>
                </div>
                <div class="setting-controls">
                    <select id="audio-backend-select" class="setting-select"
                            x-on:change="$store.audioInput.scheduleSave()">
                        <option value="auto">Auto-detect loopback</option>
                    </select>
                </div>
                <div class="signal-meter" title="Current input level (max of bass/mids/highs)">
                    <div class="signal-meter-fill" id="signal-meter-fill"></div>
                    <div class="signal-meter-label" id="signal-meter-label">Signal: —</div>
                </div>
                <div class="setting-hint">
                    Change persists to <code>~/.conductor/config.yaml</code> and hot-swaps the running engine.
                    The bar shows live input level so you can verify the selected device is actually receiving audio.
                </div>
                <div class="setting-hint os-helper" id="audio-helper-os" hidden
                     :hidden="!$store.audioInput.helperShown"
                     x-html="$store.audioInput.helperContent"></div>
                <a href="#" class="setting-help-link" id="link-mac-audio-help" hidden>
                    Not hearing system audio? Open the macOS setup guide →
                </a>
            </div>
            <div class="setting-row">
                <div class="setting-label">
                    <div class="setting-name">Autoplay</div>
                    <div class="setting-status" id="autoplay-status"
                         :class="$store.autoplay.statusClass"
                         x-text="$store.autoplay.statusText">Loading…</div>
                </div>
                <div class="setting-controls">
                    <label class="toggle">
                        <input type="checkbox" id="autoplay-toggle"
                                   x-model="$store.autoplay.enabled"
                                   x-on:change="$store.autoplay.save($el.checked)">
                        <span>Start the engine automatically on launch</span>
                    </label>
                </div>
                <div class="setting-hint">
                    When off, Conductør opens with the engine stopped and waits for you to click Play in the floating controls. Change takes effect next launch.
                </div>
            </div>
            <div class="setting-row">
                <div class="setting-label">
                    <div class="setting-name">Anonymous Usage</div>
                    <div class="setting-status" id="telemetry-status"
                         :class="$store.telemetry.statusClass"
                         x-text="$store.telemetry.statusText">Loading…</div>
                </div>
                <div class="setting-controls">
                    <label class="toggle">
                        <input type="checkbox" id="telemetry-toggle"
                                   x-model="$store.telemetry.enabled"
                                   :disabled="$store.telemetry.locked"
                                   x-on:change="$store.telemetry.toggleFromSettings($el.checked)">
                        <span>Send anonymous usage + crash reports</span>
                    </label>
                </div>
                <div class="setting-hint">
                    We send which features you touch, whether upgrade prompts convert, and unhandled errors &mdash; never audio, never config, never IP.
                    Used to prioritize what to build next and catch bugs you didn't bother reporting.
                </div>
            </div>
            <!-- KL-142: in-app fixture catalog refresh. Pulls a fresh
                 snapshot from the Conductør CDN, atomically swaps it
                 into ~/.conductor/catalog_overlay/, and the next Add
                 Fixture modal sees the new entries — no app restart. -->
            <div class="setting-row">
                <div class="setting-label">
                    <div class="setting-name">Fixture catalog</div>
                    <div class="setting-status"
                         :class="{'is-busy': $store.catalogRefresh.isBusy, 'is-error': !!$store.catalogRefresh.errorText}"
                         x-text="$store.catalogRefresh.statusText">Pull the latest fixture profiles from the Conductør CDN.</div>
                </div>
                <div class="setting-controls">
                    <button class="action-btn" id="btn-catalog-refresh"
                            :disabled="$store.catalogRefresh.isBusy"
                            x-on:click="$store.catalogRefresh.refresh()"
                            x-text="$store.catalogRefresh.isBusy ? 'Updating…' : 'Update fixture catalog'">Update fixture catalog</button>
                </div>
                <div class="setting-hint">
                    Conductør ships with the catalog as it was at release time. Tap this to pull anything that's been added or fixed upstream since &mdash; new manufacturers, channel-map corrections, freshly-tested fixtures. Safe to run any time; falls back to the bundled catalog if the network is offline.
                </div>
            </div>
        </section>
        <footer class="settings-footer">
            {% if tier_name == "starter" %}
            <a class="settings-footer-upgrade" href="https://conductor.lighting/pricing.html" data-upgrade-surface="settings_footer" target="_blank" rel="noopener">
                Upgrade to Pro →
            </a>
            {% endif %}
            <span class="brand-word">{{ brand }}</span>
            <span class="settings-footer-tagline">{{ tagline }}</span>
            <img class="settings-footer-kl" src="/static/images/kl-logo/komar-labs-wordmark.svg" alt="" aria-hidden="true">
            <span class="settings-footer-copy">© 2026 Komar Labs, LLC</span>
        </footer>
        </div><!-- /tab-panel: settings -->

        <div class="tab-panel" data-tab-panel="updates" role="tabpanel" aria-labelledby="tab-btn-updates" :hidden="$store.tabs.active !== 'updates'">
        <section class="updates-panel">
            <h2>App Updates</h2>

            <div class="updates-card" id="updates-card" data-state="loading">
                <div class="updates-version-line">
                    <span class="updates-version-label">You're on</span>
                    <span class="updates-version-current" id="updates-current-version">—</span>
                </div>

                <div class="updates-state updates-state-loading" data-state-for="loading">
                    <div class="updates-state-title">Checking for updates…</div>
                </div>

                <div class="updates-state updates-state-uptodate" data-state-for="uptodate" hidden>
                    <div class="updates-state-title">
                        <span class="updates-glyph" aria-hidden="true">✓</span>
                        You're up to date.
                    </div>
                    <div class="updates-state-detail">
                        Latest: <strong id="updates-latest-uptodate">—</strong>
                    </div>
                </div>

                <div class="updates-state updates-state-available" data-state-for="available" hidden>
                    <div class="updates-state-title">
                        <span class="updates-glyph" aria-hidden="true">↓</span>
                        Update available
                    </div>
                    <div class="updates-state-detail">
                        <strong id="updates-latest-available">—</strong>
                        <span class="updates-released" id="updates-released"></span>
                    </div>
                    <div class="updates-actions">
                        <a class="updates-download-btn" id="updates-download-btn"
                           href="#" target="_blank" rel="noopener">
                            Download for <span id="updates-platform-label">your platform</span>
                            <span class="updates-size" id="updates-size"></span>
                        </a>
                        <a class="updates-alt-link" id="updates-alt-link"
                           href="#" target="_blank" rel="noopener" hidden>
                            Or grab the portable zip
                        </a>
                    </div>
                    <div class="updates-footnote">
                        After the download finishes, quit {{ brand }} and run the installer.
                    </div>
                </div>

                <div class="updates-state updates-state-error" data-state-for="error" hidden>
                    <div class="updates-state-title">
                        <span class="updates-glyph" aria-hidden="true">!</span>
                        Couldn't check for updates
                    </div>
                    <div class="updates-state-detail" id="updates-error-detail">
                        Unknown error. Try again in a moment.
                    </div>
                </div>

                <div class="updates-state updates-state-unsupported" data-state-for="unsupported" hidden>
                    <div class="updates-state-title">
                        <span class="updates-glyph" aria-hidden="true">i</span>
                        No installer available for this platform yet
                    </div>
                    <div class="updates-state-detail">
                        Mac and Windows builds are published; this OS isn't shipped as an installer.
                    </div>
                </div>

                <div class="updates-meta">
                    <button type="button" class="updates-recheck" id="btn-updates-recheck">
                        Check now
                    </button>
                    <span class="updates-checked-at" id="updates-checked-at"></span>
                </div>
            </div>
        </section>
        </div><!-- /tab-panel: updates -->

        <div class="tab-panel" data-tab-panel="help" role="tabpanel" aria-labelledby="tab-btn-help" :hidden="$store.tabs.active !== 'help'">
            <section class="help-hero">
                <h1>Help is right here.</h1>
                <p>
                    Click any <strong>?</strong> bubble in any tab for in-context guidance; most fields explain themselves. Stuck on something specific, hit a bug, or have an idea you'd like to see in {{ brand }}? Send feedback below — it lands in our queue immediately and we may credit you on a future release if your idea ships.
                </p>
                <div class="help-actions">
                    <button type="button" class="feedback-btn" id="btn-open-feedback"
                            x-on:click="$store.feedback.open()">Send feedback</button>
                    <span class="secondary-line">No account needed. Email field is optional.</span>
                </div>
            </section>
            <section class="help-hero help-community">
                <h1>Talk to other operators.</h1>
                <p>
                    Live community on Discord — feature talk, fixture profiles, troubleshooting in real time. The founder (Kris) reads everything that lands there.
                </p>
                <div class="help-actions">
                    <a href="https://discord.gg/xUYgdCz8Rh" target="_blank" rel="noopener noreferrer" class="feedback-btn">Join the Discord</a>
                    <span class="secondary-line">Bug reports and feature requests are welcome here too — sometimes the fastest path to a fix.</span>
                </div>
            </section>
            <section class="help-hero help-diagnostics">
                <h1>For support</h1>
                <p>
                    If you're chasing down something with us, share the IDs below — they're stable, anonymous, and let us pull your events and crash reports out of the haystack.
                </p>
                <div class="diagnostics-rows">
                    <div class="diagnostics-row">
                        <span class="diagnostics-label">App version</span>
                        <code class="diagnostics-value" id="diag-version">{{ app_version }}</code>
                        <button type="button" class="diagnostics-copy" data-copy-target="diag-version" aria-label="Copy app version">Copy</button>
                    </div>
                    <div class="diagnostics-row">
                        <span class="diagnostics-label">Anonymous ID</span>
                        <code class="diagnostics-value" id="diag-anon-id">{{ anon_id or '—' }}</code>
                        <button type="button" class="diagnostics-copy" data-copy-target="diag-anon-id" aria-label="Copy anonymous ID"{% if not anon_id %} disabled{% endif %}>Copy</button>
                    </div>
                </div>
            </section>
        </div><!-- /tab-panel: help -->

        <div class="tab-panel" data-tab-panel="about" role="tabpanel" aria-labelledby="tab-btn-about" :hidden="$store.tabs.active !== 'about'">
            <section class="help-hero about-hero">
                <h1>{{ brand }}</h1>
                <p class="about-tagline">Let the music run the room.</p>
                <p>
                    {{ brand }} listens to whatever you're playing and drives your DMX, Govee, and MIDI fixtures in real time — built for venues, home setups, and anyone who wants their lights to actually feel the music.
                </p>
                <p class="about-link-line">
                    <a href="https://conductor.lighting" target="_blank" rel="noopener noreferrer">conductor.lighting</a>
                </p>
            </section>
            <section class="help-hero about-credits">
                <h2 class="about-section-title">Credits</h2>
                <ul class="about-list">
                    <li><strong>Kris Komar</strong> — design, code, signal flow, taste tests</li>
                </ul>
                <h2 class="about-section-title">Supporters</h2>
                <ul class="about-list">
                    <li><strong>Khloe the Dog</strong> — emotional support, beta tester (audio reactivity to squeaky toys still pending)</li>
                </ul>
                <p class="about-supporters-cta">
                    Want to be on this list? <button type="button" class="about-link-btn" data-jump-tab="help">Send great feedback.</button>
                </p>
            </section>
            <section class="help-hero about-acks">
                <h2 class="about-section-title">Built on the shoulders of</h2>
                <p class="about-acks-body">
                    Python, Flask &amp; Flask-SocketIO, sounddevice (PortAudio), NumPy, pywebview, ruamel.yaml, python-rtmidi, and PostHog (anonymous analytics, your call in Settings). On macOS we recommend BlackHole — a free virtual audio driver that lets {{ brand }} hear what your computer is playing.
                </p>
                {% if early_access_mode %}
                <p class="about-early-access">
                    <span class="about-ea-tag">Early Access Build</span>
                    Things may break, evolve, or surprise you. Your feedback shapes the GA release — use the Help tab to send it in.
                </p>
                {% endif %}
                <p class="about-copyright">
                    <img class="about-kl-wordmark" src="/static/images/kl-logo/komar-labs-wordmark.svg" alt="" aria-hidden="true">
                    © 2026 Komar Labs, LLC
                </p>
            </section>
        </div><!-- /tab-panel: about -->

        </div><!-- /shell-panels -->

        <!-- First-Run Telemetry Opt-In Modal. data-mode toggled by
             settings.js based on window.CONDUCTOR_EARLY_ACCESS:
               optional — pre-paid-tiers historical default; user picks
               required — early-access (KL-76); required to use, "No
                          thanks" exits the app -->
        <div class="modal-overlay" id="telemetry-opt-in-modal" hidden
             :hidden="!$store.telemetry.modalShown">
            <div class="modal-dialog" id="telemetry-opt-in-dialog" data-mode="optional"
                 :data-mode="$store.telemetry.mode"
                 role="dialog" aria-labelledby="telemetry-opt-in-title" aria-modal="true">
                <h3 id="telemetry-opt-in-title" data-mode-optional="Help improve Conductør?" data-mode-required="Anonymous telemetry — required during early access"
                    x-text="$store.telemetry.requiredMode ? $el.dataset.modeRequired : $el.dataset.modeOptional">Help improve Conductør?</h3>
                <p class="modal-intro" data-mode-optional="We'd like to send anonymous feature usage and crash reports so we know what to build next and what's silently breaking." data-mode-required="While Conductør is in early access we collect anonymous telemetry and crash reports so we can see what's breaking and ship fixes fast. It's required to use the alpha — but everything below is still true."
                   x-text="$store.telemetry.requiredMode ? $el.dataset.modeRequired : $el.dataset.modeOptional">
                    We'd like to send anonymous feature usage and crash reports so we know what to build next and what's silently breaking.
                </p>
                <ul class="modal-steps" style="list-style:none; padding-left:0;">
                    <li><strong>What we send:</strong> which features you use, whether upgrade prompts work, app version, platform, and unhandled errors with stack traces.</li>
                    <li><strong>What we never send:</strong> your audio, your config, your IP, your name, your email.</li>
                    <li class="telemetry-toggle-hint" data-mode-optional="Your call, anytime: toggle it in Settings → Anonymous Usage." data-mode-required="Once paid tiers ship, this becomes optional and toggleable in Settings → Anonymous Usage."
                        x-text="$store.telemetry.requiredMode ? $el.dataset.modeRequired : $el.dataset.modeOptional"><strong>Your call, anytime:</strong> toggle it in Settings &rarr; Anonymous Usage.</li>
                </ul>
                <div class="modal-actions">
                    <button class="action-btn modal-primary" id="btn-telemetry-optin-yes" data-mode-optional="Yes, help out" data-mode-required="Sounds good"
                            x-text="$store.telemetry.requiredMode ? $el.dataset.modeRequired : $el.dataset.modeOptional"
                            x-on:click="$store.telemetry.optInYes()">Yes, help out</button>
                    <button class="action-btn" id="btn-telemetry-optin-no"
                            x-on:click="$store.telemetry.optInNo($el)">No thanks</button>
                </div>
            </div>
        </div>

        <!-- Mac Audio Setup Modal -->
        <div class="modal-overlay" id="mac-audio-modal" hidden
             :hidden="!$store.macAudio.shown"
             x-on:click.self="$store.macAudio.close()">
            <div class="modal-dialog modal-wide" role="dialog" aria-labelledby="mac-audio-title" aria-modal="true">
                <button class="modal-close" id="btn-close-mac-audio" aria-label="Close"
                        x-on:click="$store.macAudio.close()">&times;</button>
                <h3 id="mac-audio-title">One thing first: macOS needs a setup step</h3>
                <p id="mac-audio-modal-detail" class="modal-intro">
                    Unlike Windows and Linux, <strong>macOS blocks apps from hearing the audio
                    your computer is playing.</strong> Right now, Conductør can't react to
                    Spotify, YouTube, Rekordbox, or anything else playing on this machine.
                    <strong>Without this setup, the app will look broken.</strong>
                </p>
                <p class="modal-intro">
                    Good news: it's a <strong>one-time, 3-minute, free</strong> fix.
                </p>
                <p class="modal-intro modal-intro-muted">
                    Using a mic, audio interface, or MIDI instead of Mac playback? You're
                    already set; skip this.
                </p>
                <div class="modal-actions">
                    <a class="action-btn modal-primary" id="btn-mac-audio-walkthrough"
                       href="https://conductor.lighting/faq.html#mac-setup"
                       :href="$store.macAudio.walkthroughUrl"
                       target="_blank" rel="noopener noreferrer">Walk me through it →</a>
                    <button class="action-btn" id="btn-mac-audio-dismiss-forever"
                            x-on:click="$store.macAudio.snooze(true)">I'll use mic / MIDI instead</button>
                </div>
                <div class="mac-audio-test-row mac-audio-test-row--secondary">
                    <span class="mac-audio-test-hint">Already set it up?</span>
                    <button class="action-btn" id="btn-mac-audio-test"
                            x-on:click="$store.macAudio.runCalibration()">Test audio</button>
                    <button class="action-btn" id="btn-mac-audio-recheck"
                            :disabled="$store.macAudio.recheckBusy"
                            x-text="$store.macAudio.recheckLabel"
                            x-on:click="$store.macAudio.recheck()">Recheck</button>
                    <span class="mac-audio-test-result" id="mac-audio-test-result"
                          :class="$store.macAudio.testResultLevel"
                          x-text="$store.macAudio.testResultText"></span>
                </div>
            </div>
        </div>

        <!-- KL-72: First-run Audio Source wizard -->
        <div class="modal-overlay" id="audio-source-wizard" hidden
             :hidden="!$store.audioWizard.shown"
             x-on:click.self="$store.audioWizard.markSkipAndClose()">
            <div class="modal-dialog modal-wide" role="dialog" aria-labelledby="audio-wizard-title" aria-modal="true">
                <button class="modal-close" id="btn-close-audio-wizard" aria-label="Close"
                        x-on:click="$store.audioWizard.markSkipAndClose()">&times;</button>
                <h3 id="audio-wizard-title">Pick your audio source</h3>
                <p class="modal-intro">
                    Conductør reacts to whatever it hears. Pick the source you want it
                    to listen to, then play something — when the bar wiggles, you're set.
                </p>
                <div class="audio-wizard-row">
                    <select id="audio-wizard-select" class="setting-select"
                            x-on:change="$store.audioWizard.scheduleSave()"></select>
                </div>
                <div class="signal-meter audio-wizard-meter" title="Live input level">
                    <div class="signal-meter-fill" id="audio-wizard-meter-fill"></div>
                    <div class="signal-meter-label" id="audio-wizard-meter-label">Signal: —</div>
                </div>
                <div class="setting-hint os-helper" id="audio-wizard-helper" hidden
                     :hidden="!$store.audioWizard.helperShown"
                     x-html="$store.audioWizard.helperContent"></div>
                <div class="audio-wizard-status" id="audio-wizard-status"
                     :class="$store.audioWizard.statusLevel"
                     x-text="$store.audioWizard.statusText"></div>
                <div class="modal-actions">
                    <button class="action-btn modal-primary" id="btn-audio-wizard-done"
                            x-on:click="$store.audioWizard.markDoneAndClose()">Looks good — start the show</button>
                    <button class="action-btn" id="btn-audio-wizard-skip"
                            x-on:click="$store.audioWizard.markSkipAndClose()">I'll set this up later</button>
                </div>
            </div>
        </div>

        <!-- Feedback modal (Help tab) -->
        <div class="feedback-overlay" id="feedback-overlay" hidden
             :hidden="!$store.feedback.shown"
             x-on:click.self="$store.feedback.close()">
            <div class="feedback-dialog" role="dialog" aria-labelledby="feedback-title" aria-modal="true">
                <h2 id="feedback-title">Send feedback to {{ brand }}</h2>
                <p class="lead" :hidden="$store.feedback.submitted">A bug, an idea, a question, whatever's on your mind. We auto-attach diagnostic info so we can debug.</p>
                <p class="fb-known-issues-link">
                    Filing a bug? <a href="https://conductor.lighting/known-issues.html" target="_blank" rel="noopener noreferrer">Check known issues first →</a>
                </p>

                <form id="feedback-form" class="is-bug" novalidate
                      :class="$store.feedback.formClass"
                      x-on:submit.prevent="$store.feedback.submit()"
                      x-on:input.debounce.500ms="$store.feedback.saveDraft()"
                      x-on:change="$store.feedback.saveDraft()">
                    <fieldset class="fb-radio-group">
                        <legend class="field-label">What's this?</legend>
                        <label><input type="radio" name="type" value="bug" checked x-model="$store.feedback.type"> Bug</label>
                        <label><input type="radio" name="type" value="feature" x-model="$store.feedback.type"> Feature idea</label>
                        <label><input type="radio" name="type" value="question" x-model="$store.feedback.type"> Question</label>
                        <label><input type="radio" name="type" value="other" x-model="$store.feedback.type"> Other</label>
                    </fieldset>

                    <label class="field">
                        <span class="field-label">Short summary</span>
                        <input type="text" name="title" id="fb-title" maxlength="120" required placeholder="One-line summary"
                               x-model="$store.feedback.title">
                    </label>

                    <div class="fb-bug-only">
                        <label class="field">
                            <span class="field-label">Steps to reproduce</span>
                            <textarea name="steps" id="fb-steps" rows="3" placeholder="1. ... 2. ... 3. ..."
                                      x-model="$store.feedback.steps"></textarea>
                        </label>
                        <label class="field">
                            <span class="field-label">What you expected</span>
                            <textarea name="expected" id="fb-expected" rows="2"
                                      x-model="$store.feedback.expected"></textarea>
                        </label>
                        <label class="field">
                            <span class="field-label">What actually happened</span>
                            <textarea name="actual" id="fb-actual" rows="2"
                                      x-model="$store.feedback.actual"></textarea>
                        </label>
                    </div>

                    <label class="field fb-freeform">
                        <span class="field-label">Details</span>
                        <textarea name="details" id="fb-details" rows="5" placeholder="Tell us more..."
                                  x-model="$store.feedback.details"></textarea>
                    </label>

                    <label class="field">
                        <span class="field-label">What were you doing? <span class="optional">(optional)</span></span>
                        <select name="activity" id="fb-activity" x-model="$store.feedback.activity">
                            <option value="">-- pick one --</option>
                            <option value="setup">Setup / first run</option>
                            <option value="fixtures">Configuring fixtures</option>
                            <option value="govee">Configuring Govee</option>
                            <option value="live">Running a live show</option>
                            <option value="other">Something else</option>
                        </select>
                    </label>

                    <label class="field">
                        <span class="field-label">Audio source <span class="optional">(optional)</span></span>
                        <select name="audio_source" id="fb-audio" x-model="$store.feedback.audio">
                            <option value="">-- pick one --</option>
                            <option value="loopback">Loopback (BlackHole / VB-Cable)</option>
                            <option value="mic">Microphone</option>
                            <option value="line">Line in</option>
                            <option value="midi">MIDI only</option>
                            <option value="unsure">Not sure</option>
                        </select>
                    </label>

                    <fieldset class="fb-radio-group fb-bug-only">
                        <legend class="field-label">Severity</legend>
                        <label><input type="radio" name="severity" value="crash" x-model="$store.feedback.severity"> Crash or freeze</label>
                        <label><input type="radio" name="severity" value="data_loss" x-model="$store.feedback.severity"> Data loss</label>
                        <label><input type="radio" name="severity" value="visual" x-model="$store.feedback.severity"> Visual glitch</label>
                        <label><input type="radio" name="severity" value="unexpected" checked x-model="$store.feedback.severity"> Unexpected behavior</label>
                        <label><input type="radio" name="severity" value="nitpick" x-model="$store.feedback.severity"> Nitpick</label>
                    </fieldset>

                    <label class="field">
                        <span class="field-label">Your email <span class="optional">(optional)</span></span>
                        <input type="email" name="email" id="fb-email" placeholder="So we can follow up if we have questions"
                               x-model="$store.feedback.email">
                    </label>

                    <div class="credit-row">
                        <input type="checkbox" id="fb-credit-me" name="credit_me"
                               x-model="$store.feedback.creditMe">
                        <label for="fb-credit-me">Credit me on a future {{ brand }} release if my idea ships.</label>
                    </div>
                    <label class="field credit-name">
                        <span class="field-label">Credit me as</span>
                        <input type="text" name="credit_name" id="fb-credit-name" maxlength="80" disabled placeholder="Your name, handle, or just 'anonymous'"
                               x-model="$store.feedback.creditName"
                               :disabled="!$store.feedback.creditMe">
                    </label>

                    <p class="fb-result" id="fb-result" hidden
                       :hidden="!$store.feedback.showResult"
                       :class="$store.feedback.resultKind"
                       x-html="$store.feedback.resultHtml"></p>

                    <div class="fb-actions">
                        <button type="button" class="btn-secondary" id="btn-feedback-cancel"
                                :hidden="$store.feedback.submitted"
                                x-on:click="$store.feedback.close()">Cancel</button>
                        <button type="submit" class="btn-primary" id="btn-feedback-send"
                                :hidden="$store.feedback.submitted"
                                :disabled="$store.feedback.submitBusy"
                                x-text="$store.feedback.submitLabel">Submit ticket</button>
                        <button type="button" class="btn-primary" id="btn-feedback-close" hidden
                                :hidden="!$store.feedback.submitted"
                                x-on:click="$store.feedback.close()">Close</button>
                    </div>
                    <p class="footnote">
                        We auto-attach the {{ brand }} version, OS, recent log entries, and an anonymized config summary (counts only). No license keys, IPs, hostnames, audio, or MAC addresses are sent.
                    </p>
                </form>
            </div>
        </div>

        <!-- API Key Help Modal -->

        <!-- First-run permission primer (KL-52). Renders only when
             /api/permissions/state reports first_run=true. Until the
             user clicks Continue, the engine thread is blocked
             waiting on a server-side gate so the OS prompts for mic
             + local network land *after* this primer is read. -->
        <div class="modal-overlay welcome-overlay" id="welcome-modal" hidden
             :hidden="!$store.welcome.shown">
            <div class="modal-dialog welcome-dialog" role="dialog" aria-labelledby="welcome-title" aria-modal="true">
                <h2 id="welcome-title" class="welcome-title">Welcome to Conductør</h2>
                <p class="welcome-intro">macOS is going to ask you for two permissions in a moment. Without them the lights won't react and your fixtures won't be reachable, so please click <strong>Allow</strong> on both.</p>
                <ul class="welcome-list">
                    <li>
                        <span class="welcome-glyph">🎙</span>
                        <div>
                            <strong>Microphone</strong>
                            <span class="welcome-detail">Conductør listens to your music through an audio loopback device (like BlackHole). macOS treats that as a microphone.</span>
                        </div>
                    </li>
                    <li>
                        <span class="welcome-glyph">📡</span>
                        <div>
                            <strong>Local Network</strong>
                            <span class="welcome-detail">Talks to your Govee bulbs and DMX gateways over WiFi. Without this, fixtures won't be discovered.</span>
                        </div>
                    </li>
                </ul>
                <p class="welcome-footnote">If you click <em>Don't Allow</em>, you'll need to grant access later in System Settings → Privacy &amp; Security. macOS won't ask twice.</p>
                {% if early_access_mode %}
                <p class="welcome-footnote welcome-ea">
                    <span class="welcome-ea-tag">Early Access</span>
                    You're on an in-development build. Things may break or change. Use the Help tab to send feedback — your reports shape the GA release.
                </p>
                {% endif %}
                <p class="welcome-footnote welcome-lumi">Lumi will be ready when you are.</p>
                <div class="modal-actions welcome-actions">
                    <button class="action-btn modal-primary" id="btn-welcome-continue" type="button"
                            :disabled="$store.welcome.busy"
                            x-text="$store.welcome.btnLabel"
                            x-on:click="$store.welcome.continue()">I'm ready — show the prompts</button>
                </div>
                <div class="modal-warning" id="welcome-error" hidden
                     :hidden="!$store.welcome.error"
                     x-text="$store.welcome.error"></div>
            </div>
        </div>

        <!-- KL-182: post-activation BYOK setup modal. Fires when the
             Director-status endpoint reports active_path === 'needs_byok_key'
             (Pro/Venue tier, no API key set, relay UI gated off) and the
             user hasn't dismissed it on this machine. The Director tab's
             BYOK card stays as the always-available fallback for users who
             skipped here. -->
        <div class="modal-overlay welcome-overlay byok-setup-overlay" id="byok-setup-modal" hidden
             :hidden="!$store.byokSetup.shown">
            <div class="modal-dialog welcome-dialog byok-setup-dialog" role="dialog" aria-labelledby="byok-setup-title" aria-modal="true">
                <h2 id="byok-setup-title" class="welcome-title">Set up the AI Director</h2>
                <p class="welcome-intro">
                    Your <strong x-text="$store.byokSetup.tierLabel">Pro</strong> license unlocks the AI Director.
                    Bring your own Anthropic API key &mdash; you control the meter, and a few dollars of credit lasts a long time at typical use.
                </p>
                <ol class="byok-setup-steps">
                    <li>Sign up at <a href="https://console.anthropic.com" target="_blank" rel="noopener">console.anthropic.com</a> and add a payment method (or pre-pay credit).</li>
                    <li>Open <strong>Settings → API Keys</strong> and click <strong>Create Key</strong>. Copy the value &mdash; you only see it once.</li>
                    <li>Paste it below.</li>
                </ol>
                <div class="byok-setup-input-row">
                    <input type="password" class="byok-setup-input"
                           autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
                           placeholder="sk-ant-…"
                           x-model="$store.byokSetup.inputValue"
                           x-on:input="$store.byokSetup.onInput()">
                </div>
                <div class="byok-setup-status" :class="$store.byokSetup.statusKind"
                     x-text="$store.byokSetup.statusText"></div>
                <p class="byok-setup-more">
                    Want more detail before pasting? <a href="https://conductor.lighting/byok.html" target="_blank" rel="noopener">Read the full setup guide</a> &mdash; cost numbers, spend caps, FAQ.
                </p>
                <div class="modal-actions welcome-actions byok-setup-actions">
                    <button class="action-btn" type="button"
                            x-on:click="$store.byokSetup.skip()">Skip for now</button>
                    <button class="action-btn modal-primary" type="button"
                            :disabled="$store.byokSetup.busy || !$store.byokSetup.inputValue"
                            x-on:click="$store.byokSetup.saveAndClose()"
                            x-text="$store.byokSetup.saveLabel"></button>
                </div>
            </div>
        </div>
    </main>

    <script src="/static/vendor/socket.io/socket.io-4.7.5.min.js"></script>
    <!-- main.js MUST come before Alpine. The CDN bundle queueMicrotasks
         Alpine.start() at script end, so by the time the next deferred
         script runs, Alpine.start has already walked the DOM and fired
         alpine:init. Putting main.js first lets feature modules attach
         alpine:init listeners that register stores before Alpine binds. -->
    <script type="module" src="/static/js/main.js"></script>
    <script defer src="/static/vendor/alpine/alpine-3.15.12.min.js"></script>
</body>
</html>
