Files
ledgrab/server/src/wled_controller/templates/modals/css-editor.html
T
alexei.dolgolyov bbd2ac9910 Add audio-reactive color strip sources, improve delete error messages
Add new "audio" color strip source type with three visualization modes
(spectrum analyzer, beat pulse, VU meter) supporting WASAPI loopback and
microphone input via PyAudioWPatch. Includes shared audio capture with
ref counting, real-time FFT spectrum analysis, and beat detection.

Improve all referential integrity 409 error messages across delete
endpoints to include specific names of referencing entities instead of
generic "one or more" messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:56:54 +03:00

471 lines
37 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.
<!-- Color Strip Source Editor Modal -->
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="css-editor-title" data-i18n="color_strip.add">🎞️ Add Color Strip Source</h2>
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="css-editor-form">
<input type="hidden" id="css-editor-id">
<div class="form-group">
<label for="css-editor-name" data-i18n="color_strip.name">Name:</label>
<input type="text" id="css-editor-name" data-i18n-placeholder="color_strip.name.placeholder" placeholder="Wall Strip" required>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-type" data-i18n="color_strip.type">Type:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.type.hint">Picture Source derives colors from a screen capture. Static Color fills all LEDs with one constant color. Gradient distributes a color gradient across all LEDs.</small>
<select id="css-editor-type" onchange="onCSSTypeChange()">
<option value="picture" data-i18n="color_strip.type.picture">Picture Source</option>
<option value="static" data-i18n="color_strip.type.static">Static Color</option>
<option value="gradient" data-i18n="color_strip.type.gradient">Gradient</option>
<option value="color_cycle" data-i18n="color_strip.type.color_cycle">Color Cycle</option>
<option value="effect" data-i18n="color_strip.type.effect">Procedural Effect</option>
<option value="composite" data-i18n="color_strip.type.composite">Composite</option>
<option value="audio" data-i18n="color_strip.type.audio">Audio Reactive</option>
</select>
</div>
<!-- Picture-source-specific fields -->
<div id="css-editor-picture-section">
<div class="form-group">
<div class="label-row">
<label for="css-editor-picture-source" data-i18n="color_strip.picture_source">Picture Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.picture_source.hint">Which screen capture source to use as input for LED color calculation</small>
<select id="css-editor-picture-source"></select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-interpolation" data-i18n="color_strip.interpolation">Color Mode:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.interpolation.hint">How to calculate LED color from sampled border pixels</small>
<select id="css-editor-interpolation">
<option value="average" data-i18n="color_strip.interpolation.average">Average</option>
<option value="median" data-i18n="color_strip.interpolation.median">Median</option>
<option value="dominant" data-i18n="color_strip.interpolation.dominant">Dominant</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-smoothing">
<span data-i18n="color_strip.smoothing">Smoothing:</span>
<span id="css-editor-smoothing-value">0.30</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.smoothing.hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-frame-interpolation" data-i18n="color_strip.frame_interpolation">Frame Interpolation:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.frame_interpolation.hint">Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-frame-interpolation">
<span class="settings-toggle-slider"></span>
</label>
</div>
<details class="form-collapse">
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-brightness">
<span data-i18n="color_strip.brightness">Brightness:</span>
<span id="css-editor-brightness-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-saturation">
<span data-i18n="color_strip.saturation">Saturation:</span>
<span id="css-editor-saturation-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-gamma">
<span data-i18n="color_strip.gamma">Gamma:</span>
<span id="css-editor-gamma-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, &lt;1=brighter midtones, &gt;1=darker midtones)</small>
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
</div>
</details>
</div>
<!-- Static-color-specific fields -->
<div id="css-editor-static-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-color" data-i18n="color_strip.static_color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.static_color.hint">The solid color that will be sent to all LEDs on the strip.</small>
<input type="color" id="css-editor-color" value="#ffffff">
</div>
</div>
<!-- Color-cycle-specific fields -->
<div id="css-editor-color-cycle-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.color_cycle.colors">Colors:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
<div id="color-cycle-colors-list"></div>
<button type="button" class="btn btn-secondary" onclick="colorCycleAddColor()" data-i18n="color_strip.color_cycle.add_color">+ Add Color</button>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-cycle-speed">
<span data-i18n="color_strip.color_cycle.speed">Speed:</span>
<span id="css-editor-cycle-speed-val">1.0</span>×
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.speed.hint">Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds.</small>
<input type="range" id="css-editor-cycle-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-cycle-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
</div>
<!-- Gradient-specific fields -->
<div id="css-editor-gradient-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.preset">Preset:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small>
<select id="css-editor-gradient-preset" onchange="applyGradientPreset(this.value)">
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
<option value="ocean" data-i18n="color_strip.gradient.preset.ocean">Ocean</option>
<option value="forest" data-i18n="color_strip.gradient.preset.forest">Forest</option>
<option value="fire" data-i18n="color_strip.gradient.preset.fire">Fire</option>
<option value="lava" data-i18n="color_strip.gradient.preset.lava">Lava</option>
<option value="aurora" data-i18n="color_strip.gradient.preset.aurora">Aurora</option>
<option value="ice" data-i18n="color_strip.gradient.preset.ice">Ice</option>
<option value="warm" data-i18n="color_strip.gradient.preset.warm">Warm</option>
<option value="cool" data-i18n="color_strip.gradient.preset.cool">Cool</option>
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.preview">Gradient:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preview.hint">Visual preview. Click the marker track below to add a stop. Drag markers to reposition.</small>
<div class="gradient-editor">
<canvas id="gradient-canvas" height="44"></canvas>
<div id="gradient-markers-track" class="gradient-markers-track"></div>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.stops">Color Stops:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.stops.hint">Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.</small>
<div id="gradient-stops-list"></div>
</div>
</div>
<!-- Procedural effect fields -->
<div id="css-editor-effect-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-effect-type" data-i18n="color_strip.effect.type">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.type.hint">The procedural effect algorithm to use.</small>
<select id="css-editor-effect-type" onchange="onEffectTypeChange()">
<option value="fire" data-i18n="color_strip.effect.fire">Fire</option>
<option value="meteor" data-i18n="color_strip.effect.meteor">Meteor</option>
<option value="plasma" data-i18n="color_strip.effect.plasma">Plasma</option>
<option value="noise" data-i18n="color_strip.effect.noise">Perlin Noise</option>
<option value="aurora" data-i18n="color_strip.effect.aurora">Aurora</option>
</select>
<small id="css-editor-effect-type-desc" class="field-desc"></small>
<div id="css-editor-effect-preview" class="effect-palette-preview"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-effect-speed">
<span data-i18n="color_strip.effect.speed">Speed:</span>
<span id="css-editor-effect-speed-val">1.0</span>x
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.speed.hint">How fast the effect animates. 1.0 = default speed.</small>
<input type="range" id="css-editor-effect-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-palette" data-i18n="color_strip.effect.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.palette.hint">Color palette used by the effect.</small>
<select id="css-editor-effect-palette" onchange="updateEffectPreview()">
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
</select>
</div>
<div id="css-editor-effect-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-color" data-i18n="color_strip.effect.color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.color.hint">Head color for the meteor effect.</small>
<input type="color" id="css-editor-effect-color" value="#ff5000" oninput="updateEffectPreview()">
</div>
<div id="css-editor-effect-intensity-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-intensity">
<span data-i18n="color_strip.effect.intensity">Intensity:</span>
<span id="css-editor-effect-intensity-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.intensity.hint">Effect-specific intensity.</small>
<input type="range" id="css-editor-effect-intensity" min="0.1" max="2.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-scale-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-scale">
<span data-i18n="color_strip.effect.scale">Scale:</span>
<span id="css-editor-effect-scale-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.scale.hint">Spatial zoom level.</small>
<input type="range" id="css-editor-effect-scale" min="0.5" max="5.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-mirror" data-i18n="color_strip.effect.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.mirror.hint">Bounce back and forth instead of wrapping around.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-effect-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- Composite-specific fields -->
<div id="css-editor-composite-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.composite.layers">Layers:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.composite.layers.hint">Stack multiple color strip sources. First layer is the bottom, last is the top.</small>
<div id="composite-layers-list"></div>
<button type="button" class="btn btn-secondary" onclick="compositeAddLayer()" data-i18n="color_strip.composite.add_layer">+ Add Layer</button>
</div>
</div>
<!-- Audio-reactive fields -->
<div id="css-editor-audio-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-viz" data-i18n="color_strip.audio.visualization">Visualization:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.visualization.hint">How audio data is rendered to LEDs.</small>
<select id="css-editor-audio-viz" onchange="onAudioVizChange()">
<option value="spectrum" data-i18n="color_strip.audio.viz.spectrum">Spectrum Analyzer</option>
<option value="beat_pulse" data-i18n="color_strip.audio.viz.beat_pulse">Beat Pulse</option>
<option value="vu_meter" data-i18n="color_strip.audio.viz.vu_meter">VU Meter</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-device" data-i18n="color_strip.audio.device">Audio Device:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.device.hint">Audio input source. Loopback devices capture system audio output; input devices capture microphone or line-in.</small>
<select id="css-editor-audio-device">
<!-- populated dynamically from /api/v1/audio-devices -->
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-sensitivity">
<span data-i18n="color_strip.audio.sensitivity">Sensitivity:</span>
<span id="css-editor-audio-sensitivity-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.sensitivity.hint">Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.</small>
<input type="range" id="css-editor-audio-sensitivity" min="0.1" max="5.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-audio-sensitivity-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-smoothing">
<span data-i18n="color_strip.audio.smoothing">Smoothing:</span>
<span id="css-editor-audio-smoothing-val">0.30</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.smoothing.hint">Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.</small>
<input type="range" id="css-editor-audio-smoothing" min="0.0" max="1.0" step="0.05" value="0.3"
oninput="document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div id="css-editor-audio-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-audio-palette" data-i18n="color_strip.audio.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.palette.hint">Color palette used for spectrum bars or beat pulse coloring.</small>
<select id="css-editor-audio-palette">
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
</select>
</div>
<div id="css-editor-audio-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color" data-i18n="color_strip.audio.color">Base Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color.hint">Low-level color for VU meter bar.</small>
<input type="color" id="css-editor-audio-color" value="#00ff00">
</div>
<div id="css-editor-audio-color-peak-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color-peak" data-i18n="color_strip.audio.color_peak">Peak Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color_peak.hint">High-level color at the top of the VU meter bar.</small>
<input type="color" id="css-editor-audio-color-peak" value="#ff0000">
</div>
<div id="css-editor-audio-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-mirror" data-i18n="color_strip.audio.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.mirror.hint">Mirror spectrum from center outward: bass in the middle, treble at the edges.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-audio-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- Shared LED count field -->
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to auto-detect from calibration or device.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<!-- Animation — shown for static/gradient, hidden for picture -->
<div id="css-editor-animation-section" style="display:none">
<details class="form-collapse">
<summary><span data-i18n="color_strip.animation">Animation</span></summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-animation-type" data-i18n="color_strip.animation.type">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.type.hint">The animation effect to apply. Available effects depend on source type. Select None to disable animation.</small>
<select id="css-editor-animation-type" onchange="onAnimationTypeChange()">
<!-- populated by onCSSTypeChange() -->
</select>
<small id="css-editor-animation-type-desc" class="field-desc"></small>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-animation-speed">
<span data-i18n="color_strip.animation.speed">Speed:</span>
<span id="css-editor-animation-speed-val">1.0</span>×
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.speed.hint">Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.</small>
<input type="range" id="css-editor-animation-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-animation-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
</div>
</details>
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeCSSEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveCSSEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>