Files
led-controller/src/templates/index.html
Jimmy aab62efd4f feat(ui): refresh layout, help assets, and panel styling
Update the main template and client scripts for the revised navigation
and zone/device panels, and add bundled help SVG assets under static.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 10:33:41 +12:00

1273 lines
86 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Controller - Zone Mode</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="app-container">
<header>
<div class="header-end">
<div class="zones-menu-mobile">
<button type="button" class="btn btn-secondary" id="zones-menu-btn" aria-haspopup="true" aria-expanded="false" aria-controls="zones-menu-dropdown">Zones</button>
<div id="zones-menu-dropdown" class="zones-menu-dropdown" role="menu" aria-label="Zones"></div>
</div>
<div class="nav-slide-toggle-wrap seq-switch-toggle-wrap edit-mode-only" id="seq-switch-toggle-wrap">
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--beat">Beat</span>
<button type="button" role="switch" class="nav-slide-toggle-switch seq-switch-toggle" id="seq-switch-toggle" aria-pressed="false" aria-label="Switch sequence on beat" title="When starting a sequence: wait for beat or downbeat">
<span class="nav-slide-toggle-track" aria-hidden="true"><span class="nav-slide-toggle-thumb"></span></span>
</button>
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--downbeat">Downbeat</span>
</div>
<div id="audio-top-indicator" class="audio-top-indicator">
<button type="button" id="audio-top-beat-sync" class="audio-beat-sync-btn audio-top-beat-sync" disabled title="Sync step to music (S)">
<span class="audio-top-indicator-label">BPM</span>
<span id="audio-top-bpm-value" class="audio-top-indicator-value">--</span>
<span id="audio-top-beat-readout" class="audio-top-beat-readout" aria-live="polite"></span>
<span id="audio-top-bar-phase" class="audio-top-bar-phase" aria-live="polite" title="Bar phase (beat in bar)"></span>
</button>
</div>
<div class="header-actions">
<div class="header-brightness-control">
<label for="header-brightness-slider">Brightness</label>
<input type="range" id="header-brightness-slider" min="0" max="255" value="255">
</div>
<button class="btn btn-secondary" id="profiles-btn">Profiles</button>
<button class="btn btn-secondary edit-mode-only" id="devices-btn">Devices</button>
<button class="btn btn-secondary edit-mode-only" id="groups-btn">Groups</button>
<button class="btn btn-secondary edit-mode-only" id="zones-btn">Zones</button>
<button class="btn btn-secondary edit-mode-only" id="presets-btn">Presets</button>
<button class="btn btn-secondary edit-mode-only" id="sequences-btn">Sequences</button>
<button class="btn btn-secondary edit-mode-only" id="patterns-btn">Patterns</button>
<button class="btn btn-secondary edit-mode-only" id="color-palette-btn">Colour Palette</button>
<button class="btn btn-secondary" id="audio-btn">Audio</button>
<button class="btn btn-secondary edit-mode-only" id="settings-btn">Settings</button>
<button class="btn btn-secondary" id="help-btn">Help</button>
<button type="button" class="btn btn-secondary ui-mode-toggle" id="ui-mode-toggle" aria-pressed="false" title="Switch preset strip mode — label is the mode you will switch to">Edit mode</button>
</div>
<div class="header-menu-mobile">
<button class="btn btn-secondary" id="main-menu-btn">Menu</button>
<div id="main-menu-dropdown" class="main-menu-dropdown">
<div class="nav-slide-toggle-wrap nav-slide-toggle-wrap--mobile seq-switch-toggle-wrap" id="seq-switch-toggle-wrap-mobile">
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--beat">Beat</span>
<button type="button" role="switch" class="nav-slide-toggle-switch seq-switch-toggle" id="seq-switch-toggle-mobile" aria-pressed="false" aria-label="Switch sequence on beat" title="Beat or downbeat">
<span class="nav-slide-toggle-track" aria-hidden="true"><span class="nav-slide-toggle-thumb"></span></span>
</button>
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--downbeat">Downbeat</span>
</div>
<button type="button" class="ui-mode-toggle" id="ui-mode-toggle-mobile" aria-pressed="false">Edit mode</button>
<div class="menu-brightness-control">
<label for="menu-brightness-slider">Brightness</label>
<input type="range" id="menu-brightness-slider" min="0" max="255" value="255">
</div>
<button type="button" data-target="profiles-btn">Profiles</button>
<button type="button" class="edit-mode-only" data-target="devices-btn">Devices</button>
<button type="button" class="edit-mode-only" data-target="groups-btn">Groups</button>
<button type="button" class="edit-mode-only" data-target="zones-btn">Zones</button>
<button type="button" class="edit-mode-only" data-target="presets-btn">Presets</button>
<button type="button" class="edit-mode-only" data-target="sequences-btn">Sequences</button>
<button type="button" class="edit-mode-only" data-target="patterns-btn">Patterns</button>
<button type="button" class="edit-mode-only" data-target="color-palette-btn">Colour Palette</button>
<button type="button" data-target="audio-btn">Audio</button>
<button type="button" class="edit-mode-only" data-target="settings-btn">Settings</button>
<button type="button" data-target="help-btn">Help</button>
</div>
</div>
</div>
<div class="zones-container">
<div id="zones-list">
Loading zones...
</div>
</div>
</header>
<div class="main-content">
<div id="zone-content" class="zone-content">
<div class="zone-content-placeholder">
Select a zone to get started
</div>
</div>
</div>
</div>
<!-- Zones Modal -->
<div id="zones-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Zones</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="zones-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions zone-modal-create-row">
<input type="text" id="new-zone-name" placeholder="Zone name">
<button class="btn btn-primary" id="create-zone-btn">Create</button>
</div>
<div id="zones-list-modal" class="profiles-list"></div>
</div>
</div>
<!-- Edit Zone Modal -->
<div id="edit-zone-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Edit Zone</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-zone-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-zone-modal').classList.remove('active')">Close</button>
</div>
</div>
<form id="edit-zone-form">
<input type="hidden" id="edit-zone-id">
<label>Zone Name:</label>
<input type="text" id="edit-zone-name" placeholder="Enter zone name" required>
<div id="edit-zone-block-groups">
<label class="zone-devices-label">Device groups on this zone</label>
<div id="edit-zone-groups-editor" class="zone-devices-editor"></div>
</div>
<div id="edit-zone-block-presets">
<label class="zone-presets-section-label">Presets on this zone</label>
<div id="edit-zone-presets-current" class="profiles-list edit-zone-presets-scroll"></div>
<label class="zone-presets-section-label">Add presets to this zone</label>
<div id="edit-zone-presets-list" class="profiles-list edit-zone-presets-scroll"></div>
</div>
<div id="edit-zone-block-sequences">
<label class="zone-presets-section-label">Sequences on this zone</label>
<div id="edit-zone-sequences-current" class="profiles-list edit-zone-presets-scroll"></div>
<label class="zone-presets-section-label">Add a sequence to this zone</label>
<div id="edit-zone-sequences-list" class="profiles-list edit-zone-presets-scroll"></div>
</div>
</form>
</div>
</div>
<!-- Profiles Modal -->
<div id="profiles-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Profiles</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="profiles-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" id="new-profile-name" placeholder="Profile name">
<button class="btn btn-primary" id="create-profile-btn">Create</button>
<button type="button" class="btn btn-secondary" id="import-profile-btn">Import</button>
</div>
<div class="form-group" style="margin-top: 0.5rem; margin-bottom: 0;">
<label style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
<input type="checkbox" id="new-profile-seed-dj">
DJ zone
</label>
</div>
<div id="profiles-list" class="profiles-list"></div>
</div>
</div>
<!-- Devices Modal (registry: Wi-Fi drivers appear when they connect over TCP) -->
<div id="devices-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Devices</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="devices-close-btn">Close</button>
</div>
</div>
<div class="form-group" style="margin-bottom:0.75rem;">
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;">
<input type="text" id="devices-add-name" placeholder="Device name" autocomplete="off" style="min-width:10rem;">
<select id="devices-add-transport">
<option value="espnow">ESP-NOW</option>
<option value="wifi">Wi-Fi</option>
</select>
<input type="text" id="devices-add-mac" placeholder="MAC (12 hex)" autocomplete="off" style="min-width:10rem;">
<input type="text" id="devices-add-address" placeholder="Address (IP/host for Wi-Fi)" autocomplete="off" style="min-width:12rem;" hidden>
<button type="button" class="btn btn-primary btn-small" id="devices-add-btn">Add device</button>
</div>
<small id="devices-add-status" class="muted-text" aria-live="polite"></small>
</div>
<div id="devices-list-modal" class="profiles-list"></div>
<div class="modal-actions" style="align-items:center;gap:0.75rem;flex-wrap:wrap;">
<button type="button" class="btn btn-secondary" id="devices-ping-btn" title="ESP-NOW broadcast ping (3 s)">Ping drivers</button>
<span id="devices-ping-dot" class="device-status-dot device-status-dot--unknown" role="img" title="Not pinged yet" aria-label="Not pinged yet"></span>
<span id="devices-ping-status" class="muted-text" aria-live="polite"></span>
<button type="button" class="btn btn-secondary" id="devices-update-groups-btn" title="Push group membership from Device groups to all ESP-NOW drivers">Update groups</button>
<span id="devices-groups-status" class="muted-text" aria-live="polite"></span>
</div>
</div>
</div>
<!-- Device groups: members + WiFi driver defaults (zones reference groups for presets) -->
<div id="groups-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Device groups</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="groups-close-btn">Close</button>
</div>
</div>
<p class="muted-text" style="margin-top:0;">Assign drivers to a group, set WiFi defaults once per group, then attach groups to a zone for standalone presets (sequences use each lanes groups only). By default new groups are <strong>shared</strong> across all profiles; tick “this profile only” to hide a group from other profiles.</p>
<div class="profiles-actions zone-modal-create-row">
<input type="text" id="new-group-name" placeholder="Group name">
<label class="muted-text" style="display:inline-flex;align-items:center;gap:0.35rem;white-space:nowrap;">
<input type="checkbox" id="new-group-profile-only"> This profile only
</label>
<button class="btn btn-primary" id="create-group-btn">Create</button>
</div>
<div id="groups-list-modal" class="profiles-list"></div>
</div>
</div>
<div id="edit-group-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Edit device group</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-group-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-group-close-btn">Close</button>
</div>
</div>
<form id="edit-group-form">
<input type="hidden" id="edit-group-id">
<label for="edit-group-name">Group name</label>
<input type="text" id="edit-group-name" required autocomplete="off">
<label class="muted-text" style="display:flex;align-items:flex-start;gap:0.5rem;margin-top:0.5rem;">
<input type="checkbox" id="edit-group-share-all-profiles" style="margin-top:0.2rem;">
<span>Share with all profiles (untick to keep this group on the <strong>current profile only</strong>)</span>
</label>
<label class="zone-devices-label">Devices in this group</label>
<div id="edit-group-devices-editor" class="zone-devices-editor"></div>
<div class="profiles-actions" style="margin-top: 0.5rem;">
<button type="button" class="btn btn-secondary btn-small" id="edit-group-identify-btn">Identify devices in group</button>
</div>
<p class="muted-text" style="margin-top:0.25rem;">Runs identify on every driver in the group at the same time so they blink together.</p>
<label for="edit-group-output-brightness" style="margin-top:0.75rem;display:block;">Group output brightness (0255)</label>
<div class="profiles-actions" style="align-items: center; gap: 0.75rem;">
<input type="range" id="edit-group-output-brightness" min="0" max="255" value="255" style="flex:1;">
<span id="edit-group-output-brightness-value" class="muted-text" style="min-width:2.5rem;">255</span>
</div>
</form>
</div>
</div>
<div id="edit-device-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Edit device</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-device-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-device-close-btn">Close</button>
</div>
</div>
<form id="edit-device-form">
<input type="hidden" id="edit-device-id">
<p class="muted-text" style="margin-bottom:0.75rem;">MAC (id): <code id="edit-device-storage-id"></code></p>
<label for="edit-device-name">Name</label>
<input type="text" id="edit-device-name" required autocomplete="off">
<label for="edit-device-type" style="margin-top:0.75rem;display:block;">Type</label>
<select id="edit-device-type">
<option value="led">LED</option>
</select>
<label for="edit-device-transport" style="margin-top:0.75rem;display:block;">Transport</label>
<select id="edit-device-transport">
<option value="espnow">ESP-NOW</option>
<option value="wifi">WiFi</option>
</select>
<div id="edit-device-address-espnow" style="margin-top:0.75rem;">
<label class="device-field-label">MAC (12 hex, optional)</label>
<div id="edit-device-address-boxes" class="hex-address-row" aria-label="MAC address"></div>
</div>
<div id="edit-device-address-wifi-wrap" style="margin-top:0.75rem;" hidden>
<label for="edit-device-address-wifi">Address (IP or hostname)</label>
<input type="text" id="edit-device-address-wifi" placeholder="192.168.1.50" autocomplete="off">
</div>
<div id="edit-device-wifi-driver-wrap" hidden>
<p class="muted-text" style="margin-top:0.75rem;margin-bottom:0.35rem;">On-device settings (sent over WiFi when connected). For shared defaults across several drivers, use <strong>Groups</strong>.</p>
<label for="edit-device-wifi-driver-name">Display name</label>
<input type="text" id="edit-device-wifi-driver-name" placeholder="hello / discovery name" autocomplete="off">
<label for="edit-device-wifi-num-leds" style="margin-top:0.5rem;display:block;">Number of LEDs</label>
<input type="number" id="edit-device-wifi-num-leds" min="1" max="2048" step="1" placeholder="119">
<label for="edit-device-wifi-color-order" style="margin-top:0.5rem;display:block;">Colour order</label>
<select id="edit-device-wifi-color-order">
<option value="rgb">RGB</option>
<option value="rbg">RBG</option>
<option value="grb">GRB</option>
<option value="gbr">GBR</option>
<option value="brg">BRG</option>
<option value="bgr">BGR</option>
</select>
<label for="edit-device-wifi-startup-mode" style="margin-top:0.5rem;display:block;">Power-on pattern</label>
<select id="edit-device-wifi-startup-mode">
<option value="default">Default preset</option>
<option value="last">Last preset</option>
<option value="off">Off</option>
</select>
</div>
<label for="edit-device-output-brightness" style="margin-top:0.75rem;display:block;">Output brightness (0255)</label>
<div class="profiles-actions" style="align-items: center; gap: 0.75rem;">
<input type="range" id="edit-device-output-brightness" min="0" max="255" value="255" style="flex:1;">
<span id="edit-device-output-brightness-value" class="muted-text" style="min-width:2.5rem;">255</span>
</div>
<small class="muted-text" style="display:block;margin-top:0.25rem;">Saved on the device; use <strong>Save</strong> to push to the driver (when connected).</small>
<label for="edit-device-debug" style="margin-top:1rem;display:block;">Debug</label>
<small class="muted-text" style="display:block;margin-bottom:0.35rem;">Stored registry row and the JSON preview for <strong>Save</strong> (updates as you edit).</small>
<textarea id="edit-device-debug" rows="8" readonly spellcheck="false" style="width:100%;font-family:monospace;resize:vertical;"></textarea>
</form>
</div>
</div>
<!-- Presets Modal -->
<div id="presets-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Presets</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="presets-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-primary" id="preset-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="import-preset-btn">Import</button>
<button class="btn btn-danger" id="preset-clear-device-btn">Clear Device Presets</button>
</div>
<div id="presets-list" class="profiles-list"></div>
</div>
</div>
<!-- Sequences Modal -->
<div id="sequences-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Sequences</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="sequences-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="sequence-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="sequences-open-presets-btn">Presets</button>
</div>
<div id="sequences-list" class="profiles-list"></div>
</div>
</div>
<!-- Sequence Editor Modal -->
<div id="sequence-editor-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Sequence</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" id="sequence-editor-save-btn">Save</button>
<button type="button" class="btn btn-secondary" id="sequence-editor-close-btn">Close</button>
</div>
</div>
<div class="preset-editor-field">
<label for="sequence-editor-name">Name</label>
<input type="text" id="sequence-editor-name" placeholder="Sequence name" style="width:100%;max-width:24rem;">
</div>
<div id="sequence-editor-beats-panel" style="margin:0 0 0.75rem 0;">
<p class="muted-text" style="font-size:0.85em;margin:0 0 0.5rem 0;">
Each step runs for the number of <strong>beats</strong> you set on that step.
When the header <strong>Audio</strong> detector is running, real beats advance the sequence.
When it is stopped, the server uses <strong>simulated</strong> beats at the BPM below.
</p>
<label for="sequence-editor-simulated-bpm" style="display:block;margin-bottom:0.25rem;">Simulated BPM (when audio is off)</label>
<input type="number" id="sequence-editor-simulated-bpm" min="30" max="300" value="120" style="width:6rem;" title="Used only while the audio detector is stopped">
<p id="sequence-editor-bpm-live" class="muted-text" style="font-size:0.85em;margin:0.5rem 0 0 0;"></p>
<label style="display:block;margin-top:0.65rem;">
<input type="checkbox" id="sequence-editor-loop" checked>
Loop sequence (restart from the first step after the last)
</label>
</div>
<div id="sequence-editor-lanes"></div>
<div class="modal-actions preset-editor-modal-actions">
<button type="button" class="btn btn-secondary btn-small" id="sequence-editor-add-lane-btn">Add lane</button>
<button type="button" class="btn btn-danger" id="sequence-editor-delete-btn">Delete</button>
</div>
</div>
</div>
<!-- Preset Editor Modal -->
<div id="preset-editor-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Preset</h2>
<div class="modal-top-actions">
<button class="btn btn-primary" id="preset-save-btn">Save</button>
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" id="preset-name-input" placeholder="Preset name">
<select id="preset-pattern-input">
<option value="">Pattern</option>
</select>
</div>
<label>Colours</label>
<div id="preset-colors-container" class="preset-colors-container"></div>
<div class="profiles-actions">
<input type="color" id="preset-new-color" value="#ffffff" title="Choose colour (auto-adds)">
<button class="btn btn-secondary btn-small" id="preset-add-from-palette-btn">From Palette</button>
</div>
<div class="profiles-actions">
<div class="preset-editor-field">
<label for="preset-brightness-input">Brightness (0255)</label>
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
</div>
<div class="preset-editor-field">
<label for="preset-delay-input">Delay (ms)</label>
<input type="number" id="preset-delay-input" placeholder="Delay" min="0" max="10000" value="0">
</div>
<div class="preset-editor-field">
<label for="preset-background-input">Background</label>
<div class="profiles-actions" style="gap: 0.4rem;">
<button type="button" class="btn btn-secondary btn-small" id="preset-background-btn" title="Choose background colour">#000000</button>
<button type="button" class="btn btn-secondary btn-small" id="preset-background-from-palette-btn">From Palette</button>
<input type="color" id="preset-background-input" value="#000000" title="Background colour used in patterns with background support" style="position:absolute;opacity:0;pointer-events:none;width:1px;height:1px;">
</div>
</div>
</div>
<div class="form-group" style="margin-top: 0.5rem; margin-bottom: 0;">
<label id="preset-manual-mode-label" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
<input type="checkbox" id="preset-manual-mode-input">
Manual mode (single-shot where supported)
</label>
<p id="preset-manual-mode-hint" class="muted-text" style="display: none; margin-top: 0.35rem; font-size: 0.85em;"></p>
<div id="preset-manual-beat-n-wrap" class="preset-editor-field" style="display: none; margin-top: 0.5rem;">
<label for="preset-manual-beat-n-input">Audio beat: every</label>
<input type="number" id="preset-manual-beat-n-input" min="1" max="64" value="1" style="width: 4rem;" title="Controller only; not sent to pattern logic" autocomplete="off">
<span class="muted-text" style="font-size: 0.85em;">beats (this app only)</span>
</div>
</div>
<div class="preset-editor-field" id="preset-reverse-group" hidden>
<label for="preset-reverse-input" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0;">
<input type="checkbox" id="preset-reverse-input">
Reverse direction (strip installed upside down)
</label>
</div>
<div class="preset-editor-field preset-mode-field" id="preset-mode-group" hidden>
<label for="preset-mode-input" id="preset-mode-label">Mode</label>
<select id="preset-mode-input" class="preset-mode-input"></select>
</div>
<div class="n-params-grid">
<div class="n-param-group">
<label for="preset-n1-input" id="preset-n1-label">n1:</label>
<input type="number" id="preset-n1-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n2-input" id="preset-n2-label">n2:</label>
<input type="number" id="preset-n2-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n3-input" id="preset-n3-label">n3:</label>
<input type="number" id="preset-n3-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n4-input" id="preset-n4-label">n4:</label>
<input type="number" id="preset-n4-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n5-input" id="preset-n5-label">n5:</label>
<input type="number" id="preset-n5-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n6-input" id="preset-n6-label">n6:</label>
<input type="number" id="preset-n6-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n7-input" id="preset-n7-label">n7:</label>
<input type="number" id="preset-n7-input" min="0" max="255" value="0" class="n-input">
</div>
<div class="n-param-group">
<label for="preset-n8-input" id="preset-n8-label">n8:</label>
<input type="number" id="preset-n8-input" min="0" max="255" value="0" class="n-input">
</div>
</div>
<div class="modal-actions preset-editor-modal-actions">
<button class="btn btn-secondary" id="preset-send-btn">Try</button>
<button class="btn btn-secondary" id="preset-default-btn">Default</button>
<button type="button" class="btn btn-danger" id="preset-remove-from-zone-btn" hidden>Remove from zone</button>
</div>
</div>
</div>
<!-- Patterns Modal -->
<div id="patterns-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Patterns</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="patterns-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="pattern-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="pattern-send-all-btn">Send All Patterns</button>
</div>
<div id="patterns-list" class="profiles-list"></div>
</div>
</div>
<!-- Pattern Editor Modal -->
<div id="pattern-editor-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Pattern</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" id="pattern-create-btn">Save</button>
<button type="button" class="btn btn-secondary" id="pattern-editor-close-btn">Close</button>
</div>
</div>
<p class="muted-text" style="margin: 0 0 0.75rem 0;">Add a driver <code>.py</code> file and editor metadata (stored in the pattern database).</p>
<div class="profiles-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
<label for="pattern-create-name" style="min-width: 7rem;">Name</label>
<input type="text" id="pattern-create-name" class="preset-name-like" placeholder="e.g. sparkle" pattern="[a-zA-Z_][a-zA-Z0-9_]*" style="flex: 1; min-width: 12rem;" autocomplete="off">
</div>
<div id="pattern-create-n-section" class="n-params-section" style="margin-bottom: 0.5rem;">
<h3 class="muted-text">Readable parameter names</h3>
<p id="pattern-create-n-empty" class="muted-text" style="display: none; margin: 0 0 0.5rem 0;">No parameter names are stored for this pattern.</p>
<div class="n-params-grid">
<div class="n-param-group">
<label for="pattern-create-n1"></label>
<input type="text" id="pattern-create-n1" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n2"></label>
<input type="text" id="pattern-create-n2" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n3"></label>
<input type="text" id="pattern-create-n3" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n4"></label>
<input type="text" id="pattern-create-n4" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n5"></label>
<input type="text" id="pattern-create-n5" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n6"></label>
<input type="text" id="pattern-create-n6" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n7"></label>
<input type="text" id="pattern-create-n7" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
<div class="n-param-group">
<label for="pattern-create-n8"></label>
<input type="text" id="pattern-create-n8" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
</div>
</div>
</div>
<div class="profiles-row pattern-editor-meta-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
<label for="pattern-create-min-delay" style="min-width: 7rem;">Min delay (ms)</label>
<input type="number" id="pattern-create-min-delay" min="0" value="10">
<label for="pattern-create-max-delay">Max delay (ms)</label>
<input type="number" id="pattern-create-max-delay" min="0" value="10000">
<label for="pattern-create-max-colors">Max colours</label>
<input type="number" id="pattern-create-max-colors" min="0" value="10">
</div>
<div class="profiles-row" style="flex-direction: column; align-items: stretch; gap: 0.35rem; margin-bottom: 0.5rem;">
<label for="pattern-create-file">Pattern file</label>
<input type="file" id="pattern-create-file" accept=".py,text/x-python,.PY">
<label for="pattern-create-code" class="muted-text" style="font-size: 0.85em;">Or paste Python source (if no file chosen)</label>
<textarea id="pattern-create-code" rows="5" style="width: 100%; font-family: monospace; font-size: 0.85rem;" placeholder="# class MyPattern: ..."></textarea>
</div>
<div class="modal-actions">
<label style="display: inline-flex; align-items: center; gap: 0.35rem; margin-right: auto;">
<input type="checkbox" id="pattern-create-overwrite" checked>
<span>Overwrite existing file</span>
</label>
</div>
</div>
</div>
<!-- Colour Palette Modal -->
<div id="color-palette-modal" class="modal">
<div class="modal-content">
<div class="modal-head">
<h2>Colour Palette</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
</div>
</div>
<p class="muted-text">Profile: <span id="palette-current-profile-name">None</span></p>
<div id="palette-container" class="profiles-list"></div>
<div class="profiles-actions">
<input type="color" id="palette-new-color" value="#ffffff">
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="modal">
<div class="modal-content help-modal-content">
<div class="modal-head">
<h2>Help</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="help-close-btn">Close</button>
</div>
</div>
<p class="muted-text help-modal-intro">How to use the LED controller UI. Previews use the same styles as the live interface.</p>
<div class="help-tabs" role="tablist" aria-label="Help sections">
<button type="button" class="help-tab-btn active" role="tab" id="help-tab-overview" data-help-tab="overview" aria-selected="true" aria-controls="help-panel-overview">Overview</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-profiles" data-help-tab="profiles" aria-selected="false" aria-controls="help-panel-profiles">Profiles</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-devices" data-help-tab="devices" aria-selected="false" aria-controls="help-panel-devices">Devices</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-groups" data-help-tab="groups" aria-selected="false" aria-controls="help-panel-groups">Groups</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-zones" data-help-tab="zones" aria-selected="false" aria-controls="help-panel-zones">Zones</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-presets" data-help-tab="presets" aria-selected="false" aria-controls="help-panel-presets">Presets</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-sequences" data-help-tab="sequences" aria-selected="false" aria-controls="help-panel-sequences">Sequences</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-patterns" data-help-tab="patterns" aria-selected="false" aria-controls="help-panel-patterns">Patterns</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-colour-palette" data-help-tab="colour-palette" aria-selected="false" aria-controls="help-panel-colour-palette">Colour Palette</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-audio" data-help-tab="audio" aria-selected="false" aria-controls="help-panel-audio">Audio</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-settings" data-help-tab="settings" aria-selected="false" aria-controls="help-panel-settings">Settings</button>
</div>
<div id="help-panel-overview" class="help-tab-panel active" data-help-panel="overview" role="tabpanel" aria-labelledby="help-tab-overview">
<div class="help-ui-preview help-ui-preview--header" aria-hidden="true">
<div class="help-preview-header">
<div class="header-end">
<div class="header-actions">
<div class="header-brightness-control">
<label>Brightness</label>
<input type="range" min="0" max="255" value="200" tabindex="-1">
</div>
<button type="button" class="btn btn-secondary" tabindex="-1">Profiles</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Devices</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Zones</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Presets</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Audio</button>
<button type="button" class="btn btn-secondary ui-mode-toggle" tabindex="-1">Run mode</button>
</div>
</div>
<div class="zones-container">
<div class="zones-list">
<button type="button" class="zone-button" tabindex="-1">default</button>
<button type="button" class="zone-button active" tabindex="-1">lounge</button>
<button type="button" class="zone-button" tabindex="-1">dj</button>
</div>
</div>
</div>
</div>
<p class="help-ui-preview-caption">Zone buttons below the header; management buttons on the right (Edit mode).</p>
<h3>Run mode and Edit mode</h3>
<ul>
<li><strong>Run mode</strong>: day-to-day control — choose a zone, tap presets, apply profiles. Management buttons are hidden.</li>
<li><strong>Edit mode</strong>: full setup — zones, presets, sequences, patterns, colour palette, and per-tile <strong>Edit</strong> on the strip.</li>
<li><strong>Switch modes</strong>: use the mode button in the header or mobile menu. The label shows the mode you will switch <em>to</em>.</li>
</ul>
<div class="help-ui-preview help-ui-preview--strip" aria-hidden="true">
<div class="zone-content">
<div class="presets-section">
<div class="help-preview-presets-grid">
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main active" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#ffd54f 0%,#fff8e1 100%)" tabindex="-1"><span class="pattern-button-label">warm white</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#e53935 0%,#1e88e5 100%)" tabindex="-1"><span class="pattern-button-label">rainbow</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#00897b 0%,#4db6ac 100%)" tabindex="-1"><span class="pattern-button-label">pulse</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p class="help-ui-preview-caption">Click a preset tile to select it on all devices in the zone.</p>
<ul>
<li><strong>Select zone</strong>: click a zone button in the top bar.</li>
<li><strong>Brightness</strong>: the header slider adjusts global brightness for the current zone.</li>
<li><strong>Edit mode</strong>: drag preset tiles to reorder; use <strong>Edit</strong> and <strong>Remove</strong> on each tile.</li>
</ul>
<div class="help-ui-preview help-ui-preview--mobile" aria-hidden="true">
<div class="help-preview-mobile-bar">
<button type="button" class="btn btn-secondary" tabindex="-1">Menu</button>
</div>
<div class="main-menu-dropdown">
<button type="button" tabindex="-1">Run mode</button>
<button type="button" tabindex="-1">Profiles</button>
<button type="button" tabindex="-1">Zones</button>
<button type="button" tabindex="-1">Presets</button>
<button type="button" tabindex="-1">Help</button>
</div>
</div>
<p class="help-ui-preview-caption">On narrow screens, <strong>Menu</strong> reaches the same actions as the desktop header.</p>
</div>
<div id="help-panel-profiles" class="help-tab-panel" data-help-panel="profiles" role="tabpanel" aria-labelledby="help-tab-profiles" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Profiles</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" value="New profile" readonly tabindex="-1">
<button type="button" class="btn btn-primary" tabindex="-1">Create</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Import</button>
</div>
<div class="profiles-list">
<div class="profiles-row">
<span style="font-weight:bold;color:#FFD700">&#10003; House default</span>
<span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Export</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Clone</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Delete</button>
</span>
</div>
<div class="profiles-row">
<span>Garden party</span>
<span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Export</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Clone</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Delete</button>
</span>
</div>
</div>
</div>
</div>
<ul>
<li><strong>Apply</strong>: sets the current profile. Zones and presets you see are scoped to that profile.</li>
<li><strong>Create</strong> (Edit mode): new profiles get a populated <strong>default</strong> zone. Optionally tick <strong>DJ zone</strong> for a starter <code>dj</code> zone.</li>
<li><strong>Clone</strong> / <strong>Delete</strong>: available in Edit mode from the profile list.</li>
<li>In Run mode you can only apply profiles; create, clone, and delete are hidden.</li>
</ul>
</div>
<div id="help-panel-devices" class="help-tab-panel" data-help-panel="devices" role="tabpanel" aria-labelledby="help-tab-devices" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Devices</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-list">
<div class="profiles-row">
<span class="device-status-dot device-status-dot--online" role="img"></span>
<span style="flex:1">lounge strip</span>
<code class="device-row-mac">AA:BB:CC:DD:EE:01</code>
<span class="muted-text" style="font-size:0.85em">led · espnow · —</span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Identify</button>
</div>
<div class="profiles-row">
<span class="device-status-dot device-status-dot--unknown" role="img"></span>
<span style="flex:1">ceiling</span>
<code class="device-row-mac">AA:BB:CC:DD:EE:02</code>
<span class="muted-text" style="font-size:0.85em">led · espnow · —</span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Identify</button>
</div>
</div>
<div class="modal-actions" style="justify-content:flex-start;margin-top:0.75rem;">
<button type="button" class="btn btn-secondary" tabindex="-1">Ping drivers</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Update groups</button>
</div>
</div>
</div>
<ul>
<li><strong>Devices</strong> (Edit mode): registry of LED drivers keyed by <strong>MAC</strong>.</li>
<li>ESP-NOW devices appear automatically after <strong>ANNOUNCE</strong>; you can also add rows manually.</li>
<li><strong>Identify</strong>: short red blink (~2 s) so you can spot hardware.</li>
<li><strong>Update groups</strong>: pushes group membership from device groups to ESP-NOW drivers.</li>
<li>Edit a device for transport, IP, and per-driver settings; use <strong>Groups</strong> for shared WiFi defaults.</li>
</ul>
</div>
<div id="help-panel-groups" class="help-tab-panel" data-help-panel="groups" role="tabpanel" aria-labelledby="help-tab-groups" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Device groups</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions zone-modal-create-row">
<input type="text" value="Group name" readonly tabindex="-1">
<button type="button" class="btn btn-primary" tabindex="-1">Create</button>
</div>
<div class="profiles-list">
<div class="group-list-row">
<div class="group-list-row-info">
<div class="group-list-row-title">lounge lights (3 devices)</div>
<div class="group-list-row-meta muted-text">Shared across profiles</div>
</div>
<div class="group-list-row-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply brightness</button>
</div>
</div>
<div class="group-list-row">
<div class="group-list-row-info">
<div class="group-list-row-title">dj booth (2 devices)</div>
<div class="group-list-row-meta muted-text">This profile only</div>
</div>
<div class="group-list-row-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply brightness</button>
</div>
</div>
</div>
</div>
</div>
<ul>
<li>Assign drivers to a <strong>group</strong>, set WiFi defaults once per group, then attach groups to a zone.</li>
<li>Standalone presets use the zones device groups. Sequence lanes each target their own group.</li>
<li>New groups are <strong>shared</strong> across profiles by default; tick <strong>this profile only</strong> to hide a group elsewhere.</li>
<li>In the group editor, search and pick devices from the list to add members; <strong>Identify devices in group</strong> blinks them together.</li>
</ul>
</div>
<div id="help-panel-zones" class="help-tab-panel" data-help-panel="zones" role="tabpanel" aria-labelledby="help-tab-zones" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Edit Zone</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<label>Zone Name:</label>
<input type="text" value="lounge" readonly tabindex="-1">
<label class="zone-devices-label">Device groups on this zone</label>
<div class="profiles-list">
<div class="profiles-row edit-zone-item-row"><span>lounge lights</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
<label class="zone-presets-section-label">Presets on this zone</label>
<div class="profiles-list edit-zone-presets-scroll">
<div class="profiles-row edit-zone-item-row"><span>warm white</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
<div class="profiles-row edit-zone-item-row"><span>rainbow</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
<label class="zone-presets-section-label">Sequences on this zone</label>
<div class="profiles-list edit-zone-presets-scroll">
<div class="profiles-row edit-zone-item-row"><span>intro build</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
</div>
</div>
<ul>
<li><strong>Zones</strong> (Edit mode): create and manage zones from the header <strong>Zones</strong> button.</li>
<li>Each zone lists <strong>device groups</strong>, <strong>presets</strong>, and <strong>sequences</strong> — presets and sequences can share the same zone.</li>
<li>Drag presets on the main strip or in the zone editor to reorder.</li>
<li>Right-click a zone button for quick access to zone settings.</li>
</ul>
</div>
<div id="help-panel-presets" class="help-tab-panel" data-help-panel="presets" role="tabpanel" aria-labelledby="help-tab-presets" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Preset</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" value="evening glow" readonly tabindex="-1">
<select tabindex="-1"><option>pulse</option></select>
</div>
<label>Colours</label>
<div class="preset-colors-container">
<div class="help-preview-color-swatch" style="background-color:#7e57c2"><span class="help-preview-p-badge">P</span></div>
<div class="help-preview-color-swatch" style="background-color:#26a69a"></div>
</div>
<div class="profiles-actions">
<input type="color" value="#ffffff" tabindex="-1">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">From Palette</button>
</div>
<div class="modal-actions preset-editor-modal-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Try</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Default</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Send</button>
</div>
</div>
</div>
<ul>
<li><strong>Presets</strong> (Edit mode): profile-wide list — <strong>Add</strong>, <strong>Edit</strong>, <strong>Send</strong>, and <strong>Delete</strong>.</li>
<li><strong>Pattern</strong> and optional <strong>n1n8</strong> fields depend on the pattern.</li>
<li><strong>From Palette</strong>: inserts a colour linked to the profile palette (badge <strong>P</strong>).</li>
<li><strong>Try</strong>: previews on the current zone without saving on the device.</li>
<li><strong>Save</strong>: writes the preset to the server (does not close the editor).</li>
<li><strong>Send</strong>: pushes the definition to devices with save.</li>
<li><strong>Remove from zone</strong> (when opened from a zone): removes from this zone only.</li>
</ul>
</div>
<div id="help-panel-sequences" class="help-tab-panel" data-help-panel="sequences" role="tabpanel" aria-labelledby="help-tab-sequences" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Sequence</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="preset-editor-field">
<label>Name</label>
<input type="text" value="intro build" readonly tabindex="-1">
</div>
<p class="muted-text" style="font-size:0.85em;margin:0.5rem 0;">Lane 1 — lounge lights</p>
<div class="profiles-list">
<div class="sequence-step-row profiles-row" style="display:flex;flex-direction:column;gap:0.35rem;padding:0.5rem;border:1px solid rgba(255,255,255,0.12);border-radius:6px;">
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
<label>Preset</label>
<select tabindex="-1"><option>warm white — 1</option></select>
<label>Beats</label>
<input type="number" value="4" readonly style="width:4rem" tabindex="-1">
</div>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Add lane</button>
</div>
</div>
</div>
<ul>
<li><strong>Sequences</strong> (Edit mode): build multi-step shows with one or more <strong>lanes</strong> (each lane targets a device group).</li>
<li>Add presets as steps per lane; open from the zone editor to attach a sequence to a zone.</li>
<li><strong>Beat</strong> / <strong>Downbeat</strong> toggle (header): when starting a sequence, wait for beat or downbeat before step 1.</li>
<li>Tap <kbd>S</kbd> or the BPM button during playback to sync step timing to music (with Audio running).</li>
</ul>
</div>
<div id="help-panel-patterns" class="help-tab-panel" data-help-panel="patterns" role="tabpanel" aria-labelledby="help-tab-patterns" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Patterns</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="modal-actions" style="margin-top:0;justify-content:flex-start;">
<button type="button" class="btn btn-primary" tabindex="-1">Add</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Send All Patterns</button>
</div>
<div class="profiles-list">
<div class="profiles-row"><span>pulse</span><span class="muted-text">delay 20200 ms</span><button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button></div>
<div class="profiles-row"><span>rainbow</span><span class="muted-text">delay 1080 ms</span><button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button></div>
</div>
</div>
</div>
<ul>
<li><strong>Patterns</strong> (Edit mode): reference list of pattern names and typical delay ranges.</li>
<li>Choose the pattern inside the preset editor; parameters map to <strong>n1n8</strong>.</li>
<li>WiFi drivers can install pattern modules over HTTP (OTA upload); ESP-NOW devices use the bridge you configure in <strong>Settings</strong>.</li>
</ul>
</div>
<div id="help-panel-colour-palette" class="help-tab-panel" data-help-panel="colour-palette" role="tabpanel" aria-labelledby="help-tab-colour-palette" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Colour Palette</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<p class="muted-text">Profile: <span>House default</span></p>
<div id="palette-container" class="profiles-list">
<div class="profiles-row" style="display:flex;align-items:center;gap:1rem;">
<div style="width:64px;height:64px;border-radius:8px;background:#7e57c2;border:2px solid #4a4a4a;flex-shrink:0;"></div>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
<div class="profiles-row" style="display:flex;align-items:center;gap:1rem;">
<div style="width:64px;height:64px;border-radius:8px;background:#26a69a;border:2px solid #4a4a4a;flex-shrink:0;"></div>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
<div class="profiles-actions">
<input type="color" value="#ffffff" tabindex="-1">
</div>
</div>
</div>
<p class="help-ui-preview-caption">Add or change swatches; linked preset colours update automatically.</p>
<ul>
<li><strong>Colour Palette</strong> (Edit mode): edits the current profiles palette swatches.</li>
<li>Use <strong>From Palette</strong> in the preset editor for colours that stay in sync (badge <strong>P</strong>).</li>
</ul>
</div>
<div id="help-panel-audio" class="help-tab-panel" data-help-panel="audio" role="tabpanel" aria-labelledby="help-tab-audio" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content audio-modal-content help-preview-surface">
<div class="modal-head">
<h2>Audio Beat Detection</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="form-group audio-device-block">
<label>Input device</label>
<div class="profiles-actions audio-device-select-row">
<select tabindex="-1"><option>Monitor of Built-in Audio</option></select>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Refresh</button>
</div>
</div>
<div class="form-group">
<label>Beat indicators</label>
<button type="button" class="audio-beat-sync-btn audio-modal-beat-sync" tabindex="-1">
<span class="audio-top-indicator-label">BPM</span>
<span class="audio-top-indicator-value">128</span>
</button>
</div>
<div class="form-group audio-volume-block">
<div class="audio-volume-header">
<label>Volume</label>
<span class="audio-volume-readout">100% (0.00 dB)</span>
</div>
<input type="range" class="audio-volume-slider" min="0" max="200" value="100" tabindex="-1">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Start</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Stop</button>
</div>
</div>
</div>
<ul>
<li><strong>Audio</strong>: beat detection from a chosen input device (monitor sources follow playback).</li>
<li>BPM and beat indicators appear in the header and Audio modal while detection is running.</li>
<li>Adjust <strong>Volume</strong> (gain before detection); the level meter shows live input.</li>
<li><strong>Start</strong> / <strong>Stop</strong> detection; <strong>Reset detector</strong> clears stuck BPM tracking.</li>
<li>Sync sequences to music with <kbd>S</kbd> on a downbeat while a sequence plays.</li>
</ul>
</div>
<div id="help-panel-settings" class="help-tab-panel" data-help-panel="settings" role="tabpanel" aria-labelledby="help-tab-settings" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content settings-modal-content help-preview-surface">
<div class="modal-head">
<h2>Settings</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="settings-tabs" role="tablist">
<button type="button" class="settings-tab-btn active" tabindex="-1">Bridge</button>
<button type="button" class="settings-tab-btn" tabindex="-1">LED Tool</button>
</div>
<div class="settings-section">
<span class="muted-text">USB serial: /dev/ttyUSB0 (connected)</span>
<h3 class="settings-subheading" style="margin-top:0.75rem;">Wi-Fi</h3>
<p class="muted-text" style="margin:0;">Bridge-AP — ws://192.168.4.1/ws</p>
</div>
</div>
</div>
<ul>
<li><strong>Settings</strong> (Edit mode): <strong>Bridge</strong> connects the Pi to ESP-NOW hardware over USB serial or WiFi.</li>
<li>Save bridge profiles, scan for the bridge AP, and check connection status.</li>
<li><strong>LED Tool</strong>: USB serial setup for drivers — <code>settings.json</code>, deploy, flash, and maintenance.</li>
<li>LED Tool configures devices directly; this UI controls profiles, zones, presets, and runtime messages.</li>
</ul>
</div>
</div>
</div>
<!-- Audio Modal -->
<div id="audio-modal" class="modal">
<div class="modal-content audio-modal-content">
<div class="modal-head">
<h2>Audio Beat Detection</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="audio-close-btn">Close</button>
</div>
</div>
<div class="form-group audio-device-block">
<label for="audio-device-select">Input device</label>
<div class="profiles-actions audio-device-select-row">
<select id="audio-device-select">
<option value="">System default input</option>
</select>
<button type="button" class="btn btn-secondary btn-small" id="audio-refresh-btn" title="Refresh device list">Refresh</button>
</div>
<small class="muted-text">Same sources as PulseAudio volume control. Pick a <strong>monitor</strong> source to follow playback.</small>
</div>
<div class="form-group">
<label>Beat indicators</label>
<button type="button" id="audio-modal-beat-sync" class="audio-beat-sync-btn audio-modal-beat-sync" disabled title="Start beat detection">
<span class="audio-top-indicator-label">BPM</span>
<span id="audio-bpm-value" class="audio-top-indicator-value">--</span>
<span id="audio-modal-beat-readout" class="audio-top-beat-readout" aria-live="polite"></span>
<span id="audio-bar-phase-value" class="audio-top-bar-phase" aria-live="polite" title="Bar phase (beat in bar)"></span>
</button>
<small class="muted-text">Flashes on each beat (same as the header). Tap on a downbeat while a sequence is playing to sync (<kbd>S</kbd>).</small>
</div>
<div class="form-group">
<label>Detected hit type</label>
<div id="audio-hit-type-value" class="audio-hit-type-readout">unknown</div>
</div>
<div class="form-group audio-volume-block">
<div class="audio-volume-header">
<label for="audio-input-volume">Volume</label>
<span id="audio-input-volume-readout" class="audio-volume-readout" aria-live="polite">100% (0.00 dB)</span>
</div>
<div class="audio-volume-slider-row">
<input type="range" id="audio-input-volume" class="audio-volume-slider" min="0" max="200" value="100" step="1" aria-label="Input gain">
</div>
<div class="audio-volume-scale" aria-hidden="true">
<span class="audio-volume-scale-silence">Silence</span>
<span class="audio-volume-scale-unity">100% (0 dB)</span>
</div>
<div class="audio-input-level-meter" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Live input level">
<div id="audio-input-level-bar" class="audio-input-level-bar"></div>
</div>
<small class="muted-text">Gain before beat detection (saved on the controller). The bar shows live input level while running.</small>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="audio-start-btn">Start</button>
<button type="button" class="btn btn-secondary" id="audio-stop-btn">Stop</button>
<button type="button" class="btn btn-secondary" id="audio-reset-btn" disabled title="Clear stuck BPM / beat tracking">Reset detector</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal">
<div class="modal-content settings-modal-content">
<div class="modal-head">
<h2>Settings</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="settings-close-btn">Close</button>
</div>
</div>
<div class="settings-tabs" role="tablist" aria-label="Settings sections">
<button type="button" class="settings-tab-btn active" role="tab" id="settings-tab-bridge" data-settings-tab="bridge" aria-selected="true" aria-controls="settings-panel-bridge">Bridge</button>
<button type="button" class="settings-tab-btn" role="tab" id="settings-tab-led-tool" data-settings-tab="led-tool" aria-selected="false" aria-controls="settings-panel-led-tool">LED Tool</button>
</div>
<div id="settings-panel-bridge" class="settings-tab-panel active" data-settings-panel="bridge" role="tabpanel" aria-labelledby="settings-tab-bridge">
<div class="settings-section">
<div class="profiles-actions" style="align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.5rem;">
<span id="bridge-ws-status" class="muted-text" aria-live="polite"></span>
</div>
<ul id="bridge-connection-details" class="settings-bridge-connection-details muted-text" aria-live="polite"></ul>
<h3 class="settings-subheading">USB serial</h3>
<p class="muted-text" style="margin-top:0;margin-bottom:0.75rem;">Pi ↔ bridge over USB/UART. The bridge still uses WiFi radio for ESP-NOW only.</p>
<div class="form-group">
<label for="bridge-serial-label">Profile label</label>
<input type="text" id="bridge-serial-label" placeholder="e.g. Pi USB bridge" autocomplete="off">
</div>
<div class="form-group">
<label for="bridge-serial-port">USB serial port</label>
<select id="bridge-serial-port">
<option value="">— select port —</option>
</select>
<button type="button" class="btn btn-secondary btn-small" id="bridge-serial-refresh-btn" style="margin-top:0.5rem;">Refresh ports</button>
</div>
<div class="form-group">
<label for="bridge-serial-baud">Baud rate</label>
<input type="number" id="bridge-serial-baud" value="115200" min="9600" max="3000000" step="1">
</div>
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;margin-bottom:1.25rem;">
<button type="button" class="btn btn-primary btn-small" id="bridge-serial-connect-btn">Connect serial</button>
<button type="button" class="btn btn-secondary btn-small" id="bridge-serial-save-profile-btn">Save serial profile</button>
</div>
<h3 class="settings-subheading">WiFi</h3>
<p class="muted-text" style="margin-top:0;margin-bottom:0.75rem;">Pi joins the bridge access point, then connects to <code>ws://&lt;bridge-ip&gt;/ws</code>.</p>
<div class="form-group">
<label for="bridge-wifi-interface">WiFi adapter</label>
<select id="bridge-wifi-interface">
<option value="">— select adapter —</option>
</select>
<button type="button" class="btn btn-secondary btn-small" id="bridge-wifi-refresh-interfaces-btn" style="margin-top:0.5rem;">Refresh adapters</button>
</div>
<div class="form-group">
<label for="bridge-wifi-ssid">Bridge SSID</label>
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;align-items:flex-end;">
<select id="bridge-wifi-ssid" style="flex:1;min-width:12rem;">
<option value="">— scan or type below —</option>
</select>
<button type="button" class="btn btn-secondary btn-small" id="bridge-wifi-scan-btn">Scan</button>
</div>
<input type="text" id="bridge-wifi-ssid-manual" placeholder="Or type SSID" autocomplete="off" style="margin-top:0.5rem;">
</div>
<div class="form-group">
<label for="bridge-wifi-password">Password</label>
<input type="password" id="bridge-wifi-password" autocomplete="off">
</div>
<div class="form-group">
<label for="bridge-wifi-label">Profile label</label>
<input type="text" id="bridge-wifi-label" placeholder="e.g. Garden bridge" autocomplete="off">
</div>
<div class="form-group" style="display:flex;gap:1rem;flex-wrap:wrap;">
<div style="flex:1;min-width:8rem;">
<label for="bridge-wifi-ap-ip">Bridge IP</label>
<input type="text" id="bridge-wifi-ap-ip" value="192.168.4.1" autocomplete="off">
</div>
<div style="flex:0 0 6rem;">
<label for="bridge-wifi-ws-port">WS port</label>
<input type="number" id="bridge-wifi-ws-port" value="80" min="1" max="65535" step="1">
</div>
</div>
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;margin-bottom:1rem;">
<button type="button" class="btn btn-primary btn-small" id="bridge-wifi-connect-btn">Connect WiFi</button>
<button type="button" class="btn btn-secondary btn-small" id="bridge-wifi-save-profile-btn">Save WiFi profile</button>
</div>
<h3 class="settings-subheading">Saved profiles</h3>
<ul id="bridge-profiles-list" class="settings-bridge-profiles muted-text"></ul>
</div>
</div>
<div id="settings-panel-led-tool" class="settings-tab-panel" data-settings-panel="led-tool" role="tabpanel" aria-labelledby="settings-tab-led-tool" hidden>
<p class="muted-text settings-led-tool-intro">USB serial setup for drivers and bridges: device settings, deploy, and firmware.</p>
<iframe id="led-tool-iframe" title="LED device settings editor" src="about:blank" allow="serial" class="settings-led-tool-iframe"></iframe>
</div>
</div>
</div>
<!-- Styles moved to /static/style.css -->
<script src="/static/zone-devices-panel.js"></script>
<script src="/static/groups.js"></script>
<script src="/static/zones.js"></script>
<script src="/static/help.js"></script>
<script src="/static/color_palette.js"></script>
<script src="/static/bundle_io.js"></script>
<script src="/static/profiles.js"></script>
<script src="/static/zone_palette.js"></script>
<script src="/static/patterns.js"></script>
<script src="/static/presets.js"></script>
<script src="/static/sequences.js"></script>
<script src="/static/devices.js"></script>
<script src="/static/audio.js"></script>
<script src="/static/numpad.js"></script>
</body>
</html>