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

// Layer 0: state
import { apiKey, setApiKey, authRequired, refreshInterval } 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,
} 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, kc-targets, 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 {
    createKCTargetCard, testKCTarget,
    showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
    deleteKCTarget, disconnectAllKCWebSockets,
    updateKCBrightnessLabel, saveKCBrightness,
    cloneKCTarget,
} from './features/kc-targets.ts';
import {
    createPatternTemplateCard,
    showPatternTemplateEditor, closePatternTemplateModal, forceClosePatternTemplateModal,
    savePatternTemplate, deletePatternTemplate,
    renderPatternRectList, selectPatternRect, updatePatternRect,
    addPatternRect, deleteSelectedPatternRect, removePatternRect,
    capturePatternBackground,
    clonePatternTemplate,
} from './features/pattern-templates.ts';
import {
    loadAutomations, switchAutomationTab, openAutomationEditor, closeAutomationEditorModal,
    saveAutomationEditor, addAutomationCondition,
    toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
} from './features/automations.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,
} from './features/device-discovery.ts';
import {
    loadTargetsTab, switchTargetSubTab,
    showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
    startTargetProcessing, stopTargetProcessing,
    stopAllLedTargets, stopAllKCTargets,
    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,
} from './features/color-strips.ts';

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

// Layer 5: value sources
import {
    showValueSourceModal, closeValueSourceModal, saveValueSource,
    editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
    onDaylightVSRealTimeChange,
    addSchedulePoint,
    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,
} from './features/graph-editor.ts';

// Layer 6: tabs, navigation, command palette, settings
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.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, saveMqttSettings,
    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,

    // kc-targets
    createKCTargetCard,
    testKCTarget,
    showKCEditor,
    closeKCEditorModal,
    forceCloseKCEditorModal,
    saveKCEditor,
    deleteKCTarget,
    disconnectAllKCWebSockets,
    updateKCBrightnessLabel,
    saveKCBrightness,
    cloneKCTarget,

    // pattern-templates
    createPatternTemplateCard,
    showPatternTemplateEditor,
    closePatternTemplateModal,
    forceClosePatternTemplateModal,
    savePatternTemplate,
    deletePatternTemplate,
    renderPatternRectList,
    selectPatternRect,
    updatePatternRect,
    addPatternRect,
    deleteSelectedPatternRect,
    removePatternRect,
    capturePatternBackground,
    clonePatternTemplate,

    // automations
    loadAutomations,
    switchAutomationTab,
    openAutomationEditor,
    closeAutomationEditorModal,
    saveAutomationEditor,
    addAutomationCondition,
    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,

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

    // targets
    loadTargetsTab,
    switchTargetSubTab,
    showTargetEditor,
    closeTargetEditorModal,
    forceCloseTargetEditorModal,
    saveTargetEditor,
    startTargetProcessing,
    stopTargetProcessing,
    stopAllLedTargets,
    stopAllKCTargets,
    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,

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

    // value sources
    showValueSourceModal,
    closeValueSourceModal,
    saveValueSource,
    editValueSource,
    cloneValueSource,
    deleteValueSource,
    onValueSourceTypeChange,
    onDaylightVSRealTimeChange,
    addSchedulePoint,
    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,

    // 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,
    saveMqttSettings,
    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': '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();
    disconnectAllKCWebSockets();
    disconnectAllLedPreviewWS();
});

// ─── Initialization ───

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

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

    // Load external URL setting early so getBaseOrigin() is available for card rendering
    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);

    // 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);
    }

    // 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 });
    }

    // Initialize command palette
    initCommandPalette();

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

    // Always monitor server connection (even before login)
    await loadServerInfo();
    startConnectionMonitor();

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

    // 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();

    // 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);
    }
});
