/**
 * Color Strip Sources — CRUD, editor orchestration, type switching.
 * Sub-modules handle type-specific logic: audio, cards, composite,
 * game-event, gradient, mapped, math-wave, notification, test.
 */

import { fetchWithAuth, escapeHtml } from '../../core/api.ts';
import {
    _cachedSyncClocks, _cachedCSPTemplates, _cachedWeatherSources, _cachedValueSources,
    streamsCache, colorStripSourcesCache, valueSourcesCache, csptCache, gradientsCache,
    weatherSourcesCache, GradientEntity,
} from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import { showToast, showConfirm, desktopFocus } from '../../core/ui.ts';
import { Modal } from '../../core/modal.ts';
import {
    getColorStripIcon, getPictureSourceIcon,
    ICON_CLONE, ICON_PALETTE, ICON_CLOCK, ICON_TRASH,
} from '../../core/icons.ts';
import * as P from '../../core/icon-paths.ts';
import type { ColorStripSource } from '../../types.ts';
import { TagInput } from '../../core/tag-input.ts';
import { IconSelect, showTypePicker, type IconSelectItem } from '../../core/icon-select.ts';
import { EntitySelect } from '../../core/entity-palette.ts';
import { BindableScalarWidget } from '../../core/bindable-scalar.ts';
import { BindableColorWidget } from '../../core/bindable-color.ts';
import { getBaseOrigin } from '../settings.ts';
import {
    rgbArrayToHex, hexToRgbArray,
    gradientInit, gradientRenderAll, gradientAddStop,
    getGradientStops, gradientSetIdPrefix,
} from '../css-gradient-editor.ts';

// ── Sub-module imports ──────────────────────────────────────────
import {
    compositeDestroyEntitySelects, compositeSetAvailableSources,
    compositeRenderList, compositeAddLayer, compositeRemoveLayer, compositeGetLayers,
    loadCompositeState, compositeGetRawLayers,
} from './composite.ts';
import {
    ensureNotificationEffectIconSelect, ensureNotificationFilterModeIconSelect,
    onNotificationFilterModeChange,
    notificationAddAppOverride, notificationRemoveAppOverride,
    notificationGetAppColorsDict, notificationGetAppSoundsDict, notificationGetRawAppOverrides,
    testNotification, showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
    loadNotificationState, resetNotificationState,
    destroyNotificationDurationWidget, getNotificationDurationValue, getNotificationDurationSnapshot,
    getNotificationDefaultColorValue, getNotificationDefaultColorSnapshot,
    getNotificationVolumeValue, getNotificationVolumeSnapshot,
} from './notification.ts';
import {
    destroyGameEventWidgets, ensureGameEventIdleColorWidget, populateGameIntegrationDropdownCSS,
    initCSSGamePresetIconSelect, addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange,
    renderCSSGameMappings, collectCSSGameMappings, getCSSGameMappings,
} from './game-event.ts';
import {
    destroyMathWaveWidgets, ensureMathWaveSpeedWidget, ensureMathWaveGradientEntitySelect,
    refreshMathWaveGradientEntitySelect, getMathWaveGradientEntitySelect,
    mathWaveRenderLayers, mathWaveGetLayers, mathWaveAddLayer, mathWaveRemoveLayer,
} from './math-wave.ts';
import {
    mappedSetAvailableSources, mappedRenderList, mappedAddZone, mappedRemoveZone,
    mappedGetZones, loadMappedState, resetMappedState, getMappedZones,
} from './mapped.ts';
import {
    destroyAudioWidgets, ensureAudioSensitivityWidget, ensureAudioSmoothingWidget,
    ensureAudioBeatDecayWidget, ensureAudioColorWidget, ensureAudioColorPeakWidget,
    ensureAudioVizIconSelect, ensureAudioPaletteEntitySelect, refreshAudioPaletteEntitySelect,
    getAudioVizIconSelect, getAudioPaletteEntitySelect,
    loadAudioSources, loadAudioState, resetAudioState,
} from './audio.ts';
import { createColorStripCard } from './cards.ts';
import {
    buildGradientEntityItems, syncSelectOptions,
    getGradients, findGradient, gradientEntityStripHTML,
    renderCustomPresetList,
    onGradientPresetChange, promptAndSaveGradientPreset, deleteAndRefreshGradientPreset,
    showGradientModal, closeGradientEditor, saveGradientEntity,
    cloneGradient, editGradient, deleteGradient,
} from './gradient.ts';

// ── Re-exports for app.ts window global bindings ────────────────
export { gradientInit, gradientRenderAll, gradientAddStop };
export { compositeAddLayer, compositeRemoveLayer };
export {
    onNotificationFilterModeChange,
    notificationAddAppOverride, notificationRemoveAppOverride,
    testNotification, showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
};
export { _getAnimationPayload };
export { addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange };
export { mathWaveAddLayer, mathWaveRemoveLayer };
export { mappedAddZone, mappedRemoveZone };
export { createColorStripCard };
export {
    onGradientPresetChange, promptAndSaveGradientPreset, deleteAndRefreshGradientPreset,
    showGradientModal, closeGradientEditor, saveGradientEntity,
    cloneGradient, editGradient, deleteGradient,
    renderCustomPresetList,
};
export {
    previewCSSFromEditor,
    testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings,
    fireCssTestNotification, fireCssTestNotificationLayer,
} from './test.ts';

// ── Icons helper ────────────────────────────────────────────────
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;

// ══════════════════════════════════════════════════════════════════
// CSS Editor Modal
// ══════════════════════════════════════════════════════════════════

class CSSEditorModal extends Modal {
    constructor() {
        super('css-editor-modal');
    }

    onForceClose() {
        if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; }
        if (_smoothingWidget) { _smoothingWidget.destroy(); _smoothingWidget = null; }
        if (_effectIntensityWidget) { _effectIntensityWidget.destroy(); _effectIntensityWidget = null; }
        if (_effectScaleWidget) { _effectScaleWidget.destroy(); _effectScaleWidget = null; }
        if (_apiInputTimeoutWidget) { _apiInputTimeoutWidget.destroy(); _apiInputTimeoutWidget = null; }
        if (_candlelightIntensityWidget) { _candlelightIntensityWidget.destroy(); _candlelightIntensityWidget = null; }
        if (_candlelightSpeedWidget) { _candlelightSpeedWidget.destroy(); _candlelightSpeedWidget = null; }
        if (_candlelightWindWidget) { _candlelightWindWidget.destroy(); _candlelightWindWidget = null; }
        if (_weatherSpeedWidget) { _weatherSpeedWidget.destroy(); _weatherSpeedWidget = null; }
        if (_weatherTempInfluenceWidget) { _weatherTempInfluenceWidget.destroy(); _weatherTempInfluenceWidget = null; }
        if (_singleColorWidget) { _singleColorWidget.destroy(); _singleColorWidget = null; }
        if (_effectColorWidget) { _effectColorWidget.destroy(); _effectColorWidget = null; }
        if (_apiInputFallbackColorWidget) { _apiInputFallbackColorWidget.destroy(); _apiInputFallbackColorWidget = null; }
        if (_candlelightColorWidget) { _candlelightColorWidget.destroy(); _candlelightColorWidget = null; }
        destroyAudioWidgets();
        destroyNotificationDurationWidget();
        if (_kcPictureSourceEntitySelect) { _kcPictureSourceEntitySelect.destroy(); _kcPictureSourceEntitySelect = null; }
        if (_kcInterpolationIconSelect) { _kcInterpolationIconSelect.destroy(); _kcInterpolationIconSelect = null; }
        if (_kcSmoothingWidget) { _kcSmoothingWidget.destroy(); _kcSmoothingWidget = null; }
        if (_kcBrightnessWidget) { _kcBrightnessWidget.destroy(); _kcBrightnessWidget = null; }
        destroyGameEventWidgets();
        destroyMathWaveWidgets();
        compositeDestroyEntitySelects();
    }

    snapshotValues() {
        const type = (document.getElementById('css-editor-type') as HTMLInputElement).value;
        return {
            name: (document.getElementById('css-editor-name') as HTMLInputElement).value,
            type,
            picture_source: (document.getElementById('css-editor-picture-source') as HTMLInputElement).value,
            interpolation: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value,
            smoothing: _smoothingWidget ? JSON.stringify(_smoothingWidget.getValue()) : '0.3',
            color: _singleColorWidget ? JSON.stringify(_singleColorWidget.getValue()) : '[]',
            led_count: (document.getElementById('css-editor-led-count') as HTMLInputElement).value,
            gradient_stops: type === 'gradient' ? JSON.stringify(getGradientStops()) : '[]',
            animation_type: (document.getElementById('css-editor-animation-type') as HTMLInputElement).value,
            effect_type: (document.getElementById('css-editor-effect-type') as HTMLInputElement).value,
            effect_palette: (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value,
            effect_color: _effectColorWidget ? JSON.stringify(_effectColorWidget.getValue()) : '[]',
            effect_intensity: _effectIntensityWidget ? JSON.stringify(_effectIntensityWidget.getValue()) : '1.0',
            effect_scale: _effectScaleWidget ? JSON.stringify(_effectScaleWidget.getValue()) : '1.0',
            effect_mirror: (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked,
            composite_layers: JSON.stringify(compositeGetRawLayers()),
            mapped_zones: JSON.stringify(getMappedZones()),
            audio_viz: (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value,
            audio_source: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value,
            audio_sensitivity: JSON.stringify(ensureAudioSensitivityWidget().getValue()),
            audio_smoothing: JSON.stringify(ensureAudioSmoothingWidget().getValue()),
            audio_beat_decay: JSON.stringify(ensureAudioBeatDecayWidget().getValue()),
            audio_palette: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value,
            audio_color: JSON.stringify(ensureAudioColorWidget().getValue()),
            audio_color_peak: JSON.stringify(ensureAudioColorPeakWidget().getValue()),
            audio_mirror: (document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked,
            api_input_fallback_color: _apiInputFallbackColorWidget ? JSON.stringify(_apiInputFallbackColorWidget.getValue()) : '[]',
            api_input_timeout: _apiInputTimeoutWidget ? JSON.stringify(_apiInputTimeoutWidget.getValue()) : '5.0',
            api_input_interpolation: (document.getElementById('css-editor-api-input-interpolation') as HTMLInputElement).value,
            notification_os_listener: (document.getElementById('css-editor-notification-os-listener') as HTMLInputElement).checked,
            notification_effect: (document.getElementById('css-editor-notification-effect') as HTMLInputElement).value,
            notification_duration: getNotificationDurationSnapshot(),
            notification_default_color: getNotificationDefaultColorSnapshot(),
            notification_filter_mode: (document.getElementById('css-editor-notification-filter-mode') as HTMLInputElement).value,
            notification_filter_list: (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value,
            notification_app_overrides: JSON.stringify(notificationGetRawAppOverrides()),
            clock_id: (document.getElementById('css-editor-clock') as HTMLInputElement).value,
            daylight_speed: (document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value,
            daylight_use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked,
            daylight_latitude: (document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value,
            daylight_longitude: (document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value,
            candlelight_color: _candlelightColorWidget ? JSON.stringify(_candlelightColorWidget.getValue()) : '[]',
            candlelight_intensity: _candlelightIntensityWidget ? JSON.stringify(_candlelightIntensityWidget.getValue()) : '1.0',
            candlelight_num_candles: (document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value,
            candlelight_speed: _candlelightSpeedWidget ? JSON.stringify(_candlelightSpeedWidget.getValue()) : '1.0',
            processed_input: (document.getElementById('css-editor-processed-input') as HTMLInputElement).value,
            processed_template: (document.getElementById('css-editor-processed-template') as HTMLInputElement).value,
            kc_smoothing: _kcSmoothingWidget ? JSON.stringify(_kcSmoothingWidget.getValue()) : '0.3',
            kc_brightness: _kcBrightnessWidget ? JSON.stringify(_kcBrightnessWidget.getValue()) : '1.0',
            kc_rects: JSON.stringify(_kcEditorRects),
            ge_integration: (document.getElementById('css-editor-game-integration') as HTMLInputElement)?.value || '',
            ge_idle_color: JSON.stringify(ensureGameEventIdleColorWidget().getValue()),
            ge_mappings: JSON.stringify(getCSSGameMappings()),
            mw_gradient: (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement)?.value || '',
            mw_speed: JSON.stringify(ensureMathWaveSpeedWidget().getValue()),
            mw_waves: JSON.stringify(mathWaveGetLayers()),
            tags: JSON.stringify(_cssTagsInput ? _cssTagsInput.getValue() : []),
        };
    }
}

const cssEditorModal = new CSSEditorModal();

// ══════════════════════════════════════════════════════════════════
// Shared widget state (types that remain in index.ts)
// ══════════════════════════════════════════════════════════════════

let _cssTagsInput: any = null;
let _smoothingWidget: BindableScalarWidget | null = null;
let _effectIntensityWidget: BindableScalarWidget | null = null;
let _effectScaleWidget: BindableScalarWidget | null = null;
let _apiInputTimeoutWidget: BindableScalarWidget | null = null;
let _candlelightIntensityWidget: BindableScalarWidget | null = null;
let _candlelightSpeedWidget: BindableScalarWidget | null = null;
let _candlelightWindWidget: BindableScalarWidget | null = null;
let _weatherSpeedWidget: BindableScalarWidget | null = null;
let _weatherTempInfluenceWidget: BindableScalarWidget | null = null;

let _singleColorWidget: BindableColorWidget | null = null;
let _effectColorWidget: BindableColorWidget | null = null;
let _apiInputFallbackColorWidget: BindableColorWidget | null = null;
let _candlelightColorWidget: BindableColorWidget | null = null;

let _cssPictureSourceEntitySelect: any = null;
let _cssClockEntitySelect: any = null;
let _processedInputEntitySelect: any = null;
let _processedTemplateEntitySelect: any = null;
let _kcPictureSourceEntitySelect: any = null;
let _kcInterpolationIconSelect: any = null;
let _kcSmoothingWidget: BindableScalarWidget | null = null;
let _kcBrightnessWidget: BindableScalarWidget | null = null;

// ── Key Colors rectangle editor state ──
let _kcEditorRects: Array<{ name: string; x: number; y: number; width: number; height: number }> = [];

function _renderKCRectSummary(): void {
    const el = document.getElementById('css-editor-kc-rect-summary');
    if (!el) return;
    if (_kcEditorRects.length === 0) {
        el.textContent = t('color_strip.key_colors.no_rects');
    } else {
        const names = _kcEditorRects.map(r => r.name).join(', ');
        el.textContent = `${_kcEditorRects.length} region${_kcEditorRects.length !== 1 ? 's' : ''}: ${names}`;
    }
}

function _openKCRegionEditor(): void {
    const { showPatternTemplateEditor } = window as any;
    if (!showPatternTemplateEditor) return;
    showPatternTemplateEditor(null, null, {
        rects: _kcEditorRects.map(r => ({ ...r })),
        onSave: (rects: any[]) => {
            _kcEditorRects = rects;
            _renderKCRectSummary();
        },
    });
}

window._openKCRegionEditor = _openKCRegionEditor;

async function configureKCRegions(sourceId: string): Promise<void> {
    try {
        const resp = await fetchWithAuth(`/color-strip-sources/${sourceId}`);
        if (!resp.ok) throw new Error('Failed to load source');
        const source = await resp.json();
        const rects = source.rectangles || [];
        const { showPatternTemplateEditor } = window as any;
        if (!showPatternTemplateEditor) return;
        showPatternTemplateEditor(null, null, {
            rects: rects.map((r: any) => ({ ...r })),
            pictureSourceId: source.picture_source_id || '',
            onSave: async (newRects: any[]) => {
                try {
                    const putResp = await fetchWithAuth(`/color-strip-sources/${sourceId}`, {
                        method: 'PUT',
                        body: JSON.stringify({ rectangles: newRects }),
                    });
                    if (!putResp.ok) throw new Error('Failed to save');
                    showToast(t('color_strip.updated'), 'success');
                    colorStripSourcesCache.invalidate();
                    if (window.loadPictureSources) await window.loadPictureSources();
                } catch (e: any) {
                    showToast(e.message, 'error');
                }
            },
        });
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}
window.configureKCRegions = configureKCRegions;

// ══════════════════════════════════════════════════════════════════
// Type selector
// ══════════════════════════════════════════════════════════════════

const CSS_TYPE_KEYS = [
    'picture', 'picture_advanced', 'single_color', 'gradient',
    'effect', 'composite', 'mapped', 'audio',
    'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors',
    'game_event', 'math_wave',
];

function _buildCSSTypeItems() {
    return CSS_TYPE_KEYS.map(key => ({
        value: key,
        icon: getColorStripIcon(key),
        label: t(`color_strip.type.${key}`),
        desc: t(`color_strip.type.${key}.desc`),
    }));
}

let _cssTypeIconSelect: any = null;

function _ensureCSSTypeIconSelect() {
    const sel = document.getElementById('css-editor-type') as HTMLSelectElement;
    if (!sel) return;
    if (_cssTypeIconSelect) {
        _cssTypeIconSelect.updateItems(_buildCSSTypeItems());
        return;
    }
    _cssTypeIconSelect = new IconSelect({
        target: sel,
        items: _buildCSSTypeItems(),
        columns: 2,
    });
}

// ══════════════════════════════════════════════════════════════════
// Type switching
// ══════════════════════════════════════════════════════════════════

const CSS_SECTION_MAP: Record<string, string> = {
    'picture': 'css-editor-picture-section',
    'picture_advanced': 'css-editor-picture-section',
    'single_color': 'css-editor-single-color-section',
    'gradient': 'css-editor-gradient-section',
    'effect': 'css-editor-effect-section',
    'composite': 'css-editor-composite-section',
    'mapped': 'css-editor-mapped-section',
    'audio': 'css-editor-audio-section',
    'api_input': 'css-editor-api-input-section',
    'notification': 'css-editor-notification-section',
    'daylight': 'css-editor-daylight-section',
    'candlelight': 'css-editor-candlelight-section',
    'weather': 'css-editor-weather-section',
    'processed': 'css-editor-processed-section',
    'key_colors': 'css-editor-key-colors-section',
    'game_event': 'css-editor-game-event-section',
    'math_wave': 'css-editor-math-wave-section',
};

const CSS_ALL_SECTION_IDS = [...new Set(Object.values(CSS_SECTION_MAP))];

const CSS_TYPE_SETUP: Record<string, () => void> = {
    processed: () => _populateProcessedSelectors(),
    effect: () => { _ensureEffectTypeIconSelect(); _ensureEffectPaletteEntitySelect(); onEffectTypeChange(); },
    audio: () => { ensureAudioVizIconSelect(); _ensureAudioPaletteEntitySelectProxy(); onAudioVizChange(); loadAudioSources(); },
    gradient: () => { _ensureGradientPresetEntitySelect(); _ensureGradientEasingIconSelect(); requestAnimationFrame(() => gradientRenderAll()); },
    notification: () => { ensureNotificationEffectIconSelect(); ensureNotificationFilterModeIconSelect(); },
    candlelight: () => _ensureCandleTypeIconSelect(),
    game_event: () => { populateGameIntegrationDropdownCSS(); initCSSGamePresetIconSelect(); },
    math_wave: () => { _ensureMathWaveGradientEntitySelectProxy(); },
    weather: () => { weatherSourcesCache.fetch().then(() => _populateWeatherSourceDropdown()); },
    composite: () => compositeRenderList(),
    mapped: () => mappedRenderList(),
};

export function onCSSTypeChange() {
    const type = (document.getElementById('css-editor-type') as HTMLInputElement).value;
    if (_cssTypeIconSelect) _cssTypeIconSelect.setValue(type);

    CSS_ALL_SECTION_IDS.forEach(id => {
        const el = document.getElementById(id);
        if (el) el.style.display = 'none';
    });
    const activeSection = CSS_SECTION_MAP[type];
    if (activeSection) {
        const el = document.getElementById(activeSection);
        if (el) el.style.display = '';
    }

    const psGroup = document.getElementById('css-editor-picture-source-group') as HTMLElement | null;
    if (psGroup) psGroup.style.display = (type === 'picture') ? '' : 'none';

    const isPictureType = type === 'picture' || type === 'picture_advanced';
    if (isPictureType) _ensureInterpolationIconSelect();

    const setupFn = CSS_TYPE_SETUP[type];
    if (setupFn) setupFn();

    const animSection = document.getElementById('css-editor-animation-section') as HTMLElement;
    const animTypeSelect = document.getElementById('css-editor-animation-type') as HTMLSelectElement;
    if (type === 'single_color' || type === 'gradient') {
        animSection.style.display = '';
        const opts = type === 'gradient'
            ? ['none','breathing','gradient_shift','wave','noise_perturb','hue_rotate','strobe','sparkle','pulse','candle','rainbow_fade']
            : ['none','breathing','strobe','sparkle','pulse','candle','rainbow_fade'];
        animTypeSelect.innerHTML = opts.map(v =>
            `<option value="${v}">${t('color_strip.animation.type.' + v)}</option>`
        ).join('');
        _ensureAnimationTypeIconSelect(type);
    } else {
        animSection.style.display = 'none';
    }
    _syncAnimationSpeedState();

    const hasLedCount = ['picture', 'picture_advanced'];
    (document.getElementById('css-editor-led-count-group') as HTMLElement).style.display =
        hasLedCount.includes(type) ? '' : 'none';

    const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
    (document.getElementById('css-editor-clock-group') as HTMLElement).style.display = clockTypes.includes(type) ? '' : 'none';
    if (clockTypes.includes(type)) _populateClockDropdown();

    _autoGenerateCSSName();
}

// ══════════════════════════════════════════════════════════════════
// Shared dropdown / widget helpers
// ══════════════════════════════════════════════════════════════════

function _populateClockDropdown(selectedId?: any) {
    const sel = document.getElementById('css-editor-clock') as HTMLSelectElement;
    const prev = selectedId !== undefined ? selectedId : sel.value;
    sel.innerHTML = `<option value="">${t('common.none_own_speed')}</option>` +
        _cachedSyncClocks.map(c => `<option value="${c.id}">${escapeHtml(c.name)} (${c.speed}x)</option>`).join('');
    sel.value = prev || '';

    if (_cssClockEntitySelect) _cssClockEntitySelect.destroy();
    _cssClockEntitySelect = new EntitySelect({
        target: sel,
        getItems: () => _cachedSyncClocks.map(c => ({
            value: c.id,
            label: c.name,
            icon: ICON_CLOCK,
            desc: `${c.speed}x`,
        })),
        placeholder: t('palette.search'),
        allowNone: true,
        noneLabel: t('common.none_own_speed'),
    });
}

export function onCSSClockChange() {
    // No-op: speed sliders removed; speed is now clock-only
}

let _weatherSourceEntitySelect: any = null;

function _populateWeatherSourceDropdown() {
    const sources = _cachedWeatherSources || [];
    const sel = document.getElementById('css-editor-weather-source') as HTMLSelectElement;
    const prev = sel.value;
    sel.innerHTML = `<option value="">\u2014</option>` +
        sources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
    sel.value = prev || '';
    if (_weatherSourceEntitySelect) _weatherSourceEntitySelect.destroy();
    if (sources.length > 0) {
        _weatherSourceEntitySelect = new EntitySelect({
            target: sel,
            getItems: () => (_cachedWeatherSources || []).map(s => ({
                value: s.id,
                label: s.name,
                icon: getColorStripIcon('weather'),
            })),
            placeholder: t('palette.search'),
        });
    }
}

function _populateProcessedSelectors() {
    const editingId = (document.getElementById('css-editor-id') as HTMLInputElement).value;
    const allSources = (colorStripSourcesCache.data || []) as any[];
    const inputSources = allSources.filter(s => s.id !== editingId && s.source_type !== 'processed');
    const inputSel = document.getElementById('css-editor-processed-input') as HTMLSelectElement;
    const prevInput = inputSel.value;
    inputSel.innerHTML = `<option value="">${t('common.none_no_input')}</option>` +
        inputSources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
    inputSel.value = prevInput || '';
    if (_processedInputEntitySelect) _processedInputEntitySelect.destroy();
    if (inputSources.length > 0) {
        _processedInputEntitySelect = new EntitySelect({
            target: inputSel,
            getItems: () => ((colorStripSourcesCache.data || []) as any[])
                .filter(s => s.id !== editingId && s.source_type !== 'processed')
                .map(s => ({ value: s.id, label: s.name, icon: getColorStripIcon(s.source_type) })),
            placeholder: t('palette.search'),
        });
    }

    const templates = (csptCache.data || []) as any[];
    const tplSel = document.getElementById('css-editor-processed-template') as HTMLSelectElement;
    const prevTpl = tplSel.value;
    tplSel.innerHTML = `<option value="">${t('common.none_no_cspt')}</option>` +
        templates.map(tp => `<option value="${tp.id}">${escapeHtml(tp.name)}</option>`).join('');
    tplSel.value = prevTpl || '';
    if (_processedTemplateEntitySelect) _processedTemplateEntitySelect.destroy();
    if (templates.length > 0) {
        _processedTemplateEntitySelect = new EntitySelect({
            target: tplSel,
            getItems: () => ((csptCache.data || []) as any[]).map(tp => ({
                value: tp.id, label: tp.name, icon: `<svg class="icon" viewBox="0 0 24 24">${P.sparkles}</svg>`,
            })),
            placeholder: t('palette.search'),
        });
    }
}

// ── Animation helpers ───────────────────────────────────────────

function _getAnimationPayload() {
    const type = (document.getElementById('css-editor-animation-type') as HTMLInputElement).value;
    return {
        enabled: type !== 'none',
        type: type !== 'none' ? type : 'breathing',
    };
}

function _loadAnimationState(anim: any) {
    const val = (anim && anim.enabled && anim.type) ? anim.type : 'none';
    (document.getElementById('css-editor-animation-type') as HTMLInputElement).value = val;
    if (_animationTypeIconSelect) _animationTypeIconSelect.setValue(val);
    _syncAnimationSpeedState();
}

export function onAnimationTypeChange() {
    if (_animationTypeIconSelect) _animationTypeIconSelect.setValue((document.getElementById('css-editor-animation-type') as HTMLInputElement).value);
    _syncAnimationSpeedState();
}

function _syncAnimationSpeedState() {
    const type = (document.getElementById('css-editor-animation-type') as HTMLInputElement).value;
    const descEl = document.getElementById('css-editor-animation-type-desc') as HTMLElement | null;
    if (descEl) {
        const desc = t('color_strip.animation.type.' + type + '.desc') || '';
        descEl.textContent = desc;
        descEl.style.display = desc ? '' : 'none';
    }
}

// ── Daylight real-time toggle ───────────────────────────────────

export function onDaylightRealTimeChange() {
    _syncDaylightSpeedVisibility();
}

function _syncDaylightSpeedVisibility() {
    const isRealTime = (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked;
    (document.getElementById('css-editor-daylight-speed-group') as HTMLElement).style.display = isRealTime ? 'none' : '';
}

// ══════════════════════════════════════════════════════════════════
// IconSelect / EntitySelect instances (that remain in index.ts)
// ══════════════════════════════════════════════════════════════════

let _animationTypeIconSelect: any = null;
let _interpolationIconSelect: any = null;
let _effectTypeIconSelect: any = null;
let _effectPaletteEntitySelect: EntitySelect | null = null;
let _gradientPresetEntitySelect: EntitySelect | null = null;
let _gradientEasingIconSelect: any = null;
let _candleTypeIconSelect: any = null;
let _apiInputInterpolationIconSelect: any = null;

function _ensureInterpolationIconSelect() {
    const sel = document.getElementById('css-editor-interpolation') as HTMLSelectElement | null;
    if (!sel) return;
    const items = [
        { value: 'average',  icon: _icon(P.slidersHorizontal), label: t('color_strip.interpolation.average'),  desc: t('color_strip.interpolation.average.desc') },
        { value: 'median',   icon: _icon(P.activity),          label: t('color_strip.interpolation.median'),   desc: t('color_strip.interpolation.median.desc') },
        { value: 'dominant', icon: _icon(P.target),             label: t('color_strip.interpolation.dominant'), desc: t('color_strip.interpolation.dominant.desc') },
    ];
    if (_interpolationIconSelect) { _interpolationIconSelect.updateItems(items); return; }
    _interpolationIconSelect = new IconSelect({ target: sel, items, columns: 3 });
}

function _ensureSmoothingWidget(): BindableScalarWidget {
    const container = document.getElementById('css-editor-smoothing-container')!;
    if (!_smoothingWidget) {
        _smoothingWidget = new BindableScalarWidget({
            container,
            min: 0, max: 1, step: 0.05, default: 0.3,
            idPrefix: 'css-editor-smoothing',
            valueSources: () => _cachedValueSources,
        });
    }
    return _smoothingWidget;
}

function _ensureKcSmoothingWidget(): BindableScalarWidget {
    if (!_kcSmoothingWidget) {
        _kcSmoothingWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-kc-smoothing-container')!,
            min: 0, max: 1, step: 0.05, default: 0.3,
            idPrefix: 'css-editor-kc-smoothing',
            format: (v) => v.toFixed(2),
            valueSources: () => _cachedValueSources,
        });
    }
    return _kcSmoothingWidget;
}

function _ensureKcBrightnessWidget(): BindableScalarWidget {
    if (!_kcBrightnessWidget) {
        _kcBrightnessWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-kc-brightness-container')!,
            min: 0, max: 1, step: 0.05, default: 1.0,
            idPrefix: 'css-editor-kc-brightness',
            format: (v) => v.toFixed(2),
            valueSources: () => _cachedValueSources,
        });
    }
    return _kcBrightnessWidget;
}

function _ensureEffectIntensityWidget(): BindableScalarWidget {
    if (!_effectIntensityWidget) {
        _effectIntensityWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-effect-intensity-container')!,
            min: 0.1, max: 2.0, step: 0.1, default: 1.0,
            idPrefix: 'css-editor-effect-intensity',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _effectIntensityWidget;
}

function _ensureEffectScaleWidget(): BindableScalarWidget {
    if (!_effectScaleWidget) {
        _effectScaleWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-effect-scale-container')!,
            min: 0.5, max: 5.0, step: 0.1, default: 1.0,
            idPrefix: 'css-editor-effect-scale',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _effectScaleWidget;
}

function _ensureApiInputTimeoutWidget(): BindableScalarWidget {
    if (!_apiInputTimeoutWidget) {
        _apiInputTimeoutWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-api-input-timeout-container')!,
            min: 0, max: 60, step: 0.5, default: 5.0,
            idPrefix: 'css-editor-api-input-timeout',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _apiInputTimeoutWidget;
}

function _ensureCandlelightIntensityWidget(): BindableScalarWidget {
    if (!_candlelightIntensityWidget) {
        _candlelightIntensityWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-candlelight-intensity-container')!,
            min: 0.1, max: 2.0, step: 0.1, default: 1.0,
            idPrefix: 'css-editor-candlelight-intensity',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _candlelightIntensityWidget;
}

function _ensureCandlelightSpeedWidget(): BindableScalarWidget {
    if (!_candlelightSpeedWidget) {
        _candlelightSpeedWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-candlelight-speed-container')!,
            min: 0.1, max: 5.0, step: 0.1, default: 1.0,
            idPrefix: 'css-editor-candlelight-speed',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _candlelightSpeedWidget;
}

function _ensureCandlelightWindWidget(): BindableScalarWidget {
    if (!_candlelightWindWidget) {
        _candlelightWindWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-candlelight-wind-container')!,
            min: 0.0, max: 2.0, step: 0.1, default: 0.0,
            idPrefix: 'css-editor-candlelight-wind',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _candlelightWindWidget;
}

function _ensureWeatherSpeedWidget(): BindableScalarWidget {
    if (!_weatherSpeedWidget) {
        _weatherSpeedWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-weather-speed-container')!,
            min: 0.1, max: 5, step: 0.1, default: 1.0,
            idPrefix: 'css-editor-weather-speed',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(1),
        });
    }
    return _weatherSpeedWidget;
}

function _ensureWeatherTempInfluenceWidget(): BindableScalarWidget {
    if (!_weatherTempInfluenceWidget) {
        _weatherTempInfluenceWidget = new BindableScalarWidget({
            container: document.getElementById('css-editor-weather-temp-influence-container')!,
            min: 0, max: 1, step: 0.05, default: 0.5,
            idPrefix: 'css-editor-weather-temp-influence',
            valueSources: () => _cachedValueSources,
            format: (v) => v.toFixed(2),
        });
    }
    return _weatherTempInfluenceWidget;
}

function _ensureSingleColorWidget(): BindableColorWidget {
    if (!_singleColorWidget) {
        _singleColorWidget = new BindableColorWidget({
            container: document.getElementById('css-editor-color-container')!,
            default: [255, 255, 255],
            valueSources: () => _cachedValueSources,
            idPrefix: 'css-editor-color',
        });
    }
    return _singleColorWidget;
}

function _ensureEffectColorWidget(): BindableColorWidget {
    if (!_effectColorWidget) {
        _effectColorWidget = new BindableColorWidget({
            container: document.getElementById('css-editor-effect-color-container')!,
            default: [255, 80, 0],
            valueSources: () => _cachedValueSources,
            idPrefix: 'css-editor-effect-color',
        });
    }
    return _effectColorWidget;
}

function _ensureApiInputFallbackColorWidget(): BindableColorWidget {
    if (!_apiInputFallbackColorWidget) {
        _apiInputFallbackColorWidget = new BindableColorWidget({
            container: document.getElementById('css-editor-api-input-fallback-color-container')!,
            default: [0, 0, 0],
            valueSources: () => _cachedValueSources,
            idPrefix: 'css-editor-api-input-fallback-color',
        });
    }
    return _apiInputFallbackColorWidget;
}

function _ensureCandlelightColorWidget(): BindableColorWidget {
    if (!_candlelightColorWidget) {
        _candlelightColorWidget = new BindableColorWidget({
            container: document.getElementById('css-editor-candlelight-color-container')!,
            default: [255, 147, 41],
            valueSources: () => _cachedValueSources,
            idPrefix: 'css-editor-candlelight-color',
        });
    }
    return _candlelightColorWidget;
}

function _ensureApiInputInterpolationIconSelect() {
    const sel = document.getElementById('css-editor-api-input-interpolation') as HTMLSelectElement | null;
    if (!sel) return;
    const items = [
        { value: 'linear',  icon: _icon(P.activity),  label: t('color_strip.api_input.interpolation.linear'),  desc: t('color_strip.api_input.interpolation.linear.desc') },
        { value: 'nearest', icon: _icon(P.hash),       label: t('color_strip.api_input.interpolation.nearest'), desc: t('color_strip.api_input.interpolation.nearest.desc') },
        { value: 'none',    icon: _icon(P.circleOff),  label: t('color_strip.api_input.interpolation.none'),    desc: t('color_strip.api_input.interpolation.none.desc') },
    ];
    if (_apiInputInterpolationIconSelect) { _apiInputInterpolationIconSelect.updateItems(items); return; }
    _apiInputInterpolationIconSelect = new IconSelect({ target: sel, items, columns: 3 });
}

function _ensureEffectTypeIconSelect() {
    const sel = document.getElementById('css-editor-effect-type') as HTMLSelectElement | null;
    if (!sel) return;
    const items = [
        { value: 'fire',             icon: _icon(P.zap),       label: t('color_strip.effect.fire'),             desc: t('color_strip.effect.fire.desc') },
        { value: 'meteor',           icon: _icon(P.sparkles),  label: t('color_strip.effect.meteor'),           desc: t('color_strip.effect.meteor.desc') },
        { value: 'plasma',           icon: _icon(P.rainbow),   label: t('color_strip.effect.plasma'),           desc: t('color_strip.effect.plasma.desc') },
        { value: 'noise',            icon: _icon(P.activity),  label: t('color_strip.effect.noise'),            desc: t('color_strip.effect.noise.desc') },
        { value: 'aurora',           icon: _icon(P.sparkles),  label: t('color_strip.effect.aurora'),           desc: t('color_strip.effect.aurora.desc') },
        { value: 'rain',             icon: _icon(P.cloudSun),  label: t('color_strip.effect.rain'),             desc: t('color_strip.effect.rain.desc') },
        { value: 'comet',            icon: _icon(P.rocket),    label: t('color_strip.effect.comet'),            desc: t('color_strip.effect.comet.desc') },
        { value: 'bouncing_ball',    icon: _icon(P.activity),  label: t('color_strip.effect.bouncing_ball'),    desc: t('color_strip.effect.bouncing_ball.desc') },
        { value: 'fireworks',        icon: _icon(P.sparkles),  label: t('color_strip.effect.fireworks'),        desc: t('color_strip.effect.fireworks.desc') },
        { value: 'sparkle_rain',     icon: _icon(P.star),      label: t('color_strip.effect.sparkle_rain'),     desc: t('color_strip.effect.sparkle_rain.desc') },
        { value: 'lava_lamp',        icon: _icon(P.flame),     label: t('color_strip.effect.lava_lamp'),        desc: t('color_strip.effect.lava_lamp.desc') },
        { value: 'wave_interference',icon: _icon(P.rainbow),   label: t('color_strip.effect.wave_interference'),desc: t('color_strip.effect.wave_interference.desc') },
    ];
    if (_effectTypeIconSelect) { _effectTypeIconSelect.updateItems(items); return; }
    _effectTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}

function _ensureEffectPaletteEntitySelect() {
    const sel = document.getElementById('css-editor-effect-palette') as HTMLSelectElement | null;
    if (!sel) return;
    const items = buildGradientEntityItems();
    syncSelectOptions(sel, items);
    if (_effectPaletteEntitySelect) { _effectPaletteEntitySelect.refresh(); return; }
    _effectPaletteEntitySelect = new EntitySelect({
        target: sel,
        getItems: buildGradientEntityItems,
        placeholder: t('palette.search'),
    });
}

function _ensureGradientEasingIconSelect() {
    const sel = document.getElementById('css-editor-gradient-easing') as HTMLSelectElement | null;
    if (!sel) return;
    const items = [
        { value: 'linear',      icon: _icon(P.trendingUp),         label: t('color_strip.gradient.easing.linear'),      desc: t('color_strip.gradient.easing.linear.desc') },
        { value: 'ease_in_out', icon: _icon(P.activity),           label: t('color_strip.gradient.easing.ease_in_out'), desc: t('color_strip.gradient.easing.ease_in_out.desc') },
        { value: 'step',        icon: _icon(P.layoutDashboard),    label: t('color_strip.gradient.easing.step'),        desc: t('color_strip.gradient.easing.step.desc') },
        { value: 'cubic',       icon: _icon(P.slidersHorizontal), label: t('color_strip.gradient.easing.cubic'),       desc: t('color_strip.gradient.easing.cubic.desc') },
    ];
    if (_gradientEasingIconSelect) { _gradientEasingIconSelect.updateItems(items); return; }
    _gradientEasingIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}

function _ensureCandleTypeIconSelect() {
    const sel = document.getElementById('css-editor-candlelight-type') as HTMLSelectElement | null;
    if (!sel) return;
    const items = [
        { value: 'default',  icon: _icon(P.flame),    label: t('color_strip.candlelight.type.default'),  desc: t('color_strip.candlelight.type.default.desc') },
        { value: 'taper',    icon: _icon(P.activity),  label: t('color_strip.candlelight.type.taper'),    desc: t('color_strip.candlelight.type.taper.desc') },
        { value: 'votive',   icon: _icon(P.lightbulb), label: t('color_strip.candlelight.type.votive'),   desc: t('color_strip.candlelight.type.votive.desc') },
        { value: 'bonfire',  icon: _icon(P.zap),       label: t('color_strip.candlelight.type.bonfire'),  desc: t('color_strip.candlelight.type.bonfire.desc') },
    ];
    if (_candleTypeIconSelect) { _candleTypeIconSelect.updateItems(items); return; }
    _candleTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}

// ── Proxy calls to sub-modules that need shared helpers ─────────

function _ensureAudioPaletteEntitySelectProxy() {
    ensureAudioPaletteEntitySelect(buildGradientEntityItems, syncSelectOptions);
}

function _ensureMathWaveGradientEntitySelectProxy() {
    ensureMathWaveGradientEntitySelect(buildGradientEntityItems, syncSelectOptions);
}

function _ensureGradientPresetEntitySelect() {
    const sel = document.getElementById('css-editor-gradient-preset') as HTMLSelectElement | null;
    if (!sel) return;
    const items = buildGradientEntityItems();
    syncSelectOptions(sel, items);
    if (_gradientPresetEntitySelect) { _gradientPresetEntitySelect.refresh(); return; }
    _gradientPresetEntitySelect = new EntitySelect({
        target: sel,
        getItems: buildGradientEntityItems,
        placeholder: t('palette.search'),
    });
}

export function refreshGradientPresetPicker() {
    for (const selId of ['css-editor-gradient-preset', 'css-editor-effect-palette', 'css-editor-audio-palette', 'css-editor-math-wave-gradient']) {
        const sel = document.getElementById(selId) as HTMLSelectElement | null;
        if (sel) syncSelectOptions(sel, buildGradientEntityItems());
    }
    if (_gradientPresetEntitySelect) _gradientPresetEntitySelect.refresh();
    if (_effectPaletteEntitySelect) _effectPaletteEntitySelect.refresh();
    refreshAudioPaletteEntitySelect();
    refreshMathWaveGradientEntitySelect();
}

function _buildAnimationTypeItems(cssType: any) {
    const items = [
        { value: 'none',         icon: _icon(P.square),    label: t('color_strip.animation.type.none'),         desc: t('color_strip.animation.type.none.desc') },
        { value: 'breathing',    icon: _icon(P.activity),  label: t('color_strip.animation.type.breathing'),    desc: t('color_strip.animation.type.breathing.desc') },
    ];
    if (cssType === 'gradient') {
        items.push(
            { value: 'gradient_shift', icon: _icon(P.fastForward), label: t('color_strip.animation.type.gradient_shift'), desc: t('color_strip.animation.type.gradient_shift.desc') },
            { value: 'wave',           icon: _icon(P.rainbow),     label: t('color_strip.animation.type.wave'),           desc: t('color_strip.animation.type.wave.desc') },
            { value: 'noise_perturb',  icon: _icon(P.activity),    label: t('color_strip.animation.type.noise_perturb'),  desc: t('color_strip.animation.type.noise_perturb.desc') },
            { value: 'hue_rotate',     icon: _icon(P.rotateCw),    label: t('color_strip.animation.type.hue_rotate'),     desc: t('color_strip.animation.type.hue_rotate.desc') },
        );
    }
    items.push(
        { value: 'strobe',       icon: _icon(P.zap),       label: t('color_strip.animation.type.strobe'),       desc: t('color_strip.animation.type.strobe.desc') },
        { value: 'sparkle',      icon: _icon(P.sparkles),  label: t('color_strip.animation.type.sparkle'),      desc: t('color_strip.animation.type.sparkle.desc') },
        { value: 'pulse',        icon: _icon(P.trendingUp),label: t('color_strip.animation.type.pulse'),        desc: t('color_strip.animation.type.pulse.desc') },
        { value: 'candle',       icon: _icon(P.flame),     label: t('color_strip.animation.type.candle'),       desc: t('color_strip.animation.type.candle.desc') },
        { value: 'rainbow_fade', icon: _icon(P.rainbow),   label: t('color_strip.animation.type.rainbow_fade'), desc: t('color_strip.animation.type.rainbow_fade.desc') },
    );
    return items;
}

function _ensureAnimationTypeIconSelect(cssType: any) {
    const sel = document.getElementById('css-editor-animation-type') as HTMLSelectElement | null;
    if (!sel) return;
    const items = _buildAnimationTypeItems(cssType);
    if (_animationTypeIconSelect) { _animationTypeIconSelect.destroy(); _animationTypeIconSelect = null; }
    _animationTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}

// ── Effect type helpers ─────────────────────────────────────────

export function onEffectTypeChange() {
    const et = (document.getElementById('css-editor-effect-type') as HTMLInputElement).value;
    const usesColorPicker = ['meteor', 'comet', 'bouncing_ball'].includes(et);
    (document.getElementById('css-editor-effect-palette-group') as HTMLElement).style.display = !usesColorPicker ? '' : 'none';
    (document.getElementById('css-editor-effect-color-group') as HTMLElement).style.display = usesColorPicker ? '' : 'none';
    const usesIntensity = ['fire', 'meteor', 'aurora', 'rain', 'comet', 'bouncing_ball', 'fireworks', 'sparkle_rain'].includes(et);
    (document.getElementById('css-editor-effect-intensity-group') as HTMLElement).style.display = usesIntensity ? '' : 'none';
    const usesScale = ['plasma', 'noise', 'aurora', 'rain', 'lava_lamp', 'wave_interference'].includes(et);
    (document.getElementById('css-editor-effect-scale-group') as HTMLElement).style.display = usesScale ? '' : 'none';
    (document.getElementById('css-editor-effect-mirror-group') as HTMLElement).style.display =
        ['meteor', 'comet'].includes(et) ? '' : 'none';
    if (!usesColorPicker) onEffectPaletteChange();
    else (document.getElementById('css-editor-effect-custom-palette-group') as HTMLElement).style.display = 'none';
    const descEl = document.getElementById('css-editor-effect-type-desc') as HTMLElement | null;
    if (descEl) {
        const desc = t('color_strip.effect.' + et + '.desc') || '';
        descEl.textContent = desc;
        descEl.style.display = desc ? '' : 'none';
    }
    _autoGenerateCSSName();
}

export function onEffectPaletteChange() {
    // No-op — kept for HTML onclick compatibility
}

// ── Audio viz change ────────────────────────────────────────────

export function onAudioVizChange() {
    const viz = (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value;
    const MUSIC_MODES = new Set(['pulse_on_beat', 'energy_gradient', 'spectrum_bands', 'strobe_on_drop']);
    (document.getElementById('css-editor-audio-palette-group') as HTMLElement).style.display =
        (viz === 'spectrum' || viz === 'beat_pulse' || viz === 'energy_gradient' || viz === 'spectrum_bands') ? '' : 'none';
    (document.getElementById('css-editor-audio-color-group') as HTMLElement).style.display = viz === 'vu_meter' ? '' : 'none';
    (document.getElementById('css-editor-audio-color-peak-group') as HTMLElement).style.display = viz === 'vu_meter' ? '' : 'none';
    (document.getElementById('css-editor-audio-mirror-group') as HTMLElement).style.display = viz === 'spectrum' ? '' : 'none';
    (document.getElementById('css-editor-audio-beat-decay-group') as HTMLElement).style.display = MUSIC_MODES.has(viz) ? '' : 'none';
    if (MUSIC_MODES.has(viz)) ensureAudioBeatDecayWidget();
    _autoGenerateCSSName();
}

// ══════════════════════════════════════════════════════════════════
// Auto-name generation
// ══════════════════════════════════════════════════════════════════

let _cssNameManuallyEdited: boolean = false;

function _autoGenerateCSSName() {
    if (_cssNameManuallyEdited) return;
    if ((document.getElementById('css-editor-id') as HTMLInputElement).value) return;
    const type = (document.getElementById('css-editor-type') as HTMLInputElement).value;
    const typeLabel = t(`color_strip.type.${type}`);
    let detail = '';
    if (type === 'picture') {
        const sel = document.getElementById('css-editor-picture-source') as HTMLSelectElement | null;
        const name = sel?.selectedOptions[0]?.textContent?.trim();
        if (name) detail = name;
    } else if (type === 'effect') {
        const eff = (document.getElementById('css-editor-effect-type') as HTMLInputElement).value;
        if (eff) detail = t(`color_strip.effect.${eff}`);
    } else if (type === 'audio') {
        const viz = (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value;
        if (viz) detail = t(`color_strip.audio.viz.${viz}`);
    } else if (type === 'notification') {
        const eff = (document.getElementById('css-editor-notification-effect') as HTMLInputElement).value;
        if (eff) detail = t(`color_strip.notification.effect.${eff}`);
    }
    (document.getElementById('css-editor-name') as HTMLInputElement).value = detail ? `${typeLabel} \u00B7 ${detail}` : typeLabel;
}

// ══════════════════════════════════════════════════════════════════
// Per-type handler registry
// ══════════════════════════════════════════════════════════════════

const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...args: any[]) => any; getPayload: (name: any) => any }> = {
    single_color: {
        load(css) {
            _ensureSingleColorWidget().setValue(css.color);
            _loadAnimationState(css.animation);
        },
        reset() {
            _ensureSingleColorWidget().setValue([255, 255, 255]);
            _loadAnimationState(null);
        },
        getPayload(name) {
            return { name, color: _ensureSingleColorWidget().getValue(), animation: _getAnimationPayload() };
        },
    },
    gradient: {
        load(css) {
            const gradientId = css.gradient_id || '';
            _ensureGradientPresetEntitySelect();
            (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = gradientId;
            if (_gradientPresetEntitySelect) _gradientPresetEntitySelect.setValue(gradientId);
            _loadAnimationState(css.animation);
            (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = css.easing || 'linear';
            if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue(css.easing || 'linear');
        },
        reset() {
            _ensureGradientPresetEntitySelect();
            const gradients = getGradients();
            const defaultId = gradients.length > 0 ? gradients[0].id : '';
            (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = defaultId;
            if (_gradientPresetEntitySelect) _gradientPresetEntitySelect.setValue(defaultId);
            _loadAnimationState(null);
            (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = 'linear';
            if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue('linear');
        },
        getPayload(name) {
            const gradientId = (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value;
            if (!gradientId) { cssEditorModal.showError(t('color_strip.gradient.error.no_gradient')); return null; }
            return { name, gradient_id: gradientId, animation: _getAnimationPayload(), easing: (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value };
        },
    },
    effect: {
        load(css) {
            (document.getElementById('css-editor-effect-type') as HTMLInputElement).value = css.effect_type || 'fire';
            if (_effectTypeIconSelect) _effectTypeIconSelect.setValue(css.effect_type || 'fire');
            onEffectTypeChange();
            const gradientId = css.gradient_id || 'gr_builtin_fire';
            (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value = gradientId;
            if (_effectPaletteEntitySelect) _effectPaletteEntitySelect.setValue(gradientId);
            _ensureEffectColorWidget().setValue(css.color);
            _ensureEffectIntensityWidget().setValue(css.intensity ?? 1.0);
            _ensureEffectScaleWidget().setValue(css.scale ?? 1.0);
            (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked = css.mirror || false;
            onEffectPaletteChange();
        },
        reset() {
            (document.getElementById('css-editor-effect-type') as HTMLInputElement).value = 'fire';
            (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value = 'gr_builtin_fire';
            if (_effectPaletteEntitySelect) _effectPaletteEntitySelect.setValue('gr_builtin_fire');
            _ensureEffectColorWidget().setValue([255, 80, 0]);
            _ensureEffectIntensityWidget().setValue(1.0);
            _ensureEffectScaleWidget().setValue(1.0);
            (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked = false;
        },
        getPayload(name) {
            const payload: any = {
                name,
                effect_type: (document.getElementById('css-editor-effect-type') as HTMLInputElement).value,
                gradient_id: (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value,
                intensity: _ensureEffectIntensityWidget().getValue(),
                scale: _ensureEffectScaleWidget().getValue(),
                mirror: (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked,
            };
            if (['meteor', 'comet', 'bouncing_ball'].includes(payload.effect_type)) {
                payload.color = _ensureEffectColorWidget().getValue();
            }
            return payload;
        },
    },
    audio: {
        async load(css) { await loadAudioSources(); loadAudioState(css); },
        reset() { resetAudioState(); },
        getPayload(name) {
            return {
                name,
                visualization_mode: (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value,
                audio_source_id: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value || null,
                sensitivity: ensureAudioSensitivityWidget().getValue(),
                smoothing: ensureAudioSmoothingWidget().getValue(),
                beat_decay: ensureAudioBeatDecayWidget().getValue(),
                gradient_id: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value,
                color: ensureAudioColorWidget().getValue(),
                color_peak: ensureAudioColorPeakWidget().getValue(),
                mirror: (document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked,
            };
        },
    },
    composite: {
        load(css) { loadCompositeState(css); },
        reset() { loadCompositeState(null); },
        getPayload(name) {
            const layers = compositeGetLayers();
            if (layers.length < 1) { cssEditorModal.showError(t('color_strip.composite.error.min_layers')); return null; }
            if (layers.some(l => !l.source_id)) { cssEditorModal.showError(t('color_strip.composite.error.no_source')); return null; }
            return { name, layers };
        },
    },
    mapped: {
        load(css) { loadMappedState(css); },
        reset() { resetMappedState(); },
        getPayload(name) {
            const zones = mappedGetZones();
            if (zones.some(z => !z.source_id)) { cssEditorModal.showError(t('color_strip.mapped.error.no_source')); return null; }
            return { name, zones };
        },
    },
    api_input: {
        load(css) {
            _ensureApiInputFallbackColorWidget().setValue(css.fallback_color);
            _ensureApiInputTimeoutWidget().setValue(css.timeout ?? 5.0);
            (document.getElementById('css-editor-api-input-interpolation') as HTMLSelectElement).value = css.interpolation || 'linear';
            _ensureApiInputInterpolationIconSelect();
            _showApiInputEndpoints(css.id);
        },
        reset() {
            _ensureApiInputFallbackColorWidget().setValue([0, 0, 0]);
            _ensureApiInputTimeoutWidget().setValue(5.0);
            (document.getElementById('css-editor-api-input-interpolation') as HTMLSelectElement).value = 'linear';
            _ensureApiInputInterpolationIconSelect();
            _showApiInputEndpoints(null);
        },
        getPayload(name) {
            return {
                name,
                fallback_color: _ensureApiInputFallbackColorWidget().getValue(),
                timeout: _ensureApiInputTimeoutWidget().getValue(),
                interpolation: (document.getElementById('css-editor-api-input-interpolation') as HTMLSelectElement).value,
            };
        },
    },
    notification: {
        load(css) { loadNotificationState(css); },
        reset() { resetNotificationState(); },
        getPayload(name) {
            const filterList = (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value
                .split('\n').map(s => s.trim()).filter(Boolean);
            return {
                name,
                os_listener: (document.getElementById('css-editor-notification-os-listener') as HTMLInputElement).checked,
                notification_effect: (document.getElementById('css-editor-notification-effect') as HTMLInputElement).value,
                duration_ms: getNotificationDurationValue(),
                default_color: getNotificationDefaultColorValue(),
                app_filter_mode: (document.getElementById('css-editor-notification-filter-mode') as HTMLInputElement).value,
                app_filter_list: filterList,
                app_colors: notificationGetAppColorsDict(),
                sound_asset_id: (document.getElementById('css-editor-notification-sound') as HTMLSelectElement).value || null,
                sound_volume: getNotificationVolumeValue(),
                app_sounds: notificationGetAppSoundsDict(),
            };
        },
    },
    daylight: {
        load(css) {
            (document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value = css.speed ?? 1.0;
            (document.getElementById('css-editor-daylight-speed-val') as HTMLElement).textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
            (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked = css.use_real_time || false;
            (document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value = css.latitude ?? 50.0;
            (document.getElementById('css-editor-daylight-latitude-val') as HTMLElement).textContent = parseFloat(css.latitude ?? 50.0).toFixed(0);
            (document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value = css.longitude ?? 0.0;
            (document.getElementById('css-editor-daylight-longitude-val') as HTMLElement).textContent = parseFloat(css.longitude ?? 0.0).toFixed(0);
            _syncDaylightSpeedVisibility();
        },
        reset() {
            (document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value = 1.0 as any;
            (document.getElementById('css-editor-daylight-speed-val') as HTMLElement).textContent = '1.0';
            (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked = false;
            (document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value = 50.0 as any;
            (document.getElementById('css-editor-daylight-latitude-val') as HTMLElement).textContent = '50';
            (document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value = 0.0 as any;
            (document.getElementById('css-editor-daylight-longitude-val') as HTMLElement).textContent = '0';
            _syncDaylightSpeedVisibility();
        },
        getPayload(name) {
            return {
                name,
                speed: parseFloat((document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value),
                use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked,
                latitude: parseFloat((document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value),
                longitude: parseFloat((document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value),
            };
        },
    },
    candlelight: {
        load(css) {
            _ensureCandlelightColorWidget().setValue(css.color);
            _ensureCandlelightIntensityWidget().setValue(css.intensity ?? 1.0);
            (document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value = css.num_candles ?? 3;
            _ensureCandlelightSpeedWidget().setValue(css.speed ?? 1.0);
            _ensureCandlelightWindWidget().setValue(css.wind_strength ?? 0.0);
            (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value = css.candle_type || 'default';
            if (_candleTypeIconSelect) _candleTypeIconSelect.setValue(css.candle_type || 'default');
        },
        reset() {
            _ensureCandlelightColorWidget().setValue([255, 147, 41]);
            _ensureCandlelightIntensityWidget().setValue(1.0);
            (document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value = 3 as any;
            _ensureCandlelightSpeedWidget().setValue(1.0);
            _ensureCandlelightWindWidget().setValue(0.0);
            (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value = 'default';
            if (_candleTypeIconSelect) _candleTypeIconSelect.setValue('default');
        },
        getPayload(name) {
            return {
                name,
                color: _ensureCandlelightColorWidget().getValue(),
                intensity: _ensureCandlelightIntensityWidget().getValue(),
                num_candles: parseInt((document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value) || 3,
                speed: _ensureCandlelightSpeedWidget().getValue(),
                wind_strength: _ensureCandlelightWindWidget().getValue(),
                candle_type: (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value,
            };
        },
    },
    weather: {
        async load(css) {
            await weatherSourcesCache.fetch();
            _populateWeatherSourceDropdown();
            (document.getElementById('css-editor-weather-source') as HTMLSelectElement).value = css.weather_source_id || '';
            if (_weatherSourceEntitySelect) _weatherSourceEntitySelect.refresh();
            _ensureWeatherSpeedWidget().setValue(css.speed ?? 1.0);
            _ensureWeatherTempInfluenceWidget().setValue(css.temperature_influence ?? 0.5);
        },
        async reset() {
            await weatherSourcesCache.fetch();
            _populateWeatherSourceDropdown();
            (document.getElementById('css-editor-weather-source') as HTMLSelectElement).value = '';
            _ensureWeatherSpeedWidget().setValue(1.0);
            _ensureWeatherTempInfluenceWidget().setValue(0.5);
        },
        getPayload(name) {
            const wsId = (document.getElementById('css-editor-weather-source') as HTMLSelectElement).value;
            if (!wsId) { cssEditorModal.showError(t('color_strip.weather.error.no_source')); return null; }
            return { name, weather_source_id: wsId, speed: _ensureWeatherSpeedWidget().getValue(), temperature_influence: _ensureWeatherTempInfluenceWidget().getValue() };
        },
    },
    processed: {
        async load(css) {
            await csptCache.fetch();
            await colorStripSourcesCache.fetch();
            _populateProcessedSelectors();
            (document.getElementById('css-editor-processed-input') as HTMLInputElement).value = css.input_source_id || '';
            if (_processedInputEntitySelect) _processedInputEntitySelect.refresh();
            (document.getElementById('css-editor-processed-template') as HTMLInputElement).value = css.processing_template_id || '';
            if (_processedTemplateEntitySelect) _processedTemplateEntitySelect.refresh();
        },
        async reset(presetType) {
            if (presetType === 'processed') {
                await csptCache.fetch();
                await colorStripSourcesCache.fetch();
                _populateProcessedSelectors();
            }
        },
        getPayload(name) {
            const inputId = (document.getElementById('css-editor-processed-input') as HTMLInputElement).value;
            if (!inputId) { cssEditorModal.showError(t('color_strip.processed.error.no_input')); return null; }
            return { name, input_source_id: inputId, processing_template_id: (document.getElementById('css-editor-processed-template') as HTMLInputElement).value || null };
        },
    },
    picture_advanced: {
        load(css) {
            (document.getElementById('css-editor-interpolation') as HTMLInputElement).value = css.interpolation_mode || 'average';
            if (_interpolationIconSelect) _interpolationIconSelect.setValue(css.interpolation_mode || 'average');
            _ensureSmoothingWidget().setValue(css.smoothing);
        },
        reset() {
            (document.getElementById('css-editor-interpolation') as HTMLInputElement).value = 'average';
            if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
            _ensureSmoothingWidget().setValue(0.3);
        },
        getPayload(name) {
            return { name, interpolation_mode: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value, smoothing: _ensureSmoothingWidget().getValue(), led_count: parseInt((document.getElementById('css-editor-led-count') as HTMLInputElement).value) || 0 };
        },
    },
    picture: {
        load(css, sourceSelect) {
            sourceSelect.value = css.picture_source_id || '';
            if (_cssPictureSourceEntitySelect) _cssPictureSourceEntitySelect.setValue(sourceSelect.value);
            (document.getElementById('css-editor-interpolation') as HTMLInputElement).value = css.interpolation_mode || 'average';
            if (_interpolationIconSelect) _interpolationIconSelect.setValue(css.interpolation_mode || 'average');
            _ensureSmoothingWidget().setValue(css.smoothing);
        },
        reset() {
            (document.getElementById('css-editor-interpolation') as HTMLInputElement).value = 'average';
            if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
            _ensureSmoothingWidget().setValue(0.3);
        },
        getPayload(name) {
            return { name, picture_source_id: (document.getElementById('css-editor-picture-source') as HTMLInputElement).value, interpolation_mode: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value, smoothing: _ensureSmoothingWidget().getValue(), led_count: parseInt((document.getElementById('css-editor-led-count') as HTMLInputElement).value) || 0 };
        },
    },
    key_colors: {
        async load(css) {
            const sourceSelect = document.getElementById('css-editor-kc-picture-source') as HTMLSelectElement;
            const sources = await streamsCache.fetch().catch((): any[] => []);
            sourceSelect.innerHTML = sources.map((s: any) =>
                `<option value="${s.id}" ${s.id === (css.picture_source_id || '') ? 'selected' : ''}>${escapeHtml(s.name)}</option>`
            ).join('');
            if (_kcPictureSourceEntitySelect) _kcPictureSourceEntitySelect.destroy();
            _kcPictureSourceEntitySelect = new EntitySelect({
                target: sourceSelect,
                getItems: () => sources.map((s: any) => ({ value: s.id, label: s.name, icon: getPictureSourceIcon(s.stream_type), desc: s.stream_type })),
                placeholder: t('palette.search'),
            });
            const interpSelect = document.getElementById('css-editor-kc-interpolation') as HTMLSelectElement;
            interpSelect.value = css.interpolation_mode || 'average';
            if (_kcInterpolationIconSelect) _kcInterpolationIconSelect.destroy();
            _kcInterpolationIconSelect = new IconSelect({
                target: interpSelect,
                items: [
                    { value: 'average', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.average'), desc: t('color_strip.key_colors.mode.average.desc') },
                    { value: 'median', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.median'), desc: t('color_strip.key_colors.mode.median.desc') },
                    { value: 'dominant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.dominant'), desc: t('color_strip.key_colors.mode.dominant.desc') },
                ],
                columns: 1,
            });
            _ensureKcSmoothingWidget().setValue(css.smoothing ?? 0.3);
            _ensureKcBrightnessWidget().setValue(css.brightness ?? 1.0);
            _kcEditorRects = (css.rectangles || []).map((r: any) => ({ ...r }));
            _renderKCRectSummary();
        },
        async reset() {
            const sourceSelect = document.getElementById('css-editor-kc-picture-source') as HTMLSelectElement;
            const sources = await streamsCache.fetch().catch((): any[] => []);
            sourceSelect.innerHTML = sources.map((s: any) =>
                `<option value="${s.id}">${escapeHtml(s.name)}</option>`
            ).join('');
            if (_kcPictureSourceEntitySelect) _kcPictureSourceEntitySelect.destroy();
            _kcPictureSourceEntitySelect = new EntitySelect({
                target: sourceSelect,
                getItems: () => sources.map((s: any) => ({ value: s.id, label: s.name, icon: getPictureSourceIcon(s.stream_type), desc: s.stream_type })),
                placeholder: t('palette.search'),
            });
            const interpSelect = document.getElementById('css-editor-kc-interpolation') as HTMLSelectElement;
            interpSelect.value = 'average';
            if (_kcInterpolationIconSelect) _kcInterpolationIconSelect.destroy();
            _kcInterpolationIconSelect = new IconSelect({
                target: interpSelect,
                items: [
                    { value: 'average', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.average'), desc: t('color_strip.key_colors.mode.average.desc') },
                    { value: 'median', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.median'), desc: t('color_strip.key_colors.mode.median.desc') },
                    { value: 'dominant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.palette}</svg>`, label: t('color_strip.key_colors.mode.dominant'), desc: t('color_strip.key_colors.mode.dominant.desc') },
                ],
                columns: 1,
            });
            _ensureKcSmoothingWidget().setValue(0.3);
            _ensureKcBrightnessWidget().setValue(1.0);
            _kcEditorRects = [{ name: 'Region 1', x: 0.0, y: 0.0, width: 1.0, height: 1.0 }];
            _renderKCRectSummary();
        },
        getPayload(name) {
            const psId = (document.getElementById('css-editor-kc-picture-source') as HTMLSelectElement).value;
            if (!psId) { cssEditorModal.showError(t('color_strip.key_colors.error.no_source')); return null; }
            if (_kcEditorRects.length === 0) { cssEditorModal.showError(t('color_strip.key_colors.error.no_rects')); return null; }
            return {
                name,
                picture_source_id: psId,
                rectangles: _kcEditorRects.map(r => ({ name: r.name, x: r.x, y: r.y, width: r.width, height: r.height })),
                interpolation_mode: (document.getElementById('css-editor-kc-interpolation') as HTMLSelectElement).value,
                smoothing: _ensureKcSmoothingWidget().getValue(),
                brightness: _ensureKcBrightnessWidget().getValue(),
            };
        },
    },
    math_wave: {
        load(css: any) {
            _ensureMathWaveGradientEntitySelectProxy();
            const gradientId = css.gradient_id || '';
            (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value = gradientId;
            const es = getMathWaveGradientEntitySelect();
            if (es) es.setValue(gradientId);
            ensureMathWaveSpeedWidget().setValue(css.speed ?? 1.0);
            mathWaveRenderLayers(css.waves || [{ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 }]);
        },
        reset() {
            _ensureMathWaveGradientEntitySelectProxy();
            const gradients = getGradients();
            const defaultId = gradients.length > 0 ? gradients[0].id : '';
            (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value = defaultId;
            const es = getMathWaveGradientEntitySelect();
            if (es) es.setValue(defaultId);
            ensureMathWaveSpeedWidget().setValue(1.0);
            mathWaveRenderLayers([{ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 }]);
        },
        getPayload(name: any) {
            const gradientId = (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value;
            const waves = mathWaveGetLayers();
            if (waves.length === 0) { cssEditorModal.showError(t('color_strip.math_wave.error.no_waves')); return null; }
            return { name, gradient_id: gradientId || null, speed: ensureMathWaveSpeedWidget().getValue(), waves };
        },
    },
    game_event: {
        load(css: any) {
            populateGameIntegrationDropdownCSS(css.game_integration_id || '');
            ensureGameEventIdleColorWidget().setValue(css.idle_color);
            renderCSSGameMappings(css.event_mappings || []);
        },
        reset() {
            populateGameIntegrationDropdownCSS('');
            ensureGameEventIdleColorWidget().setValue([0, 0, 0]);
            renderCSSGameMappings([]);
        },
        getPayload(name: any) {
            const giId = (document.getElementById('css-editor-game-integration') as HTMLSelectElement).value;
            if (!giId) { cssEditorModal.showError(t('color_strip.game_event.error.no_integration')); return null; }
            return { name, game_integration_id: giId, idle_color: ensureGameEventIdleColorWidget().getValue(), event_mappings: collectCSSGameMappings() };
        },
    },
};

// ══════════════════════════════════════════════════════════════════
// Preview payload — collects the current editor form into a config dict
// suitable for the transient preview WebSocket. Returns null if the type
// has no handler or if validation fails. Used by test.ts.
// ══════════════════════════════════════════════════════════════════

export function getCSSEditorPreviewPayload(sourceType: string): any {
    const handler = _typeHandlers[sourceType];
    if (!handler) return null;
    const payload = handler.getPayload('__preview__');
    if (!payload) return null;
    payload.source_type = sourceType;
    const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
    if (clockTypes.includes(sourceType)) {
        const clockEl = document.getElementById('css-editor-clock') as HTMLInputElement | null;
        if (clockEl && clockEl.value) payload.clock_id = clockEl.value;
    }
    return payload;
}

// ══════════════════════════════════════════════════════════════════
// Editor open / close / save
// ══════════════════════════════════════════════════════════════════

export async function showCSSEditor(cssId: any = null, cloneData: any = null, presetType: any = null) {
    if (!cssId && !cloneData && !presetType) {
        showTypePicker({
            title: t('color_strip.select_type'),
            items: _buildCSSTypeItems(),
            onPick: (type) => showCSSEditor(null, null, type),
        });
        return;
    }
    try {
        const sources: any[] = await streamsCache.fetch();
        await valueSourcesCache.fetch().catch((): any[] => []);
        const allCssSources: any[] = await colorStripSourcesCache.fetch().catch((): any[] => []);
        compositeSetAvailableSources(allCssSources.filter(s =>
            !cssId || s.id !== cssId
        ));
        mappedSetAvailableSources(allCssSources.filter(s =>
            s.source_type !== 'mapped' && (!cssId || s.id !== cssId)
        ));

        const sourceSelect = document.getElementById('css-editor-picture-source') as HTMLSelectElement;
        sourceSelect.innerHTML = '';
        sources.forEach(s => {
            const opt = document.createElement('option');
            opt.value = s.id;
            opt.dataset.name = s.name;
            opt.textContent = s.name;
            sourceSelect.appendChild(opt);
        });

        if (_cssPictureSourceEntitySelect) _cssPictureSourceEntitySelect.destroy();
        _cssPictureSourceEntitySelect = new EntitySelect({
            target: sourceSelect,
            getItems: () => sources.map(s => ({
                value: s.id,
                label: s.name,
                icon: getPictureSourceIcon(s.stream_type),
            })),
            placeholder: t('palette.search'),
        });

        const _populateFromCSS = async (css: any) => {
            const sourceType = css.source_type || 'picture';
            (document.getElementById('css-editor-type') as HTMLInputElement).value = sourceType;
            onCSSTypeChange();
            if (css.clock_id) {
                _populateClockDropdown(css.clock_id);
                onCSSClockChange();
            }
            const handler = _typeHandlers[sourceType] || _typeHandlers.picture;
            await handler.load(css, sourceSelect);
            (document.getElementById('css-editor-led-count') as HTMLInputElement).value = css.led_count ?? 0;
        };

        await gradientsCache.fetch();
        _ensureCSSTypeIconSelect();
        (document.getElementById('css-editor-type-group') as HTMLElement).style.display = 'none';

        if (cssId) {
            const cssSources: any[] = await colorStripSourcesCache.fetch();
            const css = cssSources.find(s => s.id === cssId);
            if (!css) throw new Error('Failed to load color strip source');
            (document.getElementById('css-editor-id') as HTMLInputElement).value = css.id;
            (document.getElementById('css-editor-name') as HTMLInputElement).value = css.name;
            if (css.source_type === 'composite') {
                compositeSetAvailableSources(allCssSources.filter(s =>
                    s.id !== css.id
                ));
            } else if (css.source_type === 'mapped') {
                mappedSetAvailableSources(allCssSources.filter(s =>
                    s.source_type !== 'mapped' && s.id !== css.id
                ));
            }
            await _populateFromCSS(css);
            const editIcon = getColorStripIcon(css.source_type);
            (document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${editIcon} ${t('color_strip.edit')} ${t(`color_strip.type.${css.source_type}`)}`;
        } else if (cloneData) {
            (document.getElementById('css-editor-id') as HTMLInputElement).value = '';
            (document.getElementById('css-editor-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
            await _populateFromCSS(cloneData);
            const cloneIcon = getColorStripIcon(cloneData.source_type);
            (document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${cloneIcon} ${t('color_strip.add')} ${t(`color_strip.type.${cloneData.source_type}`)}`;
        } else {
            (document.getElementById('css-editor-id') as HTMLInputElement).value = '';
            (document.getElementById('css-editor-name') as HTMLInputElement).value = '';
            const effectiveType = presetType || 'picture';
            (document.getElementById('css-editor-type') as HTMLInputElement).value = effectiveType;
            onCSSTypeChange();
            (document.getElementById('css-editor-led-count') as HTMLInputElement).value = 0 as any;
            for (const handler of Object.values(_typeHandlers)) {
                await handler.reset(effectiveType);
            }
            const typeIcon = getColorStripIcon(effectiveType);
            (document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${typeIcon} ${t('color_strip.add')} ${t(`color_strip.type.${effectiveType}`)}`;
            _autoGenerateCSSName();
        }

        _cssNameManuallyEdited = !!(cssId || cloneData);
        (document.getElementById('css-editor-name') as HTMLElement).oninput = () => { _cssNameManuallyEdited = true; };
        (document.getElementById('css-editor-picture-source') as HTMLElement).onchange = () => _autoGenerateCSSName();
        (document.getElementById('css-editor-notification-effect') as HTMLElement).onchange = () => _autoGenerateCSSName();
        (document.getElementById('css-editor-error') as HTMLElement).style.display = 'none';

        if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; }
        const _cssTags = cssId
            ? (((await colorStripSourcesCache.fetch()) as any[]).find((s: any) => s.id === cssId)?.tags || [])
            : (cloneData ? (cloneData.tags || []) : []);
        _cssTagsInput = new TagInput(document.getElementById('css-tags-container') as HTMLElement, { placeholder: t('tags.placeholder') });
        _cssTagsInput.setValue(_cssTags);

        cssEditorModal.snapshot();
        cssEditorModal.open();
        setTimeout(() => desktopFocus(document.getElementById('css-editor-name') as HTMLElement), 100);
    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to open CSS editor:', error);
        showToast(t('color_strip.error.editor_open_failed'), 'error');
    }
}

export function closeCSSEditorModal() { cssEditorModal.close(); }
export function forceCSSEditorClose() { cssEditorModal.forceClose(); }
export function isCSSEditorDirty() { return cssEditorModal.isDirty(); }

// ══════════════════════════════════════════════════════════════════
// Save
// ══════════════════════════════════════════════════════════════════

export async function saveCSSEditor() {
    const cssId = (document.getElementById('css-editor-id') as HTMLInputElement).value;
    if (cssEditorModal.closeIfPristine(cssId)) return;

    const name = (document.getElementById('css-editor-name') as HTMLInputElement).value.trim();
    const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value;

    if (!name) { cssEditorModal.showError(t('color_strip.error.name_required')); return; }

    const knownType = sourceType in _typeHandlers;
    const handler = knownType ? _typeHandlers[sourceType] : _typeHandlers.picture;
    const payload = handler.getPayload(name);
    if (payload === null) return;

    payload.source_type = knownType ? sourceType : 'picture';

    const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
    if (clockTypes.includes(sourceType)) {
        payload.clock_id = (document.getElementById('css-editor-clock') as HTMLInputElement).value || null;
    }
    payload.tags = _cssTagsInput ? _cssTagsInput.getValue() : [];

    try {
        let response: any;
        if (cssId) {
            response = await fetchWithAuth(`/color-strip-sources/${cssId}`, { method: 'PUT', body: JSON.stringify(payload) });
        } else {
            response = await fetchWithAuth('/color-strip-sources', { method: 'POST', body: JSON.stringify(payload) });
        }
        if (!response.ok) {
            const err = await response.json();
            throw new Error(err.detail || 'Failed to save');
        }
        showToast(cssId ? t('color_strip.updated') : t('color_strip.created'), 'success');
        colorStripSourcesCache.invalidate();
        cssEditorModal.forceClose();
        if (window.loadPictureSources) window.loadPictureSources();
        if (window.loadTargetsTab) await window.loadTargetsTab();
    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Error saving CSS:', error);
        cssEditorModal.showError(error.message);
    }
}

// ══════════════════════════════════════════════════════════════════
// API Input endpoint helper
// ══════════════════════════════════════════════════════════════════

function _showApiInputEndpoints(cssId: any) {
    const el = document.getElementById('css-editor-api-input-endpoints') as HTMLElement | null;
    const group = document.getElementById('css-editor-api-input-endpoints-group') as HTMLElement | null;
    if (!el || !group) return;
    if (!cssId) {
        el.innerHTML = `<em data-i18n="color_strip.api_input.save_first">${t('color_strip.api_input.save_first')}</em>`;
        return;
    }
    const origin = getBaseOrigin();
    const base = `${origin}/api/v1`;
    const wsProto = origin.startsWith('https') ? 'wss:' : 'ws:';
    const hostPart = origin.replace(/^https?:\/\//, '');
    const wsBase = `${wsProto}//${hostPart}/api/v1`;
    const restUrl = `${base}/color-strip-sources/${cssId}/colors`;
    const wsUrl = `${wsBase}/color-strip-sources/${cssId}/ws`;
    el.innerHTML = `
        <small class="endpoint-label">REST POST</small>
        <div class="ws-url-row" style="margin-bottom:6px"><input type="text" value="${restUrl}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">${ICON_CLONE}</button></div>
        <small class="endpoint-label">WebSocket</small>
        <div class="ws-url-row"><input type="text" value="${wsUrl}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">${ICON_CLONE}</button></div>
    `;
}

export function copyEndpointUrl(btn: any) {
    const input = btn.parentElement.querySelector('input');
    if (!input || !input.value) return;
    if (navigator.clipboard && window.isSecureContext) {
        navigator.clipboard.writeText(input.value).then(() => {
            showToast(t('settings.copied') || 'Copied!', 'success');
        });
    } else {
        input.select();
        document.execCommand('copy');
        showToast(t('settings.copied') || 'Copied!', 'success');
    }
}

// ══════════════════════════════════════════════════════════════════
// Clone / Delete / Overlay
// ══════════════════════════════════════════════════════════════════

export async function cloneColorStrip(cssId: string) {
    try {
        const sources: any[] = await colorStripSourcesCache.fetch();
        const css = sources.find(s => s.id === cssId);
        if (!css) throw new Error('Color strip source not found');
        showCSSEditor(null, css);
    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to clone color strip:', error);
        showToast(t('color_strip.error.clone_failed'), 'error');
    }
}

export async function deleteColorStrip(cssId: string) {
    const confirmed = await showConfirm(t('color_strip.delete.confirm'));
    if (!confirmed) return;
    try {
        const response = (await fetchWithAuth(`/color-strip-sources/${cssId}`, { method: 'DELETE' }))!;
        if (response.ok) {
            showToast(t('color_strip.deleted'), 'success');
            colorStripSourcesCache.invalidate();
            if (window.loadPictureSources) window.loadPictureSources();
            if (window.loadTargetsTab) await window.loadTargetsTab();
        } else {
            const err = await response.json();
            const msg = err.detail || (response.status === 409 ? t('color_strip.delete.referenced') : 'Failed to delete');
            showToast(msg, 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        showToast(t('color_strip.error.delete_failed'), 'error');
    }
}

export async function startCSSOverlay(cssId: string) {
    try {
        const response = (await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/start`, { method: 'POST' }))!;
        if (response.ok) {
            showToast(t('overlay.started'), 'success');
            if (window.loadTargetsTab) window.loadTargetsTab();
        } else {
            const error = await response.json();
            showToast(t('overlay.error.start') + ': ' + error.detail, 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        showToast(t('overlay.error.start'), 'error');
    }
}

export async function toggleCSSOverlay(cssId: string) {
    try {
        const resp = (await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/status`))!;
        if (!resp.ok) return;
        const { active } = await resp.json();
        if (active) await stopCSSOverlay(cssId);
        else await startCSSOverlay(cssId);
    } catch (err: any) {
        if (err.isAuth) return;
        console.error('Failed to toggle CSS overlay:', err);
    }
}

export async function stopCSSOverlay(cssId: string) {
    try {
        const response = (await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/stop`, { method: 'POST' }))!;
        if (response.ok) {
            showToast(t('overlay.stopped'), 'success');
            if (window.loadTargetsTab) window.loadTargetsTab();
        } else {
            const error = await response.json();
            showToast(t('overlay.error.stop') + ': ' + error.detail, 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        showToast(t('overlay.error.stop'), 'error');
    }
}
