/**
 * Entry point — imports all modules, registers globals, initializes app.
 */

// Layer 0: state
import { apiKey, setApiKey, authRequired, refreshInterval, setupRequired } from './core/state.ts';
import { Modal } from './core/modal.ts';
import { queryEl } from './core/dom-utils.ts';

// Layer 1: api, i18n
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor, serverRepoUrl, serverDonateUrl } from './core/api.ts';
import { t, initLocale, changeLocale } from './core/i18n.ts';

// Layer 1.5: visual effects
import { initCardGlare } from './core/card-glare.ts';
import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.ts';
import { initBgShaders } from './core/bg-shaders.ts';
import { initTabIndicator, updateTabIndicator } from './core/tab-indicator.ts';

// Layer 2: ui
import {
    toggleHint, lockBody, unlockBody, closeLightbox,
    showToast, showUndoToast, showConfirm, closeConfirmModal,
    openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner,
    setFieldError, clearFieldError, setupBlurValidation, initLightbox,
} from './core/ui.ts';

// Layer 3: displays, tutorials
import {
    openDisplayPicker, closeDisplayPicker, selectDisplay, formatDisplayLabel,
} from './features/displays.ts';
import {
    startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial,
    startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial,
    closeTutorial, tutorialNext, tutorialPrev,
} from './features/tutorials.ts';

// Layer 4: devices, dashboard, streams, pattern-templates, automations
import {
    showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
    saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
    turnOffDevice, pingDevice, removeDevice, loadDevices,
    updateSettingsBaudFpsHint, copyWsUrl,
} from './features/devices.ts';
import {
    loadDashboard, stopUptimeTimer,
    dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
    dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
    toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.ts';
import { startEventsWS, stopEventsWS } from './core/events-ws.ts';
import { startEntityEventListeners } from './core/entity-events.ts';
import {
    startPerfPolling, stopPerfPolling, setPerfMode,
} from './features/perf-charts.ts';
import {
    loadPictureSources, switchStreamTab,
    showAddTemplateModal, editTemplate, closeTemplateModal, saveTemplate, deleteTemplate,
    showTestTemplateModal, closeTestTemplateModal, onEngineChange, runTemplateTest,
    showAddAudioTemplateModal, editAudioTemplate, closeAudioTemplateModal, saveAudioTemplate, deleteAudioTemplate,
    cloneAudioTemplate, onAudioEngineChange,
    showTestAudioTemplateModal, closeTestAudioTemplateModal, startAudioTemplateTest,
    showAddStreamModal, editStream, closeStreamModal, saveStream, deleteStream,
    onStreamTypeChange, onStreamDisplaySelected, onTestDisplaySelected,
    showTestStreamModal, closeTestStreamModal, updateStreamTestDuration, runStreamTest,
    showTestPPTemplateModal, closeTestPPTemplateModal, updatePPTestDuration, runPPTemplateTest,
    showAddPPTemplateModal, editPPTemplate, closePPTemplateModal, savePPTemplate, deletePPTemplate,
    addFilterFromSelect, toggleFilterExpand, removeFilter, moveFilter, updateFilterOption,
    renderModalFilterList, updateCaptureDuration,
    cloneStream, cloneCaptureTemplate, clonePPTemplate,
    showAddCSPTModal, editCSPT, closeCSPTModal, saveCSPT, deleteCSPT, cloneCSPT,
    csptAddFilterFromSelect, csptToggleFilterExpand, csptRemoveFilter, csptUpdateFilterOption,
    renderCSPTModalFilterList,
} from './features/streams.ts';
import {
    showPatternTemplateEditor, closePatternTemplateModal, forceClosePatternTemplateModal,
    savePatternTemplate,
    renderPatternRectList, selectPatternRect, updatePatternRect,
    addPatternRect, deleteSelectedPatternRect, removePatternRect,
    capturePatternBackground,
} from './features/pattern-templates.ts';
import {
    loadAutomations, switchAutomationTab, openAutomationEditor, closeAutomationEditorModal,
    saveAutomationEditor, addAutomationRule,
    toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
} from './features/automations.ts';
import {
    showGameIntegrationEditor, saveGameIntegration, closeGameIntegrationModal,
    cloneGameIntegration, deleteGameIntegration,
    addGameMapping, removeGameMapping, onMappingPresetChange,
    testGameConnection, showGameEventMonitor,
    openSetupInstructions, closeSetupInstructions,
    autoSetupGameIntegration,
} from './features/game-integration.ts';
import {
    loadIntegrations, switchIntegrationTab,
} from './features/integrations.ts';
import {
    openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
    activateScenePreset, cloneScenePreset, deleteScenePreset,
    addSceneTarget,
} from './features/scene-presets.ts';

// Layer 5: device-discovery, targets
import {
    onDeviceTypeChanged, updateBaudFpsHint, onSerialPortFocus,
    showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice,
    cloneDevice, addGroupChild, addGroupChildSettings,
} from './features/device-discovery.ts';
import {
    loadTargetsTab, switchTargetSubTab,
    showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
    startTargetProcessing, stopTargetProcessing,
    stopAllLedTargets,
    startTargetOverlay, stopTargetOverlay, deleteTarget,
    cloneTarget, toggleLedPreview,
    disconnectAllLedPreviewWS,
} from './features/targets.ts';

// Layer 5: color-strip sources
import {
    showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
    onCSSTypeChange, onEffectTypeChange, onEffectPaletteChange, onAnimationTypeChange, onCSSClockChange, onDaylightRealTimeChange,
    colorCycleAddColor, colorCycleRemoveColor,
    compositeAddLayer, compositeRemoveLayer,
    mappedAddZone, mappedRemoveZone,
    onAudioVizChange,
    onGradientPresetChange,
    promptAndSaveGradientPreset,
    deleteAndRefreshGradientPreset,
    showGradientModal,
    closeGradientEditor,
    saveGradientEntity,
    cloneGradient,
    editGradient,
    deleteGradient,
    cloneColorStrip,
    toggleCSSOverlay,
    previewCSSFromEditor,
    copyEndpointUrl,
    onNotificationFilterModeChange,
    notificationAddAppOverride, notificationRemoveAppOverride,
    testNotification,
    showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
    testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
    addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange,
    mathWaveAddLayer, mathWaveRemoveLayer,
} from './features/color-strips/index.ts';

// Layer 5: audio sources
import {
    showAudioSourceModal, closeAudioSourceModal, saveAudioSource,
    editAudioSource, cloneAudioSource, deleteAudioSource,
    testAudioSource, closeTestAudioSourceModal,
    refreshAudioDevices,
} from './features/audio-sources.ts';

// Layer 5: audio processing templates
import {
    showAudioProcessingTemplateModal, closeAudioProcessingTemplateModal,
    saveAudioProcessingTemplate, editAudioProcessingTemplate,
    cloneAudioProcessingTemplate, deleteAudioProcessingTemplate,
    aptAddFilterFromSelect, aptToggleFilterExpand, aptRemoveFilter, aptUpdateFilterOption,
    renderAPTModalFilterList,
} from './features/audio-processing-templates.ts';

// Layer 5: value sources
import {
    showValueSourceModal, closeValueSourceModal, saveValueSource,
    editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
    onDaylightVSRealTimeChange,
    addSchedulePoint,
    addAnimatedColor, removeAnimatedColor,
    addColorSchedulePoint, removeColorSchedulePoint,
    testValueSource, closeTestValueSourceModal,
} from './features/value-sources.ts';

// Layer 5: calibration
import {
    showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration,
    updateOffsetSkipLock, updateCalibrationPreview,
    setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge,
    showCSSCalibration, toggleCalibrationOverlay,
} from './features/calibration.ts';
import {
    showAdvancedCalibration, closeAdvancedCalibration, saveAdvancedCalibration,
    addCalibrationLine, removeCalibrationLine, selectCalibrationLine, moveCalibrationLine,
    updateCalibrationLine, resetCalibrationView,
} from './features/advanced-calibration.ts';

// Layer 5.5: graph editor
import {
    loadGraphEditor,
    toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, toggleGraphFilterTypes, toggleGraphHelp, graphUndo, graphRedo,
    graphFitAll, graphZoomIn, graphZoomOut, graphRelayout,
    graphToggleFullscreen, graphAddEntity, toggleToolbarOverflow, closeToolbarOverflow,
} from './features/graph-editor.ts';

// Layer 6: tabs, navigation, command palette, settings
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.ts';
import { callTabLoader } from './core/tab-registry.ts';
import { navigateToCard } from './core/navigation.ts';
import { openCommandPalette, closeCommandPalette, initCommandPalette } from './core/command-palette.ts';
import {
    applyStylePreset, applyBgEffect, renderAppearanceTab, initAppearance,
} from './features/appearance.ts';
import {
    openSettingsModal, closeSettingsModal, switchSettingsTab,
    downloadBackup, handleRestoreFileSelected,
    saveAutoBackupSettings, triggerBackupNow, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
    restartServer,
    loadApiKeysList,
    connectLogViewer, disconnectLogViewer, clearLogViewer, applyLogFilter,
    openLogOverlay, closeLogOverlay,
    loadLogLevel, setLogLevel,
    saveExternalUrl, getBaseOrigin, loadExternalUrl,
} from './features/settings.ts';
import {
    loadUpdateStatus, initUpdateListener, checkForUpdates,
    loadUpdateSettings, saveUpdateSettings, dismissUpdate,
    initUpdateSettingsPanel, applyUpdate,
    openReleaseNotes, closeReleaseNotes,
} from './features/update.ts';
import {
    initDonationBanner, dismissDonation, snoozeDonation, renderAboutPanel, setProjectUrls,
} from './features/donation.ts';

// ─── Register all HTML onclick / onchange / onfocus globals ───

Object.assign(window, {
    // core / state (for inline script)
    setApiKey,

    // visual effects (called from inline <script>)
    _updateBgAnimAccent: updateBgAnimAccent,
    _updateBgAnimTheme: updateBgAnimTheme,
    _updateTabIndicator: updateTabIndicator,

    // core / ui
    toggleHint,
    lockBody,
    unlockBody,
    closeLightbox,
    showToast,
    showUndoToast,
    showConfirm,
    closeConfirmModal,
    openFullImageLightbox,
    showOverlaySpinner,
    hideOverlaySpinner,
    setFieldError,
    clearFieldError,
    setupBlurValidation,

    // core / api + i18n
    t,
    configureApiKey,
    loadServerInfo,
    loadDisplays,
    changeLocale,

    // displays
    openDisplayPicker,
    closeDisplayPicker,
    selectDisplay,
    formatDisplayLabel,

    // tutorials
    startCalibrationTutorial,
    startDeviceTutorial,
    startGettingStartedTutorial,
    startDashboardTutorial,
    startTargetsTutorial,
    startSourcesTutorial,
    startAutomationsTutorial,
    closeTutorial,
    tutorialNext,
    tutorialPrev,

    // devices
    showSettings,
    closeDeviceSettingsModal,
    forceCloseDeviceSettingsModal,
    saveDeviceSettings,
    updateBrightnessLabel,
    saveCardBrightness,
    turnOffDevice,
    pingDevice,
    removeDevice,
    loadDevices,
    updateSettingsBaudFpsHint,
    copyWsUrl,
    cloneDevice,

    // dashboard
    loadDashboard,
    dashboardToggleAutomation,
    dashboardStartTarget,
    dashboardStopTarget,
    dashboardStopAll,
    dashboardPauseClock,
    dashboardResumeClock,
    dashboardResetClock,
    toggleDashboardSection,
    changeDashboardPollInterval,
    stopUptimeTimer,
    startPerfPolling,
    stopPerfPolling,
    setPerfMode,

    // streams / capture templates / PP templates
    loadPictureSources,
    switchStreamTab,
    showAddTemplateModal,
    editTemplate,
    closeTemplateModal,
    saveTemplate,
    deleteTemplate,
    showTestTemplateModal,
    closeTestTemplateModal,
    onEngineChange,
    runTemplateTest,
    updateCaptureDuration,
    showAddStreamModal,
    editStream,
    closeStreamModal,
    saveStream,
    deleteStream,
    onStreamTypeChange,
    onStreamDisplaySelected,
    onTestDisplaySelected,
    showTestStreamModal,
    closeTestStreamModal,
    updateStreamTestDuration,
    runStreamTest,
    showTestPPTemplateModal,
    closeTestPPTemplateModal,
    updatePPTestDuration,
    runPPTemplateTest,
    showAddPPTemplateModal,
    editPPTemplate,
    closePPTemplateModal,
    savePPTemplate,
    deletePPTemplate,
    addFilterFromSelect,
    toggleFilterExpand,
    removeFilter,
    moveFilter,
    updateFilterOption,
    renderModalFilterList,
    cloneStream,
    cloneCaptureTemplate,
    clonePPTemplate,
    showAddCSPTModal,
    editCSPT,
    closeCSPTModal,
    saveCSPT,
    deleteCSPT,
    cloneCSPT,
    csptAddFilterFromSelect,
    csptToggleFilterExpand,
    csptRemoveFilter,
    csptUpdateFilterOption,
    renderCSPTModalFilterList,
    showAddAudioTemplateModal,
    editAudioTemplate,
    closeAudioTemplateModal,
    saveAudioTemplate,
    deleteAudioTemplate,
    cloneAudioTemplate,
    onAudioEngineChange,
    showTestAudioTemplateModal,
    closeTestAudioTemplateModal,
    startAudioTemplateTest,

    // pattern-templates (canvas editor — used by key_colors CSS source)
    showPatternTemplateEditor,
    closePatternTemplateModal,
    forceClosePatternTemplateModal,
    savePatternTemplate,
    renderPatternRectList,
    selectPatternRect,
    updatePatternRect,
    addPatternRect,
    deleteSelectedPatternRect,
    removePatternRect,
    capturePatternBackground,

    // automations
    loadAutomations,
    switchAutomationTab,
    openAutomationEditor,
    closeAutomationEditorModal,
    saveAutomationEditor,
    addAutomationRule,
    toggleAutomationEnabled,
    cloneAutomation,
    deleteAutomation,
    copyWebhookUrl,

    // scene presets (modal buttons stay on window; card actions migrated to event delegation)
    openScenePresetCapture,
    editScenePreset,
    saveScenePreset,
    closeScenePresetEditor,
    activateScenePreset,
    cloneScenePreset,
    deleteScenePreset,
    addSceneTarget,

    // integrations
    loadIntegrations,
    switchIntegrationTab,

    // game integration
    showGameIntegrationEditor,
    saveGameIntegration,
    closeGameIntegrationModal,
    cloneGameIntegration,
    deleteGameIntegration,
    addGameMapping,
    removeGameMapping,
    onMappingPresetChange,
    testGameConnection,
    showGameEventMonitor,
    openSetupInstructions,
    closeSetupInstructions,
    autoSetupGameIntegration,

    // device-discovery
    onDeviceTypeChanged,
    updateBaudFpsHint,
    onSerialPortFocus,
    showAddDevice,
    closeAddDeviceModal,
    scanForDevices,
    handleAddDevice,
    addGroupChild,
    addGroupChildSettings,

    // targets
    loadTargetsTab,
    switchTargetSubTab,
    showTargetEditor,
    closeTargetEditorModal,
    forceCloseTargetEditorModal,
    saveTargetEditor,
    startTargetProcessing,
    stopTargetProcessing,
    stopAllLedTargets,
    startTargetOverlay,
    stopTargetOverlay,
    deleteTarget,
    cloneTarget,
    toggleLedPreview,
    disconnectAllLedPreviewWS,

    // color-strip sources
    showCSSEditor,
    closeCSSEditorModal,
    forceCSSEditorClose,
    saveCSSEditor,
    deleteColorStrip,
    onCSSTypeChange,
    onEffectTypeChange,
    onEffectPaletteChange,
    onCSSClockChange,
    onAnimationTypeChange,
    onDaylightRealTimeChange,
    colorCycleAddColor,
    colorCycleRemoveColor,
    compositeAddLayer,
    compositeRemoveLayer,
    mappedAddZone,
    mappedRemoveZone,
    onAudioVizChange,
    onGradientPresetChange,
    promptAndSaveGradientPreset,
    deleteAndRefreshGradientPreset,
    showGradientModal,
    closeGradientEditor,
    saveGradientEntity,
    cloneGradient,
    editGradient,
    deleteGradient,
    cloneColorStrip,
    toggleCSSOverlay,
    previewCSSFromEditor,
    copyEndpointUrl,
    onNotificationFilterModeChange,
    notificationAddAppOverride, notificationRemoveAppOverride,
    testNotification,
    showNotificationHistory, closeNotificationHistory, refreshNotificationHistory,
    testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
    addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange,
    mathWaveAddLayer, mathWaveRemoveLayer,

    // audio sources
    showAudioSourceModal,
    closeAudioSourceModal,
    saveAudioSource,
    editAudioSource,
    cloneAudioSource,
    deleteAudioSource,
    testAudioSource,
    closeTestAudioSourceModal,
    refreshAudioDevices,

    // audio processing templates
    showAudioProcessingTemplateModal,
    closeAudioProcessingTemplateModal,
    saveAudioProcessingTemplate,
    editAudioProcessingTemplate,
    cloneAudioProcessingTemplate,
    deleteAudioProcessingTemplate,
    aptAddFilterFromSelect,
    aptToggleFilterExpand,
    aptRemoveFilter,
    aptUpdateFilterOption,
    renderAPTModalFilterList,

    // value sources
    showValueSourceModal,
    closeValueSourceModal,
    saveValueSource,
    editValueSource,
    cloneValueSource,
    deleteValueSource,
    onValueSourceTypeChange,
    onDaylightVSRealTimeChange,
    addSchedulePoint,
    addAnimatedColor,
    removeAnimatedColor,
    addColorSchedulePoint,
    removeColorSchedulePoint,
    testValueSource,
    closeTestValueSourceModal,

    // calibration
    showCalibration,
    closeCalibrationModal,
    forceCloseCalibrationModal,
    saveCalibration,
    updateOffsetSkipLock,
    updateCalibrationPreview,
    setStartPosition,
    toggleEdgeInputs,
    toggleDirection,
    toggleTestEdge,
    showCSSCalibration,
    toggleCalibrationOverlay,

    // advanced calibration
    showAdvancedCalibration,
    closeAdvancedCalibration,
    saveAdvancedCalibration,
    addCalibrationLine,
    removeCalibrationLine,
    selectCalibrationLine,
    moveCalibrationLine,
    updateCalibrationLine,
    resetCalibrationView,

    // graph editor
    loadGraphEditor,
    toggleGraphLegend,
    toggleGraphMinimap,
    toggleGraphFilter,
    toggleGraphFilterTypes,
    toggleGraphHelp,
    graphUndo,
    graphRedo,
    graphFitAll,
    graphZoomIn,
    graphZoomOut,
    graphRelayout,
    graphToggleFullscreen,
    graphAddEntity,
    toggleToolbarOverflow,
    closeToolbarOverflow,

    // tabs / navigation / command palette
    switchTab,
    startAutoRefresh,
    navigateToCard,
    openCommandPalette,
    closeCommandPalette,

    // settings (tabs / backup / restore / auto-backup / MQTT / api keys / log level)
    openSettingsModal,
    closeSettingsModal,
    switchSettingsTab,
    downloadBackup,
    handleRestoreFileSelected,
    saveAutoBackupSettings,
    triggerBackupNow,
    restoreSavedBackup,
    downloadSavedBackup,
    deleteSavedBackup,
    restartServer,
    loadApiKeysList,
    connectLogViewer,
    disconnectLogViewer,
    clearLogViewer,
    applyLogFilter,
    openLogOverlay,
    closeLogOverlay,
    loadLogLevel,
    setLogLevel,
    saveExternalUrl,
    getBaseOrigin,

    // update
    checkForUpdates,
    loadUpdateSettings,
    saveUpdateSettings,
    dismissUpdate,
    initUpdateSettingsPanel,
    applyUpdate,
    openReleaseNotes,
    closeReleaseNotes,

    // donation
    dismissDonation,
    snoozeDonation,
    renderAboutPanel,

    // appearance
    applyStylePreset,
    applyBgEffect,
    renderAppearanceTab,
});

// ─── Global keyboard shortcuts ───

document.addEventListener('keydown', (e) => {
    const tag = document.activeElement?.tagName;
    const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';

    // Command palette: Ctrl+K / Cmd+K (works even in inputs)
    if ((e.ctrlKey || e.metaKey) && e.key === 'k' && !e.altKey && !e.shiftKey) {
        e.preventDefault();
        openCommandPalette();
        return;
    }

    // Tab shortcuts: Ctrl+1..4 (skip when typing in inputs)
    if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
        const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'integrations', '6': 'graph' };
        const tab = tabMap[e.key];
        if (tab) {
            e.preventDefault();
            switchTab(tab);
            return;
        }
    }

    if (e.key === 'Escape') {
        // Close in order: log overlay > overlay lightboxes > modals via stack
        const logOverlay = queryEl('log-overlay');
        if (logOverlay && logOverlay.style.display !== 'none') {
            closeLogOverlay();
        } else if (queryEl('display-picker-lightbox')?.classList.contains('active')) {
            closeDisplayPicker();
        } else if (queryEl('image-lightbox')?.classList.contains('active')) {
            closeLightbox();
        } else {
            Modal.closeTopmost();
        }
    }
});

// ─── Browser back/forward via hash routing ───

window.addEventListener('popstate', handlePopState);

// ─── Cleanup on page unload ───

window.addEventListener('beforeunload', () => {
    if (refreshInterval) {
        clearInterval(refreshInterval);
    }
    stopConnectionMonitor();
    stopEventsWS();
    disconnectAllLedPreviewWS();
});

// ─── Initialization ───

document.addEventListener('DOMContentLoaded', async () => {
  try {
    // Load API key from localStorage before anything that triggers API calls
    setApiKey(localStorage.getItem('ledgrab_api_key'));

    // Initialize locale (dispatches languageChanged which may trigger API calls)
    await initLocale();

    // Probe /health first so we know whether the server has API keys configured
    // AND whether this client is loopback or LAN. The result (setup_required
    // and auth_required flags) gates every subsequent call; without it, a LAN
    // client without keys flashes a useless login modal before the setup
    // screen can take over.
    await loadServerInfo();

    // Load external URL setting early so getBaseOrigin() is available for card
    // rendering — but skip when the server has no keys for LAN access, as the
    // call would just 401 and trigger the login modal behind the setup screen.
    if (!setupRequired) {
        loadExternalUrl();
    }

    // Restore active tab before showing content to avoid visible jump
    initTabs();

    // Show content now that translations are loaded and tabs are set
    document.body.style.visibility = 'visible';

    // Initialize visual effects
    initCardGlare();
    initBgAnim();
    initBgShaders();
    initAppearance();
    initTabIndicator();
    updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light');
    const accent = localStorage.getItem('accentColor') || '#4CAF50';
    updateBgAnimAccent(accent);

    // App-level listeners share a single AbortController. The app currently
    // never tears these down (it lives for the lifetime of the page), but the
    // pattern documents that future SPA-style teardown should call abort().
    const appListeners = new AbortController();
    const { signal: appSignal } = appListeners;

    // Set CSS variable for sticky header height (header now includes tab bar)
    const headerEl = document.querySelector('header');
    if (headerEl) {
        const updateHeaderHeight = () => {
            const hh = headerEl.offsetHeight;
            document.documentElement.style.setProperty('--header-height', hh + 'px');
            document.documentElement.style.setProperty('--sticky-top', hh + 'px');
        };
        updateHeaderHeight();
        window.addEventListener('resize', updateHeaderHeight, { signal: appSignal });
    }

    // Scroll-to-top button visibility
    const scrollBtn = document.getElementById('scroll-to-top');
    if (scrollBtn) {
        window.addEventListener('scroll', () => {
            scrollBtn.classList.toggle('visible', window.scrollY > 300);
        }, { passive: true, signal: appSignal });
    }

    // Initialize command palette
    initCommandPalette();

    // Enhance lightbox FPS <select> with IconSelect
    initLightbox();

    // Setup form handler
    const addDeviceForm = queryEl('add-device-form');
    if (addDeviceForm) addDeviceForm.addEventListener('submit', handleAddDevice);

    // Keep monitoring server connection (initial /health ran earlier).
    startConnectionMonitor();

    // Expose auth state for inline scripts (after loadServerInfo sets it)
    (window as any)._authRequired = authRequired;
    if (typeof window.updateAuthUI === 'function') window.updateAuthUI();

    // Server is unconfigured for LAN access → setup screen already shown by
    // loadServerInfo. Skip login modal and data loads; the user can't do
    // anything until they configure keys on the server.
    if (setupRequired) {
        return;
    }

    // Show login modal only when auth is enabled and no API key is stored
    if (authRequired && !apiKey) {
        setTimeout(() => {
            if (typeof window.showApiKeyModal === 'function') {
                window.showApiKeyModal(null, true);
            }
        }, 100);
        return;
    }

    // User is logged in, load data
    loadDisplays();
    loadTargetsTab();

    // Trigger the active tab's loader — initTabs() ran before authRequired
    // was known, so its conditional loader call may have been skipped.
    const activeTab = localStorage.getItem('activeTab') || 'dashboard';
    if (activeTab !== 'targets') {
        callTabLoader(activeTab);
    }

    // Start global events WebSocket and auto-refresh
    startEventsWS();
    startEntityEventListeners();
    startAutoRefresh();

    // Initialize update checker (banner + WS listener)
    initUpdateListener();
    loadUpdateStatus();

    // Show donation banner (after a few sessions)
    setProjectUrls(serverRepoUrl, serverDonateUrl);
    initDonationBanner();

    // Show getting-started tutorial on first visit
    if (!localStorage.getItem('tour_completed')) {
        setTimeout(() => startGettingStartedTutorial(), 600);
    }
  } catch (err) {
    // Top-level init failure should not silently leave the user with a
    // half-loaded UI — surface a toast and log a stack to DevTools.
    console.error('App init failed:', err);
    try {
        showToast(`Init failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
    } catch {/* showToast itself failed; nothing else to do */}
  }
});
