class DashboardApp {
    constructor() {
        this.locale = this.resolveLocale();
        this.translations = this.buildTranslations();

        this.elements = {
            loginOverlay: document.getElementById('loginOverlay'),
            loginForm: document.getElementById('loginForm'),
            loginButton: document.getElementById('loginButton'),
            loginError: document.getElementById('loginError'),
            username: document.getElementById('username'),
            password: document.getElementById('password'),
            brandLogo: document.getElementById('brandLogo'),
            statusDot: document.getElementById('statusDot'),
            connectionStatus: document.getElementById('connectionStatus'),
            sessionUsername: document.getElementById('sessionUsername'),
            logoutButton: document.getElementById('logoutButton'),
            easterEggOverlay: document.getElementById('easterEggOverlay'),
            easterEggCanvas: document.getElementById('easterEggCanvas'),
            easterEggClose: document.getElementById('easterEggClose'),
            easterEggScore: document.getElementById('easterEggScore'),
            chargerTableBody: document.getElementById('chargerTableBody'),
            chargerEmptyRow: document.getElementById('chargerEmptyRow'),
            fetchSessionsButton: document.getElementById('fetchSessionsButton'),
            downloadSessionsButton: document.getElementById('downloadSessionsButton'),
            chargerFilter: document.getElementById('chargerFilter'),
            carFilter: document.getElementById('carFilter'),
            sessionStartFilter: document.getElementById('sessionStartFilter'),
            sessionEndFilter: document.getElementById('sessionEndFilter'),
            chargingSessionsTableBody: document.getElementById('chargingSessionsTableBody'),
            chargingSessionsEmptyRow: document.getElementById('chargingSessionsEmptyRow'),
            panelButtons: Array.from(document.querySelectorAll('[data-panel-target]')),
            contentPanels: Array.from(document.querySelectorAll('[data-panel]'))
        };

        this.sessionKey = 'evdash.session';
        this.socket = null;
        this.token = null;
        this.tokenExpiry = null;
        this.username = null;
        this.pendingRequests = new Map();
        this.reconnectTimer = null;
        this.tokenRefreshTimer = null;
        this.refreshInFlight = false;
        this.chargers = new Map();
        this.expandedChargers = new Set();
        this.cars = new Map();
        this.sessions = [];
        this.activePanel = null;
        this.easterEggClickCount = 0;
        this.easterEggClickResetTimer = null;
        this.easterEggGame = {
            running: false,
            frameId: null,
            player: { x: 30, y: 30, size: 16, speed: 3.2 },
            target: { x: 200, y: 140, size: 10 },
            score: 0,
            keys: {}
        };
        this.chargerColumns = [
            { key: 'id', label: 'ID', hidden: true },
            { key: 'name', label: 'Name' },
            { key: 'assignedCar', label: 'Car' },
            { key: 'energyManagerMode', label: 'Energy manager mode' },
            { key: 'connected', label: 'Reachable' },
            { key: 'status', label: 'Status' },
            { key: 'chargingCurrent', label: 'Charging current' },
            { key: 'chargingPhases', label: 'Charging phases' },
            { key: 'currentPower', label: 'Current power' },
            { key: 'sessionEnergy', label: 'Session energy' }
        ];

        this.translateDocument();
        this.updateEasterEggScore();
        this.attachEventListeners();
        this.initializePanelNavigation();
        this.restoreSession();
        this.toggleChargerEmptyState();
        this.updateChargerSelector();
        this.updateCarSelector();
    }

    resolveLocale() {
        const overrideKey = 'evdash.language';
        try {
            const stored = window.localStorage.getItem(overrideKey);
            if (stored && typeof stored === 'string')
                return stored.trim().toLowerCase().startsWith('de') ? 'de' : 'en';
        } catch (error) {
            // ignore
        }

        const candidates = Array.isArray(navigator.languages) && navigator.languages.length
            ? navigator.languages
            : [navigator.language || 'en'];

        const normalized = candidates
            .filter(Boolean)
            .map(value => String(value).toLowerCase());

        if (normalized.some(value => value === 'de' || value.startsWith('de-')))
            return 'de';

        return 'en';
    }

    buildTranslations() {
        return {
            en: {
                'header.tagline': 'Monitor & troubleshoot EV chargers.',
                'header.awaitingLogin': 'Awaiting login…',
                'header.authenticateHint': 'Load the dashboard to authenticate.',
                'header.logout': 'Logout',

                'sidebar.dashboardSections': 'Dashboard sections',
                'sidebar.sections': 'Sections',
                'sidebar.workspace': 'Workspace',
                'sidebar.overview': 'Overview',
                'sidebar.chargers': 'Chargers',
                'sidebar.chargersSubtitle': 'Live table & telemetry',
                'sidebar.sessions': 'Charging sessions',
                'sidebar.sessionsSubtitle': 'History of charging sessions',
                'sidebar.help': 'Help',
                'sidebar.helpSubtitle': 'How to use the dashboard',
                'sidebar.version': 'Version',

                'chargers.liveOverview': 'Live overview',
                'chargers.title': 'Chargers',
                'chargers.empty': 'No chargers loaded yet.',
                'chargers.columns.name': 'Name',
                'chargers.columns.car': 'Car',
                'chargers.columns.energyManagerMode': 'Energy manager mode',
                'chargers.columns.connected': 'Reachable',
                'chargers.columns.status': 'Status',
                'chargers.columns.lastStatusUpdate': 'Last status update',
                'chargers.columns.chargingCurrent': 'Charging current',
                'chargers.columns.chargingPhases': 'Charging phases',
                'chargers.columns.currentPower': 'Current power',
                'chargers.columns.sessionEnergy': 'Session energy',
                'chargers.columns.version': 'Version',
                'chargers.columns.temperature': 'Temperature',
                'chargers.columns.digitalInputMode': 'Digital input',
                'chargerStatus.Init': 'Initializing',
                'chargerStatus.A1': 'Charger ready',
                'chargerStatus.A2': 'Charger ready',
                'chargerStatus.B1': 'Car connected, autorization required',
                'chargerStatus.B2': 'Car connected',
                'chargerStatus.C1': 'Charging pause, car ready',
                'chargerStatus.C2': 'Charging',
                'chargerStatus.F': 'Error',

                'sessions.history': 'History',
                'sessions.title': 'Charging sessions',
                'sessions.filterCharger': 'Charger',
                'sessions.allChargers': 'All chargers',
                'sessions.filterCar': 'Car',
                'sessions.allCars': 'All cars',
                'sessions.filterStartDate': 'Start date',
                'sessions.filterEndDate': 'End date',
                'sessions.fetch': 'Refresh view',
                'sessions.downloadCsv': 'Download CSV',
                'sessions.helper': 'Optionally filter charging sessions by charger, car and time range before downloading.',
                'sessions.columns.name': 'Name',
                'sessions.columns.charger': 'Charger',
                'sessions.columns.car': 'Car',
                'sessions.columns.start': 'Start',
                'sessions.columns.end': 'End',
                'sessions.columns.energy': 'Energy (kWh)',
                'sessions.emptyFetched': 'No charging sessions fetched yet.',
                'sessions.noneFound': 'No charging sessions found.',
                'sessions.noneInRange': 'No charging sessions match the selected time range.',
                'sessions.noneMatchingFilters': 'No charging sessions match the selected filters.',
                'sessions.fetchFailed': 'Failed to fetch charging sessions.',
                'sessions.requestFailed': 'Unable to request charging sessions. Check the connection status.',
                'sessions.displayFailed': 'Unable to display charging sessions.',
                'sessions.startBeforeEnd': 'Start date must be earlier than end date.',
                'sessions.sessionIdLabel': 'Session {id}',

                'help.guides': 'Guides',
                'help.title': 'Using EV Dash',
                'help.helper': 'This page explains the main areas of the dashboard and the most common actions.',
                'help.gettingStartedTitle': 'Getting started',
                'help.gettingStartedStep1': 'Sign in with your user name and password.',
                'help.gettingStartedStep2': 'Wait until the status in the header changes to Connected.',
                'help.gettingStartedStep3': 'Open the Chargers section to see the live status of all available chargers.',
                'help.gettingStartedStep4': 'Select a charger row to open more details for that charger.',
                'help.connectionTitle': 'Connection status',
                'help.connectionBullet1': 'Connected means live values are up to date.',
                'help.connectionBullet2': 'Connecting or Authenticating means the dashboard is restoring access.',
                'help.connectionBullet3': 'Disconnected or Connection error means live updates are temporarily unavailable.',
                'help.connectionBullet4': 'If needed, sign in again or ask your administrator for support.',
                'help.liveView': 'Live view',
                'help.chargersTitle': 'Understand the charger overview',
                'help.chargersHelper': 'The Chargers section shows the current state of each charger in one place.',
                'help.chargersBullet1': 'Name and Car help you identify the charger and the assigned vehicle.',
                'help.chargersBullet2': 'Energy manager mode shows whether charging is running in quick, eco or timed operation.',
                'help.chargersBullet3': 'Reachable shows whether the charger is currently available.',
                'help.chargersBullet4': 'Status shows whether the charger is ready, a car is connected or charging is active.',
                'help.chargersBullet5': 'Charging current, phases and current power describe the live charging performance.',
                'help.chargersBullet6': 'Session energy shows how much energy has been delivered in the current charging session.',
                'help.history': 'History',
                'help.sessionsTitle': 'Use charging sessions',
                'help.sessionsHelper': 'The Charging sessions section helps you review past charging activity and export it.',
                'help.sessionsStep1': 'Open Charging sessions to load the session history.',
                'help.sessionsStep2': 'Use the car filter if you only want to see sessions for one vehicle.',
                'help.sessionsStep3': 'Set a start date and end date to narrow the time range.',
                'help.sessionsStep4': 'Select Refresh view to update the table.',
                'help.sessionsStep5': 'Select Download CSV to export the currently shown session list.',
                'help.everyday': 'Everyday tasks',
                'help.commonTasksTitle': 'Common things you can do',
                'help.commonTaskBullet1': 'Check whether a vehicle is connected, waiting or actively charging.',
                'help.commonTaskBullet2': 'Compare the live power and charging current of multiple chargers.',
                'help.commonTaskBullet3': 'See how much energy was delivered during the current session.',
                'help.commonTaskBullet4': 'Look up earlier charging sessions for a specific car or time period.',
                'help.commonTaskBullet5': 'Download the visible charging history as a CSV file for further use.',

                'easterEgg.hiddenTreat': 'Hidden treat',
                'easterEgg.title': 'Grid Dash',
                'easterEgg.close': 'Close',
                'easterEgg.instructions': 'Use arrow keys or WASD to drive the tiny EV and catch lightning bolts. Press Esc or close to exit.',
                'easterEgg.score': 'Score: {score}',
                'easterEgg.hint': 'Stay inside the grid!',
                'easterEgg.canvasLabel': 'Mini game canvas',

                'login.title': 'Sign in',
                'login.required': 'Authorization required.',
                'login.username': 'Username',
                'login.password': 'Password',
                'login.helper': 'Contact the administrator to receive valid credentials.',
                'login.signIn': 'Sign in',
                'login.signingIn': 'Signing in…',
                'login.emptyCredentials': 'Username and password are required.',
                'login.failed': 'Login failed. Please try again.',
                'login.networkError': 'Unable to reach the login endpoint. Please check your connection.',
                'login.unexpectedResponse': 'Received an unexpected response from the server.',
                'login.invalidResponse': 'Invalid response from server.',
                'login.invalidRequest': 'The login request was malformed. Please reload the page and try again.',
                'login.unauthorized': 'The provided credentials were not accepted.',

                'connection.connecting': 'Connecting…',
                'connection.authenticating': 'Authenticating…',
                'connection.connected': 'Connected',
                'connection.disconnected': 'Disconnected',
                'connection.error': 'Connection error',
                'connection.authenticationRequired': 'Authentication required',
                'connection.loggedOut': 'Logged out',
                'connection.sessionExpired': 'Session expired. Please sign in again.',
                'connection.sessionExpiredRestore': 'Your session has expired. Please sign in again.',
                'connection.restoreFailed': 'We could not restore your previous session. Please sign in again.',
                'connection.loggedOutOverlay': 'You have been logged out.',
                'connection.authFailed': 'Authentication failed. Please try again.',

                'value.true': 'True',
                'value.false': 'False',
                'value.yes': 'Yes',
                'value.no': 'No',
                'value.unknownWithValue': 'Unknown ({value})',

                'energyManagerMode.quick': 'Quick',
                'energyManagerMode.eco': 'Eco',
                'energyManagerMode.ecoTime': 'Eco + Time',

                'digitalInputMode.chargingAllowed': 'Charging allowed',
                'digitalInputMode.chargingAllowedInverted': 'Charging allowed inverted',
                'digitalInputMode.pwmAndS0': 'PWM and S0 signaling',
                'digitalInputMode.limitAndS0': 'Limit and S0 signaling',

                'csv.sessionId': 'Session ID',
                'csv.chargerName': 'Charger name',
                'csv.chargerSerialNumber': 'Charger serial number',
                'csv.car': 'Car',
                'csv.start': 'Start',
                'csv.end': 'End',
                'csv.energyKwh': 'Energy [kWh]',
                'csv.meterStartKwh': 'Meter start [kWh]',
                'csv.meterEndKwh': 'Meter end [kWh]',

                'ws.parseFailed': 'Failed to parse message: {error}'
            },
            de: {
                'header.tagline': 'Überwachen & Fehleranalyse von EV-Ladestationen.',
                'header.awaitingLogin': 'Warte auf Anmeldung…',
                'header.authenticateHint': 'Dashboard laden, um dich zu authentifizieren.',
                'header.logout': 'Abmelden',

                'sidebar.dashboardSections': 'Dashboard-Bereiche',
                'sidebar.sections': 'Bereiche',
                'sidebar.workspace': 'Arbeitsbereich',
                'sidebar.overview': 'Übersicht',
                'sidebar.chargers': 'Ladestationen',
                'sidebar.chargersSubtitle': 'Live-Tabelle & Telemetrie',
                'sidebar.sessions': 'Ladevorgänge',
                'sidebar.sessionsSubtitle': 'Historie der Ladevorgänge',
                'sidebar.help': 'Hilfe',
                'sidebar.helpSubtitle': 'So nutzt du das Dashboard',
                'sidebar.version': 'Version',

                'chargers.liveOverview': 'Live-Übersicht',
                'chargers.title': 'Ladestationen',
                'chargers.empty': 'Noch keine Ladestationen geladen.',
                'chargers.columns.name': 'Name',
                'chargers.columns.car': 'Fahrzeug',
                'chargers.columns.energyManagerMode': 'Energiemanager-Modus',
                'chargers.columns.connected': 'Erreichbar',
                'chargers.columns.status': 'Status',
                'chargers.columns.lastStatusUpdate': 'Letzte Statusaktualisierung',
                'chargers.columns.chargingCurrent': 'Ladestrom',
                'chargers.columns.chargingPhases': 'Ladephasen',
                'chargers.columns.currentPower': 'Aktuelle Leistung',
                'chargers.columns.sessionEnergy': 'Energie (Sitzung)',
                'chargers.columns.version': 'Version',
                'chargers.columns.temperature': 'Temperatur',
                'chargers.columns.digitalInputMode': 'Digitaler Eingang',
                'chargerStatus.Init': 'Initialisierung',
                'chargerStatus.A1': 'Ladestation bereit',
                'chargerStatus.A2': 'Ladestation bereit',
                'chargerStatus.B1': 'Fahrzeug verbunden, Autorisierung erforderlich',
                'chargerStatus.B2': 'Fahrzeug verbunden',
                'chargerStatus.C1': 'Ladepause, Fahrzeug bereit',
                'chargerStatus.C2': 'Laden',
                'chargerStatus.F': 'Fehler',

                'sessions.history': 'Historie',
                'sessions.title': 'Ladevorgänge',
                'sessions.filterCharger': 'Ladestation',
                'sessions.allChargers': 'Alle Ladestationen',
                'sessions.filterCar': 'Fahrzeug',
                'sessions.allCars': 'Alle Fahrzeuge',
                'sessions.filterStartDate': 'Startdatum',
                'sessions.filterEndDate': 'Enddatum',
                'sessions.fetch': 'Ansicht aktualisieren',
                'sessions.downloadCsv': 'CSV herunterladen',
                'sessions.helper': 'Optional nach Ladestation, Fahrzeug und Zeitraum filtern, bevor du die CSV herunterlädst.',
                'sessions.columns.name': 'Name',
                'sessions.columns.charger': 'Ladestation',
                'sessions.columns.car': 'Fahrzeug',
                'sessions.columns.start': 'Start',
                'sessions.columns.end': 'Ende',
                'sessions.columns.energy': 'Energie (kWh)',
                'sessions.emptyFetched': 'Noch keine Ladevorgänge geladen.',
                'sessions.noneFound': 'Keine Ladevorgänge gefunden.',
                'sessions.noneInRange': 'Keine Ladevorgänge im ausgewählten Zeitraum.',
                'sessions.noneMatchingFilters': 'Keine Ladevorgänge entsprechen den ausgewählten Filtern.',
                'sessions.fetchFailed': 'Ladevorgänge konnten nicht geladen werden.',
                'sessions.requestFailed': 'Ladevorgänge konnten nicht angefragt werden. Bitte Verbindungsstatus prüfen.',
                'sessions.displayFailed': 'Ladevorgänge können nicht angezeigt werden.',
                'sessions.startBeforeEnd': 'Das Startdatum muss vor dem Enddatum liegen.',
                'sessions.sessionIdLabel': 'Sitzung {id}',

                'help.guides': 'Leitfäden',
                'help.title': 'EV Dash verwenden',
                'help.helper': 'Diese Seite erklärt die wichtigsten Bereiche des Dashboards und die häufigsten Aktionen.',
                'help.gettingStartedTitle': 'Erste Schritte',
                'help.gettingStartedStep1': 'Melde dich mit deinem Benutzernamen und Passwort an.',
                'help.gettingStartedStep2': 'Warte, bis der Status in der Kopfzeile auf Verbunden wechselt.',
                'help.gettingStartedStep3': 'Öffne den Bereich Ladestationen, um den Live-Status aller verfügbaren Ladestationen zu sehen.',
                'help.gettingStartedStep4': 'Wähle eine Zeile aus, um mehr Details zu dieser Ladestation zu öffnen.',
                'help.connectionTitle': 'Verbindungsstatus',
                'help.connectionBullet1': 'Verbunden bedeutet, dass die Live-Werte aktuell sind.',
                'help.connectionBullet2': 'Verbinden oder Authentifizierung bedeutet, dass das Dashboard den Zugriff wiederherstellt.',
                'help.connectionBullet3': 'Getrennt oder Verbindungsfehler bedeutet, dass Live-Aktualisierungen vorübergehend nicht verfügbar sind.',
                'help.connectionBullet4': 'Falls nötig, melde dich erneut an oder frage deinen Administrator um Unterstützung.',
                'help.liveView': 'Live-Ansicht',
                'help.chargersTitle': 'Die Ladestations-Übersicht verstehen',
                'help.chargersHelper': 'Im Bereich Ladestationen siehst du den aktuellen Zustand jeder Ladestation an einem Ort.',
                'help.chargersBullet1': 'Name und Fahrzeug helfen dir, die Ladestation und das zugeordnete Fahrzeug schnell zu erkennen.',
                'help.chargersBullet2': 'Der Energiemanager-Modus zeigt, ob schnell, im Eco-Modus oder zeitgesteuert geladen wird.',
                'help.chargersBullet3': 'Erreichbar zeigt, ob die Ladestation aktuell verfügbar ist.',
                'help.chargersBullet4': 'Status zeigt, ob die Ladestation bereit ist, ein Fahrzeug verbunden ist oder gerade geladen wird.',
                'help.chargersBullet5': 'Ladestrom, Ladephasen und aktuelle Leistung beschreiben die momentane Ladeleistung.',
                'help.chargersBullet6': 'Energie (Sitzung) zeigt, wie viel Energie in der laufenden Sitzung bereits geladen wurde.',
                'help.history': 'Historie',
                'help.sessionsTitle': 'Ladevorgänge nutzen',
                'help.sessionsHelper': 'Im Bereich Ladevorgänge kannst du frühere Ladevorgänge prüfen und exportieren.',
                'help.sessionsStep1': 'Öffne Ladevorgänge, um die Verlaufsliste zu laden.',
                'help.sessionsStep2': 'Nutze den Fahrzeugfilter, wenn du nur Vorgänge eines bestimmten Fahrzeugs sehen möchtest.',
                'help.sessionsStep3': 'Setze Start- und Enddatum, um den Zeitraum einzugrenzen.',
                'help.sessionsStep4': 'Wähle Ansicht aktualisieren, um die Tabelle neu zu laden.',
                'help.sessionsStep5': 'Wähle CSV herunterladen, um die aktuell angezeigte Liste zu exportieren.',
                'help.everyday': 'Alltag',
                'help.commonTasksTitle': 'Häufige Aufgaben',
                'help.commonTaskBullet1': 'Prüfen, ob ein Fahrzeug verbunden ist, wartet oder aktiv lädt.',
                'help.commonTaskBullet2': 'Die aktuelle Leistung und den Ladestrom mehrerer Ladestationen vergleichen.',
                'help.commonTaskBullet3': 'Sehen, wie viel Energie in der aktuellen Sitzung geladen wurde.',
                'help.commonTaskBullet4': 'Frühere Ladevorgänge für ein bestimmtes Fahrzeug oder einen Zeitraum nachschlagen.',
                'help.commonTaskBullet5': 'Die sichtbare Ladehistorie als CSV für die weitere Nutzung herunterladen.',

                'easterEgg.hiddenTreat': 'Verstecktes Extra',
                'easterEgg.title': 'Grid Dash',
                'easterEgg.close': 'Schließen',
                'easterEgg.instructions': 'Mit Pfeiltasten oder WASD fahren, Blitze einsammeln. Mit Esc oder Schließen beenden.',
                'easterEgg.score': 'Punkte: {score}',
                'easterEgg.hint': 'Bleib im Raster!',
                'easterEgg.canvasLabel': 'Mini-Spiel-Leinwand',

                'login.title': 'Anmelden',
                'login.required': 'Autorisierung erforderlich.',
                'login.username': 'Benutzername',
                'login.password': 'Passwort',
                'login.helper': 'Wende dich an den Administrator, um gültige Zugangsdaten zu erhalten.',
                'login.signIn': 'Anmelden',
                'login.signingIn': 'Anmeldung…',
                'login.emptyCredentials': 'Benutzername und Passwort sind erforderlich.',
                'login.failed': 'Anmeldung fehlgeschlagen. Bitte erneut versuchen.',
                'login.networkError': 'Login-Endpunkt nicht erreichbar. Bitte Verbindung prüfen.',
                'login.unexpectedResponse': 'Unerwartete Antwort vom Server erhalten.',
                'login.invalidResponse': 'Ungültige Serverantwort.',
                'login.invalidRequest': 'Die Login-Anfrage war fehlerhaft. Bitte Seite neu laden und erneut versuchen.',
                'login.unauthorized': 'Die eingegebenen Zugangsdaten wurden nicht akzeptiert.',

                'connection.connecting': 'Verbinden…',
                'connection.authenticating': 'Authentifizierung…',
                'connection.connected': 'Verbunden',
                'connection.disconnected': 'Getrennt',
                'connection.error': 'Verbindungsfehler',
                'connection.authenticationRequired': 'Authentifizierung erforderlich',
                'connection.loggedOut': 'Abgemeldet',
                'connection.sessionExpired': 'Sitzung abgelaufen. Bitte erneut anmelden.',
                'connection.sessionExpiredRestore': 'Deine Sitzung ist abgelaufen. Bitte erneut anmelden.',
                'connection.restoreFailed': 'Die vorige Sitzung konnte nicht wiederhergestellt werden. Bitte erneut anmelden.',
                'connection.loggedOutOverlay': 'Du wurdest abgemeldet.',
                'connection.authFailed': 'Authentifizierung fehlgeschlagen. Bitte erneut versuchen.',

                'value.true': 'Wahr',
                'value.false': 'Falsch',
                'value.yes': 'Ja',
                'value.no': 'Nein',
                'value.unknownWithValue': 'Unbekannt ({value})',

                'energyManagerMode.quick': 'Schnell',
                'energyManagerMode.eco': 'Eco',
                'energyManagerMode.ecoTime': 'Eco + Zeit',

                'digitalInputMode.chargingAllowed': 'Laden erlaubt',
                'digitalInputMode.chargingAllowedInverted': 'Laden erlaubt (invertiert)',
                'digitalInputMode.pwmAndS0': 'PWM- und S0-Signalisierung',
                'digitalInputMode.limitAndS0': 'Limit- und S0-Signalisierung',

                'csv.sessionId': 'Sitzungs-ID',
                'csv.chargerName': 'Ladestationsname',
                'csv.chargerSerialNumber': 'Seriennummer der Ladestation',
                'csv.car': 'Fahrzeug',
                'csv.start': 'Start',
                'csv.end': 'Ende',
                'csv.energyKwh': 'Energie [kWh]',
                'csv.meterStartKwh': 'Zählerstand Start [kWh]',
                'csv.meterEndKwh': 'Zählerstand Ende [kWh]',

                'ws.parseFailed': 'Nachricht konnte nicht gelesen werden: {error}'
            }
        };
    }

    t(key, variables) {
        const locale = this.locale in this.translations ? this.locale : 'en';
        const table = this.translations[locale] || {};
        const fallback = this.translations.en || {};
        let text = (key && key in table) ? table[key] : (key in fallback ? fallback[key] : String(key));

        if (variables && typeof variables === 'object') {
            Object.entries(variables).forEach(([name, value]) => {
                text = text.replaceAll(`{${name}}`, value === undefined || value === null ? '' : String(value));
            });
        }

        return text;
    }

    translateDocument() {
        try {
            document.documentElement.lang = this.locale;
        } catch (error) {
            // ignore
        }

        const nodes = document.querySelectorAll('[data-i18n]');
        nodes.forEach(node => {
            const key = node.dataset.i18n;
            if (!key)
                return;
            const attr = node.dataset.i18nAttr;
            const text = this.t(key);
            if (attr)
                node.setAttribute(attr, text);
            else if (node.dataset.i18nMode === 'html')
                node.innerHTML = text;
            else
                node.textContent = text;
        });
    }

    attachEventListeners() {
        if (this.elements.loginForm) {
            this.elements.loginForm.addEventListener('submit', event => {
                event.preventDefault();
                this.submitLogin();
            });
        }

        if (this.elements.logoutButton) {
            this.elements.logoutButton.addEventListener('click', () => {
                this.logout();
            });
        }

        if (this.elements.fetchSessionsButton) {
            this.elements.fetchSessionsButton.addEventListener('click', () => {
                this.fetchChargingSessions();
            });
        }

        if (this.elements.chargerFilter) {
            this.elements.chargerFilter.addEventListener('change', () => {
                this.fetchChargingSessions();
            });
        }

        if (this.elements.carFilter) {
            this.elements.carFilter.addEventListener('change', () => {
                this.fetchChargingSessions();
            });
        }

        if (this.elements.downloadSessionsButton) {
            this.elements.downloadSessionsButton.addEventListener('click', () => {
                this.downloadChargingSessionsCsv();
            });
        }

        if (this.elements.sessionStartFilter) {
            this.elements.sessionStartFilter.addEventListener('change', () => {
                this.renderChargingSessionsTable(this.sessions);
            });
        }

        if (this.elements.sessionEndFilter) {
            this.elements.sessionEndFilter.addEventListener('change', () => {
                this.renderChargingSessionsTable(this.sessions);
            });
        }

        if (this.elements.brandLogo) {
            this.elements.brandLogo.addEventListener('click', () => {
                this.handleBrandLogoClick();
            });
        }

        if (this.elements.easterEggClose) {
            this.elements.easterEggClose.addEventListener('click', () => {
                this.stopEasterEggGame();
            });
        }

        if (this.elements.easterEggOverlay) {
            this.elements.easterEggOverlay.addEventListener('click', event => {
                if (event.target === this.elements.easterEggOverlay)
                    this.stopEasterEggGame();
            });
        }

        if (this.elements.chargerTableBody) {
            this.elements.chargerTableBody.addEventListener('click', event => {
                const targetRow = event.target ? event.target.closest('tr[data-charger-id]') : null;
                if (!targetRow || !targetRow.dataset || !targetRow.dataset.chargerId)
                    return;
                this.toggleChargerDetails(targetRow.dataset.chargerId);
            });

            this.elements.chargerTableBody.addEventListener('keydown', event => {
                if (!event || (event.key !== 'Enter' && event.key !== ' '))
                    return;

                const targetRow = event.target ? event.target.closest('tr[data-charger-id]') : null;
                if (!targetRow || !targetRow.dataset || !targetRow.dataset.chargerId)
                    return;

                event.preventDefault();
                this.toggleChargerDetails(targetRow.dataset.chargerId);
            });
        }
    }

    initializePanelNavigation() {
        const buttons = Array.isArray(this.elements.panelButtons) ? this.elements.panelButtons : [];
        const panels = Array.isArray(this.elements.contentPanels) ? this.elements.contentPanels : [];
        if (!buttons.length || !panels.length)
            return;

        const activatePanel = target => {
            if (!target)
                return;

            const hasTarget = panels.some(panel => panel.dataset.panel === target);
            if (!hasTarget)
                return;

            this.activePanel = target;
            panels.forEach(panel => {
                const isActive = panel.dataset.panel === target;
                panel.classList.toggle('active', isActive);
                panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
            });

            buttons.forEach(button => {
                const isActive = button.dataset.panelTarget === target;
                button.classList.toggle('active', isActive);
                button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
            });

            const desiredHash = `#${target}`;
            if (window.location.hash !== desiredHash) {
                try {
                    window.history.replaceState(null, '', desiredHash);
                } catch (error) {
                    window.location.hash = target;
                }
            }
        };

        buttons.forEach(button => {
            button.addEventListener('click', () => {
                activatePanel(button.dataset.panelTarget);
            });
        });

        const hashPanel = this.normalizePanelTargetFromHash(window.location.hash);
        const preselected = buttons.find(button => button.classList.contains('active'));
        const fallback = buttons[0];
        const initialTarget = hashPanel
            || (preselected ? preselected.dataset.panelTarget : null)
            || (fallback ? fallback.dataset.panelTarget : null);

        if (initialTarget)
            activatePanel(initialTarget);

        window.addEventListener('hashchange', () => {
            const target = this.normalizePanelTargetFromHash(window.location.hash);
            if (target && target !== this.activePanel)
                activatePanel(target);
        });
    }

    normalizePanelTargetFromHash(hash) {
        if (!hash || hash.length < 2)
            return null;

        const lookup = hash.replace('#', '').trim().toLowerCase();
        if (!lookup)
            return null;

        const panels = Array.isArray(this.elements.contentPanels) ? this.elements.contentPanels : [];
        const match = panels.find(panel => {
            const id = panel.dataset.panel || '';
            return id.toLowerCase() === lookup;
        });

        return match ? match.dataset.panel : null;
    }

    restoreSession() {
        const stored = window.localStorage.getItem(this.sessionKey);
        if (!stored) {
            this.showLoginOverlay();
            return;
        }

        try {
            const parsed = JSON.parse(stored);
            if (!parsed || !parsed.token || !parsed.expiresAt) {
                this.clearSession();
                this.showLoginOverlay();
                return;
            }

            const expiresAt = new Date(parsed.expiresAt);
            if (Number.isNaN(expiresAt.getTime()) || expiresAt <= new Date()) {
                this.clearSession();
                this.showLoginOverlay(this.t('connection.sessionExpiredRestore'));
                return;
            }

            this.token = parsed.token;
            this.tokenExpiry = expiresAt;
            this.username = parsed.username || null;
            this.scheduleTokenRefresh();
            this.updateSessionUser();
            this.hideLoginOverlay();
            this.connectWebSocket();
        } catch (error) {
            console.warn('Failed to restore session', error);
            this.clearSession();
            this.showLoginOverlay(this.t('connection.restoreFailed'));
        }
    }

    submitLogin() {
        if (!this.elements.username || !this.elements.password || !this.elements.loginButton)
            return;

        const username = this.elements.username.value.trim();
        const password = this.elements.password.value;

        if (!username || !password) {
            this.showLoginError(this.t('login.emptyCredentials'));
            return;
        }

        this.setLoginLoading(true);
        this.performLoginRequest(username, password)
            .then(session => {
                this.persistSession({ ...session, username });
                this.hideLoginOverlay();
                this.updateSessionUser();
                this.connectWebSocket(true);
            })
            .catch(error => {
                const message = error && error.message ? error.message : this.t('login.failed');
                this.showLoginError(message);
            })
            .finally(() => {
                this.setLoginLoading(false);
                if (this.elements.password)
                    this.elements.password.value = '';
            });
    }

    async performLoginRequest(username, password) {
        let response;
        try {
            response = await fetch('/evdash/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ username, password })
            });
        } catch (networkError) {
            console.warn('Login request failed', networkError);
            throw new Error(this.t('login.networkError'));
        }

        let data;
        try {
            data = await response.json();
        } catch (parseError) {
            console.warn('Failed to parse login response', parseError);
            throw new Error(this.t('login.unexpectedResponse'));
        }

        if (!response.ok || !data.success) {
            const errorCode = data && data.error ? data.error : 'unauthorized';
            throw new Error(this.describeLoginError(errorCode));
        }

        if (!data.token || !data.expiresAt)
            throw new Error(this.t('login.invalidResponse'));

        return {
            token: data.token,
            expiresAt: data.expiresAt
        };
    }

    describeLoginError(code) {
        switch (code) {
        case 'invalidRequest':
            return this.t('login.invalidRequest');
        case 'unauthorized':
            return this.t('login.unauthorized');
        default:
            return this.t('login.failed');
        }
    }

    persistSession(session) {
        this.token = session.token;
        this.tokenExpiry = new Date(session.expiresAt);
        this.username = session.username || null;

        try {
            window.localStorage.setItem(this.sessionKey, JSON.stringify({
                token: this.token,
                expiresAt: this.tokenExpiry.toISOString(),
                username: this.username
            }));
        } catch (error) {
            console.warn('Failed to persist session', error);
        }

        this.scheduleTokenRefresh();
    }

    clearSession() {
        this.token = null;
        this.tokenExpiry = null;
        this.username = null;
        this.pendingRequests.clear();
        clearTimeout(this.reconnectTimer);
        this.reconnectTimer = null;
        clearTimeout(this.tokenRefreshTimer);
        this.tokenRefreshTimer = null;
        this.refreshInFlight = false;
        this.chargers.clear();
        this.cars.clear();
        this.resetChargerTable();
        this.updateCarSelector();
        this.renderChargingSessions([], this.t('sessions.emptyFetched'));

        try {
            window.localStorage.removeItem(this.sessionKey);
        } catch (error) {
            console.warn('Failed to clear session', error);
        }

        this.updateSessionUser();
    }

    connectWebSocket(resetPending = false) {
        if (!this.token) {
            this.updateConnectionStatus(this.t('header.awaitingLogin'), 'connecting');
            return;
        }

        if (this.tokenExpiry && this.tokenExpiry <= new Date()) {
            this.clearSession();
            this.showLoginOverlay(this.t('connection.sessionExpiredRestore'));
            return;
        }

        if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING))
            return;

        if (resetPending)
            this.pendingRequests.clear();

        clearTimeout(this.reconnectTimer);
        this.updateConnectionStatus(this.t('connection.connecting'), 'connecting');
        const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
        const host = window.location.hostname || 'localhost';
        const port = 4449;
        const normalizedHost = host.includes(':') ? `[${host}]` : host;
        const url = `${protocol}${normalizedHost}:${port}`;

        this.socket = new WebSocket(url);
        this.socket.addEventListener('open', () => {
            this.updateConnectionStatus(this.t('connection.authenticating'), 'authenticating');
            this.sendAuthenticate();
        });

        this.socket.addEventListener('message', event => {
            this.onSocketMessage(event);
        });

        this.socket.addEventListener('error', () => {
            this.updateConnectionStatus(this.t('connection.error'), 'error');
        });

        this.socket.addEventListener('close', () => {
            this.onSocketClosed();
        });
    }

    sendAuthenticate() {
        if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
            return;

        this.sendAction('authenticate', {
            token: this.token
        });
    }

    onSocketMessage(event) {
        let data;
        try {
            data = JSON.parse(event.data);
        } catch (error) {
            console.warn('Failed to parse WebSocket message', error);
            return;
        }

        console.log('<--', data);

        let handled = false;
        if (data.requestId && this.pendingRequests.has(data.requestId)) {
            const pending = this.pendingRequests.get(data.requestId);
            this.pendingRequests.delete(data.requestId);
            handled = this.handlePendingResponse(pending, data);
        } else {
            handled = this.handleUnsolicitedMessage(data);
        }

        if (!handled && data.success === false && data.error === 'unauthenticated')
            this.onAuthenticationFailed('unauthenticated');
    }

    handlePendingResponse(pending, data) {
        if (!pending)
            return false;

        const type = typeof pending.type === 'string' ? pending.type.toLowerCase() : '';

        if (type === 'authenticate') {
            if (data.success)
                this.onAuthenticationSucceeded();
            else
                this.onAuthenticationFailed(data.error || 'unauthorized');
            return true;
        }

        if (type === 'getchargers') {
            if (data.success) {
                const payload = data && data.payload ? data.payload : {};
                const chargers = Array.isArray(payload.chargers) ? payload.chargers : [];
                this.processChargerList(chargers);
            } else if (data.error === 'unauthenticated') {
                this.onAuthenticationFailed('unauthenticated');
            } else {
                console.warn('GetChargers request failed', data.error || 'unknownError');
            }
            return true;
        }

        if (type === 'getcars') {
            if (data.success) {
                const payload = data && data.payload ? data.payload : {};
                const cars = Array.isArray(payload.cars) ? payload.cars : [];
                this.processCarList(cars);
            } else if (data.error === 'unauthenticated') {
                this.onAuthenticationFailed('unauthenticated');
            } else {
                console.warn('GetCars request failed', data.error || 'unknownError');
            }
            return true;
        }

        if (type === 'getchargingsessions') {
            if (data.success) {
                const payload = data && data.payload ? data.payload : {};
                const sessions = Array.isArray(payload.sessions) ? payload.sessions : [];
                this.renderChargingSessions(sessions, this.t('sessions.noneFound'));
            } else if (data.error === 'unauthenticated') {
                this.onAuthenticationFailed('unauthenticated');
            } else {
                console.warn('GetChargingSessions request failed', data.error || 'unknownError');
                this.renderChargingSessions([], this.t('sessions.fetchFailed'));
            }
            return true;
        }

        return false;
    }

    handleUnsolicitedMessage(data) {
        if (!data)
            return false;

        if (data.event && this.handleNotificationEvent(data.event, data.payload))
            return true;

        if (!data.payload)
            return false;

        const payload = data.payload;

        if (Array.isArray(payload.chargers)) {
            this.processChargerList(payload.chargers);
            return true;
        }

        if (Array.isArray(payload.cars)) {
            this.processCarList(payload.cars);
            return true;
        }

        if (Array.isArray(payload.sessions)) {
            this.renderChargingSessions(payload.sessions);
            return true;
        }

        if (payload.charger) {
            this.upsertCharger(payload.charger);
            return true;
        }

        if (payload.car) {
            this.upsertCar(payload.car);
            return true;
        }

        return false;
    }

    handleNotificationEvent(eventName, payload) {
        if (!eventName)
            return false;

        const normalizedEvent = typeof eventName === 'string' ? eventName.toLowerCase() : '';
        switch (normalizedEvent) {
        case 'chargeradded':
        case 'chargerchanged':
            this.upsertCharger(payload);
            return true;
        case 'chargerremoved':
            this.removeCharger(payload);
            return true;
        case 'caradded':
        case 'carchanged':
            this.upsertCar(payload);
            return true;
        case 'carremoved':
            this.removeCar(payload);
            return true;
        case 'chargingsessionsupdated':
            if (payload && Array.isArray(payload.sessions))
                this.renderChargingSessions(payload.sessions);
            return true;
        default:
            return false;
        }
    }

    onAuthenticationSucceeded() {
        this.updateConnectionStatus(this.t('connection.connected'), 'connected');
        this.updateSessionUser();
        this.sendGetCars();
        this.sendGetChargers();
        this.fetchChargingSessions();
    }

    onAuthenticationFailed(reason) {
        const message = reason === 'unauthenticated'
            ? this.t('connection.sessionExpired')
            : this.t('connection.authFailed');

        console.warn('Authentication failed', reason);
        this.clearSession();
        this.showLoginOverlay(message);
        this.updateConnectionStatus(this.t('connection.authenticationRequired'), 'error');

        if (this.socket && this.socket.readyState === WebSocket.OPEN)
            this.socket.close();
    }

    onSocketClosed() {
        this.pendingRequests.clear();
        this.updateConnectionStatus(this.t('connection.disconnected'), 'error');
        if (!this.token) {
            this.showLoginOverlay();
            return;
        }

        clearTimeout(this.reconnectTimer);
        this.reconnectTimer = setTimeout(() => {
            this.connectWebSocket();
        }, 3000);
    }

    sendAction(action, payload = {}) {
        if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
            console.warn('Cannot send action. WebSocket not connected.');
            return null;
        }

        if (action !== 'authenticate' && this.pendingRequests.size && !this.isAuthenticated()) {
            console.warn('Cannot send action before authentication succeeded.');
            return null;
        }

        const requestId = this.generateRequestId();
        const message = {
            requestId,
            action,
            payload
        };

        const normalizedAction = typeof action === 'string' ? action.toLowerCase() : '';
        this.pendingRequests.set(requestId, { type: normalizedAction });

        this.socket.send(JSON.stringify(message));
        return requestId;
    }

    isAuthenticated() {
        for (const pending of this.pendingRequests.values()) {
            if (pending.type === 'authenticate')
                return false;
        }
        return !!this.token && !!this.socket && this.socket.readyState === WebSocket.OPEN;
    }

    sendPing() {
        return this.sendAction('ping', { timestamp: new Date().toISOString() });
    }

    sendGetCars() {
        return this.sendAction('GetCars', { });
    }

    sendGetChargers() {
        return this.sendAction('GetChargers', { });
    }

    fetchChargingSessions() {
        const payload = {};
        const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : '';
        if (chargerId)
            payload.chargerId = chargerId;

        const carId = this.elements.carFilter ? this.elements.carFilter.value : '';
        if (carId)
            payload.carId = carId;

        const requestId = this.sendAction('GetChargingSessions', payload);
        if (!requestId)
            this.renderChargingSessions([], this.t('sessions.requestFailed'));

        return requestId;
    }

    processChargerList(chargers = []) {
        if (!Array.isArray(chargers)) {
            console.warn('Expected chargers array in payload.');
            return;
        }

        const seen = new Set();
        chargers.forEach(charger => {
            const key = this.getChargerKey(charger);
            if (!key)
                return;
            seen.add(key);
            this.upsertCharger(charger);
        });

        for (const existingId of Array.from(this.chargers.keys())) {
            if (!seen.has(existingId))
                this.removeCharger(existingId);
        }
    }

    upsertCharger(charger) {
        const key = this.getChargerKey(charger);
        if (!key)
            return;

        const hasExisting = this.chargers.has(key);
        const previous = hasExisting ? this.chargers.get(key) : {};
        const merged = { ...previous, ...charger };
        merged.thingId = key;
        this.chargers.set(key, merged);
        this.updateChargerSelector();
        this.syncChargerRow(merged, !hasExisting);
    }

    syncChargerRow(charger, forceCreate = false) {
        const key = this.getChargerKey(charger);
        if (!charger || !key || !this.elements.chargerTableBody)
            return;

        let row = this.findChargerRow(key);
        if (!row || forceCreate) {
            if (row && row.parentElement) {
                const detailsRow = this.findChargerDetailsRow(key);
                if (detailsRow && detailsRow.parentElement)
                    detailsRow.parentElement.removeChild(detailsRow);
                row.parentElement.removeChild(row);
            }
            row = this.buildChargerRow(charger);
            this.elements.chargerTableBody.appendChild(row);
        } else {
            this.chargerColumns.forEach(column => {
                if (column.hidden)
                    return;
                const cell = row.querySelector(`td[data-column="${column.key}"]`);
                if (!cell)
                    return;
                this.renderCellValue(cell, column.key, charger[column.key]);
            });
        }

        this.syncChargerDetailsVisibility(key);
        this.toggleChargerEmptyState();
    }

    buildChargerRow(charger) {
        const row = document.createElement('tr');
        row.classList.add('charger-row');
        row.dataset.chargerId = this.getChargerKey(charger) || '';
        row.tabIndex = 0;
        row.setAttribute('role', 'button');
        row.setAttribute('aria-expanded', 'false');
        this.chargerColumns.forEach(column => {
            if (column.hidden)
                return;
            const cell = document.createElement('td');
            cell.dataset.column = column.key;
            this.renderCellValue(cell, column.key, charger[column.key]);
            row.appendChild(cell);
        });
        return row;
    }

    toggleChargerDetails(chargerId) {
        if (!chargerId)
            return;

        const key = this.getChargerKey(chargerId);
        if (!key)
            return;

        if (this.expandedChargers.has(key))
            this.collapseChargerDetails(key);
        else
            this.expandChargerDetails(key);
    }

    expandChargerDetails(chargerId) {
        if (!chargerId)
            return;

        const key = this.getChargerKey(chargerId);
        if (!key)
            return;

        this.expandedChargers.add(key);
        this.syncChargerDetailsVisibility(key);
    }

    collapseChargerDetails(chargerId) {
        if (!chargerId)
            return;

        const key = this.getChargerKey(chargerId);
        if (!key)
            return;

        this.expandedChargers.delete(key);
        this.syncChargerDetailsVisibility(key);
    }

    syncChargerDetailsVisibility(chargerId) {
        if (!chargerId || !this.elements.chargerTableBody)
            return;

        const key = this.getChargerKey(chargerId);
        if (!key)
            return;

        const isExpanded = this.expandedChargers.has(key);
        const row = this.findChargerRow(key);
        if (row) {
            row.classList.toggle('is-expanded', isExpanded);
            row.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
        }

        const detailsRow = this.findChargerDetailsRow(key);
        if (!isExpanded) {
            if (detailsRow && detailsRow.parentElement)
                detailsRow.parentElement.removeChild(detailsRow);
            return;
        }

        if (!row)
            return;

        const ensured = detailsRow || this.buildChargerDetailsRow(key);
        if (ensured && ensured !== detailsRow) {
            this.elements.chargerTableBody.insertBefore(ensured, row.nextSibling);
        }

        this.updateChargerDetailsRow(key);
    }

    buildChargerDetailsRow(chargerId) {
        const row = document.createElement('tr');
        row.classList.add('charger-details-row');
        row.dataset.chargerDetailsFor = this.getChargerKey(chargerId) || '';

        const cell = document.createElement('td');
        cell.colSpan = this.getVisibleChargerColumnCount();
        const list = document.createElement('dl');
        list.className = 'charger-details-list';
        cell.appendChild(list);
        row.appendChild(cell);
        return row;
    }

    updateChargerDetailsRow(chargerId) {
        if (!chargerId)
            return;

        const key = this.getChargerKey(chargerId);
        if (!key || !this.expandedChargers.has(key))
            return;

        const charger = this.chargers && this.chargers.get(key) ? this.chargers.get(key) : null;
        const detailsRow = this.findChargerDetailsRow(key);
        if (!detailsRow)
            return;

        const list = detailsRow.querySelector('dl.charger-details-list');
        if (!list)
            return;

        const items = [
            { label: this.t('chargers.columns.lastStatusUpdate'), key: 'lastStatusUpdate' },
            { label: this.t('chargers.columns.version'), key: 'version' },
            { label: this.t('chargers.columns.temperature'), key: 'temperature' },
            { label: this.t('chargers.columns.digitalInputMode'), key: 'digitalInputMode' }
        ];

        list.innerHTML = '';
        items.forEach(item => {
            const term = document.createElement('dt');
            term.textContent = item.label;
            const description = document.createElement('dd');
            const value = charger && Object.prototype.hasOwnProperty.call(charger, item.key) ? charger[item.key] : null;
            description.textContent = this.formatChargerValue(item.key, value);
            list.appendChild(term);
            list.appendChild(description);
        });
    }

    findChargerDetailsRow(chargerId) {
        if (!this.elements.chargerTableBody || !chargerId)
            return null;

        const normalizedId = typeof CSS !== 'undefined' && CSS.escape
            ? CSS.escape(String(chargerId))
            : String(chargerId).replace(/"/g, '\\"');
        return this.elements.chargerTableBody.querySelector(`tr[data-charger-details-for="${normalizedId}"]`);
    }

    getVisibleChargerColumnCount() {
        return this.chargerColumns.filter(column => !column.hidden).length;
    }

    renderCellValue(cell, key, value) {
        if (!cell)
            return;

        if (typeof value === 'boolean') {
            cell.innerHTML = '';
            const dot = document.createElement('span');
            dot.className = `value-dot ${value ? 'value-dot-true' : 'value-dot-false'}`;
            dot.setAttribute('role', 'img');
            dot.setAttribute('aria-label', value ? this.t('value.true') : this.t('value.false'));
            dot.title = value ? this.t('value.true') : this.t('value.false');
            const srText = document.createElement('span');
            srText.className = 'sr-only';
            srText.textContent = value ? this.t('value.true') : this.t('value.false');
            cell.appendChild(dot);
            cell.appendChild(srText);
            return;
        }

        cell.textContent = this.formatChargerValue(key, value);
    }

    removeCharger(identifier) {
        const key = this.getChargerKey(identifier);
        if (!key)
            return;

        this.expandedChargers.delete(key);
        this.chargers.delete(key);
        const row = this.findChargerRow(key);
        const detailsRow = this.findChargerDetailsRow(key);
        if (row && row.parentElement)
            row.parentElement.removeChild(row);
        if (detailsRow && detailsRow.parentElement)
            detailsRow.parentElement.removeChild(detailsRow);

        this.updateChargerSelector();
        this.toggleChargerEmptyState();
    }

    resetChargerTable() {
        if (!this.elements.chargerTableBody)
            return;

        this.expandedChargers.clear();
        const rows = this.elements.chargerTableBody.querySelectorAll('tr[data-charger-id], tr[data-charger-details-for]');
        rows.forEach(row => {
            if (row.parentElement)
                row.parentElement.removeChild(row);
        });

        this.toggleChargerEmptyState();
    }

    findChargerRow(chargerId) {
        if (!this.elements.chargerTableBody || !chargerId)
            return null;

        const normalizedId = typeof CSS !== 'undefined' && CSS.escape
            ? CSS.escape(String(chargerId))
            : String(chargerId).replace(/"/g, '\\"');
        return this.elements.chargerTableBody.querySelector(`tr[data-charger-id="${normalizedId}"]`);
    }

    getChargerKey(source) {
        if (!source)
            return null;

        if (typeof source === 'string')
            return source;

        if (source.thingId)
            return source.thingId;

        if (source.id)
            return source.id;

        return null;
    }

    updateChargerSelector() {
        const select = this.elements.chargerFilter;
        if (!select)
            return;

        const currentValue = select.value;
        while (select.options.length > 0)
            select.remove(0);

        const defaultOption = document.createElement('option');
        defaultOption.value = '';
        defaultOption.textContent = this.t('sessions.allChargers');
        select.appendChild(defaultOption);

        const chargers = Array.from(this.chargers.values())
            .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));

        chargers.forEach(charger => {
            const option = document.createElement('option');
            option.value = this.getChargerKey(charger) || '';
            option.textContent = charger.name || option.value;
            select.appendChild(option);
        });

        const hasValue = currentValue && select.querySelector
            && typeof CSS !== 'undefined' && CSS.escape
            && select.querySelector(`option[value="${CSS.escape(currentValue)}"]`);

        select.value = hasValue ? currentValue : '';
    }

    toggleChargerEmptyState() {
        if (!this.elements.chargerEmptyRow)
            return;

        const hasChargers = this.chargers && this.chargers.size > 0;
        this.elements.chargerEmptyRow.classList.toggle('hidden', hasChargers);
    }

    processCarList(cars = []) {
        if (!Array.isArray(cars)) {
            console.warn('Expected cars array in payload.');
            return;
        }

        const seen = new Set();
        cars.forEach(car => {
            const key = this.getCarKey(car);
            if (!key)
                return;
            seen.add(key);
            this.upsertCar(car);
        });

        for (const existingId of Array.from(this.cars.keys())) {
            if (!seen.has(existingId))
                this.removeCar(existingId);
        }
    }

    upsertCar(car) {
        const key = this.getCarKey(car);
        if (!key)
            return;

        const hasExisting = this.cars.has(key);
        const previous = hasExisting ? this.cars.get(key) : {};
        const merged = { ...previous, ...car };
        merged.thingId = key;
        this.cars.set(key, merged);
        this.updateCarSelector();
    }

    removeCar(identifier) {
        const key = this.getCarKey(identifier);
        if (!key)
            return;

        this.cars.delete(key);
        this.updateCarSelector();
    }

    getCarKey(source) {
        if (!source)
            return null;

        if (typeof source === 'string')
            return source;

        if (source.thingId)
            return source.thingId;

        if (source.id)
            return source.id;

        return null;
    }

    updateCarSelector() {
        const select = this.elements.carFilter;
        if (!select)
            return;

        const currentValue = select.value;
        while (select.options.length > 0)
            select.remove(0);

        const defaultOption = document.createElement('option');
        defaultOption.value = '';
        defaultOption.textContent = this.t('sessions.allCars');
        select.appendChild(defaultOption);

        const cars = Array.from(this.cars.values())
            .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));

        cars.forEach(car => {
            const option = document.createElement('option');
            option.value = this.getCarKey(car) || '';
            option.textContent = car.name || option.value;
            select.appendChild(option);
        });

        const hasValue = currentValue && select.querySelector
            && typeof CSS !== 'undefined' && CSS.escape
            && select.querySelector(`option[value="${CSS.escape(currentValue)}"]`);

        select.value = hasValue ? currentValue : '';
    }

    formatNumber(value, unit) {
        if (!Number.isFinite(value))
            return '—';

        const rounded = Number.parseFloat(value.toFixed(2));
        return unit ? `${rounded} ${unit}` : String(rounded);
    }

    formatNumberMaxDecimals(value, unit, decimals = 2) {
        if (!Number.isFinite(value))
            return '—';

        const factor = 10 ** decimals;
        const rounded = Math.round(value * factor) / factor;
        let text = rounded.toFixed(decimals).replace(/\.?0+$/, '');
        if (text === '-0')
            text = '0';
        return unit ? `${text} ${unit}` : text;
    }

    coerceFiniteNumber(value) {
        if (typeof value === 'number')
            return Number.isFinite(value) ? value : null;

        if (typeof value !== 'string')
            return null;

        const normalized = value.trim().replace(',', '.');
        if (!normalized)
            return null;

        const parsed = Number.parseFloat(normalized);
        return Number.isFinite(parsed) ? parsed : null;
    }

    formatChargerValue(key, value) {
        if (value === null || value === undefined || value === '')
            return '—';

        if (key === 'status') {
            const code = String(value).trim();
            const statusKeys = {
                Init: 'chargerStatus.Init',
                A1: 'chargerStatus.A1',
                A2: 'chargerStatus.A2',
                B1: 'chargerStatus.B1',
                B2: 'chargerStatus.B2',
                C1: 'chargerStatus.C1',
                C2: 'chargerStatus.C2',
                F: 'chargerStatus.F'
            };
            if (code in statusKeys)
                return `${code}: ${this.t(statusKeys[code])}`;
            return code || '—';
        }

        if (key === 'energyManagerMode') {
            const modes = {
                0: this.t('energyManagerMode.quick'),
                1: this.t('energyManagerMode.eco'),
                2: this.t('energyManagerMode.ecoTime')
            };
            if (value in modes)
                return modes[value];
            return Number.isFinite(value) ? this.t('value.unknownWithValue', { value }) : '—';
        }

        if (key === 'digitalInputMode') {
            const modes = {
                0: this.t('digitalInputMode.chargingAllowed'),
                1: this.t('digitalInputMode.chargingAllowedInverted'),
                2: this.t('digitalInputMode.pwmAndS0'),
                3: this.t('digitalInputMode.limitAndS0')
            };
            if (value in modes)
                return modes[value];
            return Number.isFinite(value) ? this.t('value.unknownWithValue', { value }) : '—';
        }

        if (key === 'lastStatusUpdate') {
            const numeric = typeof value === 'string' ? Number.parseFloat(value) : value;
            if (!Number.isFinite(numeric))
                return '—';

            const ms = numeric > 1e12 ? numeric : numeric * 1000;
            const date = new Date(ms);
            if (Number.isNaN(date.getTime()))
                return '—';

            const pad = part => String(part).padStart(2, '0');
            const day = pad(date.getDate());
            const month = pad(date.getMonth() + 1);
            const year = date.getFullYear();
            const hours = pad(date.getHours());
            const minutes = pad(date.getMinutes());
            const seconds = pad(date.getSeconds());

            return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
        }

        if (key === 'chargingCurrent' || key === 'currentPower' || key === 'sessionEnergy') {
            const numericValue = this.coerceFiniteNumber(value);
            if (numericValue === null)
                return typeof value === 'string' ? value : '—';

            const unit = key === 'chargingCurrent' ? 'A' : (key === 'currentPower' ? 'kW' : 'kWh');
            if (key === 'chargingCurrent')
                return this.formatNumber(numericValue, unit);

            if (key === 'currentPower')
                return numericValue >= 0 && numericValue < 50 ? `0 ${unit}` : this.formatNumberMaxDecimals(numericValue / 1000, unit, 2);

            return this.formatNumber(numericValue, unit);
        }

        if (typeof value === 'boolean')
            return value ? this.t('value.yes') : this.t('value.no');

        if (typeof value === 'number')
            return this.formatNumber(value);

        if (typeof value === 'string')
            return value;

        try {
            return JSON.stringify(value);
        } catch (error) {
            console.warn(`Failed to stringify value for ${key}`, error);
            return '—';
        }
    }

    renderChargingSessions(sessions, fallbackMessage) {
        const normalizedSessions = Array.isArray(sessions) ? sessions : [];
        this.sessions = normalizedSessions;

        this.renderChargingSessionsTable(normalizedSessions, fallbackMessage);
    }

    renderChargingSessionsTable(sessions, fallbackMessage) {
        const body = this.elements.chargingSessionsTableBody;
        const emptyRow = this.elements.chargingSessionsEmptyRow;
        if (!body)
            return;

        const normalizedSessions = Array.isArray(sessions) ? sessions : [];
        const filteredSessions = this.filterVisibleChargingSessions(normalizedSessions);
        const hasTimeRangeFilter = this.hasChargingSessionTimeRangeFilter();
        const hasAdditionalFilter = this.hasChargingSessionAdditionalFilter();

        const rows = body.querySelectorAll('tr[data-session-id]');
        rows.forEach(row => {
            if (row.parentElement)
                row.parentElement.removeChild(row);
        });

        if (!normalizedSessions.length || !filteredSessions.length) {
            if (emptyRow) {
                const cell = emptyRow.querySelector('td');
                if (cell) {
                    if (!normalizedSessions.length) {
                        cell.textContent = fallbackMessage || this.t('sessions.emptyFetched');
                    } else if (hasTimeRangeFilter && !hasAdditionalFilter) {
                        cell.textContent = this.t('sessions.noneInRange');
                    } else if (hasTimeRangeFilter || hasAdditionalFilter) {
                        cell.textContent = this.t('sessions.noneMatchingFilters');
                    } else {
                        cell.textContent = fallbackMessage || this.t('sessions.noneFound');
                    }
                }
                emptyRow.classList.remove('hidden');
            }
            return;
        }

        if (emptyRow)
            emptyRow.classList.add('hidden');

        filteredSessions.forEach(session => {
            body.appendChild(this.buildChargingSessionRow(session));
        });
    }

    hasChargingSessionTimeRangeFilter() {
        const start = this.elements.sessionStartFilter ? this.elements.sessionStartFilter.value : '';
        const end = this.elements.sessionEndFilter ? this.elements.sessionEndFilter.value : '';
        return !!start || !!end;
    }

    hasChargingSessionAdditionalFilter() {
        const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : '';
        const carId = this.elements.carFilter ? this.elements.carFilter.value : '';
        return !!chargerId || !!carId;
    }

    getChargingSessionTimeRangeMs() {
        const startValue = this.elements.sessionStartFilter ? this.elements.sessionStartFilter.value : '';
        const endValue = this.elements.sessionEndFilter ? this.elements.sessionEndFilter.value : '';
        const startMs = this.parseDateInputToMs(startValue, { endOfDay: false });
        const endMs = this.parseDateInputToMs(endValue, { endOfDay: true });

        if (this.elements.sessionStartFilter)
            this.elements.sessionStartFilter.setCustomValidity('');
        if (this.elements.sessionEndFilter)
            this.elements.sessionEndFilter.setCustomValidity('');

        if (Number.isFinite(startMs) && Number.isFinite(endMs) && startMs > endMs) {
            const message = this.t('sessions.startBeforeEnd');
            if (this.elements.sessionStartFilter)
                this.elements.sessionStartFilter.setCustomValidity(message);
            if (this.elements.sessionEndFilter)
                this.elements.sessionEndFilter.setCustomValidity(message);
            return { startMs: null, endMs: null };
        }

        return {
            startMs: Number.isFinite(startMs) ? startMs : null,
            endMs: Number.isFinite(endMs) ? endMs : null
        };
    }

    parseDateInputToMs(value, options = {}) {
        if (!value)
            return null;

        const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
        if (!match)
            return null;

        const year = Number.parseInt(match[1], 10);
        const month = Number.parseInt(match[2], 10);
        const day = Number.parseInt(match[3], 10);
        if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
            return null;

        const date = new Date(year, month - 1, day);
        let ms = date.getTime();
        if (!Number.isFinite(ms))
            return null;

        if (options && options.endOfDay)
            ms += 24 * 60 * 60 * 1000 - 1;

        return ms;
    }

    normalizeTimestampToMs(timestamp) {
        const numeric = typeof timestamp === 'string' ? Number.parseFloat(timestamp) : timestamp;
        if (!Number.isFinite(numeric))
            return null;

        const ms = numeric > 1e12 ? numeric : numeric * 1000;
        return Number.isFinite(ms) ? ms : null;
    }

    filterChargingSessionsByTimeRange(sessions) {
        if (!Array.isArray(sessions) || !sessions.length)
            return [];

        const { startMs, endMs } = this.getChargingSessionTimeRangeMs();
        if (!Number.isFinite(startMs) && !Number.isFinite(endMs))
            return sessions;

        return sessions.filter(session => {
            const sessionStart = this.normalizeTimestampToMs(session ? session.startTimestamp : null);
            const sessionEnd = this.normalizeTimestampToMs(session ? session.endTimestamp : null);
            const effectiveStart = Number.isFinite(sessionStart) ? sessionStart : null;
            const effectiveEnd = Number.isFinite(sessionEnd)
                ? sessionEnd
                : (Number.isFinite(sessionStart) ? sessionStart : null);

            if (!Number.isFinite(effectiveStart) && !Number.isFinite(effectiveEnd))
                return true;

            if (Number.isFinite(startMs) && Number.isFinite(effectiveEnd) && effectiveEnd < startMs)
                return false;

            if (Number.isFinite(endMs) && Number.isFinite(effectiveStart) && effectiveStart > endMs)
                return false;

            return true;
        });
    }

    filterChargingSessionsBySelectedCharger(sessions) {
        if (!Array.isArray(sessions) || !sessions.length)
            return [];

        const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : '';
        if (!chargerId)
            return sessions;

        const selectedCharger = this.chargers.has(chargerId) ? this.chargers.get(chargerId) : null;
        const selectedName = selectedCharger && selectedCharger.name ? String(selectedCharger.name).trim() : '';
        const normalizedSelectedId = this.normalizeSessionChargerIdentifier(chargerId);

        return sessions.filter(session => {
            if (!session || typeof session !== 'object')
                return false;

            const candidateIds = [
                session.chargerId,
                session.chargerThingId,
                session.thingId,
                session.evChargerId
            ];

            if (candidateIds.some(value => this.normalizeSessionChargerIdentifier(value) === normalizedSelectedId))
                return true;

            const sessionChargerName = session.chargerName ? String(session.chargerName).trim() : '';
            return !!selectedName && !!sessionChargerName
                && sessionChargerName.localeCompare(selectedName, undefined, { sensitivity: 'base' }) === 0;
        });
    }

    filterVisibleChargingSessions(sessions) {
        const byCharger = this.filterChargingSessionsBySelectedCharger(sessions);
        return this.filterChargingSessionsByTimeRange(byCharger);
    }

    normalizeSessionChargerIdentifier(value) {
        if (value === null || value === undefined)
            return '';

        return String(value).trim().replace(/[{}]/g, '').toLowerCase();
    }

    buildChargingSessionRow(session) {
        const row = document.createElement('tr');
        row.dataset.sessionId = session && session.sessionId ? session.sessionId : '';

        const cells = [
            this.deriveSessionName(session),
            session && session.chargerName ? session.chargerName : '—',
            session && session.carName ? session.carName : '—',
            this.formatTimestamp(session ? session.startTimestamp : null),
            this.formatTimestamp(session ? session.endTimestamp : null),
            this.formatSessionEnergy(session)
        ];

        cells.forEach((value, index) => {
            const cell = document.createElement('td');
            if (index === 5)
                cell.classList.add('numeric');
            cell.textContent = value;
            row.appendChild(cell);
        });

        return row;
    }

    deriveSessionName(session) {
        if (!session)
            return '—';

        if (session.name)
            return session.name;

        if (session.property)
            return session.property;

        if (session.sessionId)
            return this.t('sessions.sessionIdLabel', { id: session.sessionId });

        return '—';
    }

    formatTimestamp(timestamp) {
        const numeric = typeof timestamp === 'string' ? Number.parseFloat(timestamp) : timestamp;
        if (!Number.isFinite(numeric))
            return '—';

        const ms = numeric > 1e12 ? numeric : numeric * 1000;
        const date = new Date(ms);
        if (Number.isNaN(date.getTime()))
            return '—';

        const pad = value => String(value).padStart(2, '0');
        const day = pad(date.getDate());
        const month = pad(date.getMonth() + 1);
        const year = date.getFullYear();
        const hours = pad(date.getHours());
        const minutes = pad(date.getMinutes());

        return `${day}.${month}.${year} ${hours}:${minutes}`;
    }

    formatSessionEnergy(session) {
        const value = session && Number.isFinite(session.sessionEnergy)
            ? session.sessionEnergy
            : this.calculateSessionEnergyFromRange(session);
        if (!Number.isFinite(value))
            return '—';

        return `${value.toFixed(2)} kWh`;
    }

    calculateSessionEnergyFromRange(session) {
        if (!session)
            return null;

        const end = typeof session.energyEnd === 'string' ? Number.parseFloat(session.energyEnd) : session.energyEnd;
        const start = typeof session.energyStart === 'string' ? Number.parseFloat(session.energyStart) : session.energyStart;

        if (!Number.isFinite(end) || !Number.isFinite(start))
            return null;

        return end - start;
    }

    downloadChargingSessionsCsv() {
        const sessions = this.filterVisibleChargingSessions(this.sessions);
        if (!sessions.length) {
            console.warn('No charging sessions to download.');
            return;
        }

        const csvContent = this.buildSessionsCsv(sessions);
        if (!csvContent) {
            console.warn('Failed to build CSV for charging sessions.');
            return;
        }

        const chargerId = this.elements.chargerFilter ? this.elements.chargerFilter.value : '';
        const chargerName = chargerId && this.chargers.has(chargerId) ? this.chargers.get(chargerId).name : '';
        const chargerSuffix = chargerName ? `-${this.sanitizeFilename(chargerName)}` : '';
        const carId = this.elements.carFilter ? this.elements.carFilter.value : '';
        const carName = carId && this.cars.has(carId) ? this.cars.get(carId).name : '';
        const carSuffix = carName ? `-${this.sanitizeFilename(carName)}` : '';
        const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = `charging-sessions${chargerSuffix}${carSuffix}-${new Date().toISOString().slice(0, 10)}.csv`;
        document.body.appendChild(link);
        link.click();
        setTimeout(() => {
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
        }, 0);
    }

    buildSessionsCsv(sessions) {
        if (!Array.isArray(sessions) || !sessions.length)
            return '';

        const columns = [
            { label: this.t('csv.sessionId'), key: 'sessionId' },
            { label: this.t('csv.chargerName'), key: 'chargerName' },
            { label: this.t('csv.chargerSerialNumber'), key: 'chargerSerialNumber' },
            { label: this.t('csv.car'), key: 'carName' },
            { label: this.t('csv.start'), key: 'startTimestamp', formatter: value => this.formatCsvTimestamp(value) },
            { label: this.t('csv.end'), key: 'endTimestamp', formatter: value => this.formatCsvTimestamp(value) },
            { label: this.t('csv.energyKwh'), key: 'sessionEnergy' },
            { label: this.t('csv.meterStartKwh'), key: 'energyStart' },
            { label: this.t('csv.meterEndKwh'), key: 'energyEnd' }
        ];

        const lines = [];
        lines.push(columns.map(column => column.label).join(';'));
        sessions.forEach(session => {
            const row = columns.map(column => {
                const raw = session ? session[column.key] : '';
                const formatted = typeof column.formatter === 'function'
                    ? column.formatter(raw)
                    : this.formatCsvPrimitive(raw);
                return this.escapeCsvValue(formatted);
            });
            lines.push(row.join(';'));
        });

        return lines.join('\n');
    }

    formatCsvPrimitive(value) {
        if (value === null || value === undefined)
            return '';

        if (typeof value === 'number')
            return Number.isFinite(value) ? String(value) : '';

        if (typeof value === 'string') {
            const trimmed = value.trim();
            return trimmed.length ? trimmed : '';
        }

        if (Array.isArray(value)) {
            if (!value.length)
                return '';
            try {
                return JSON.stringify(value);
            } catch (error) {
                return '';
            }
        }

        if (typeof value === 'object') {
            if (!Object.keys(value).length)
                return '';
            try {
                return JSON.stringify(value);
            } catch (error) {
                return '';
            }
        }

        return String(value);
    }

    formatCsvTimestamp(value) {
        const numeric = typeof value === 'string' ? Number.parseFloat(value) : value;
        if (!Number.isFinite(numeric))
            return '';

        const ms = numeric > 1e12 ? numeric : numeric * 1000;
        const date = new Date(ms);
        if (Number.isNaN(date.getTime()))
            return '';

        const pad = part => String(part).padStart(2, '0');
        const day = pad(date.getDate());
        const month = pad(date.getMonth() + 1);
        const year = date.getFullYear();
        const hours = pad(date.getHours());
        const minutes = pad(date.getMinutes());

        return `${day}.${month}.${year} ${hours}:${minutes}`;
    }

    escapeCsvValue(value) {
        if (value === null || value === undefined)
            return '';

        if (typeof value === 'number' && !Number.isFinite(value))
            return '';

        if (typeof value === 'object') {
            try {
                value = JSON.stringify(value);
            } catch (error) {
                value = String(value);
            }
        }

        let stringValue = String(value);
        if (stringValue.includes('"'))
            stringValue = stringValue.replace(/"/g, '""');

        if (stringValue.search(/[;\n"]/g) !== -1)
            stringValue = `"${stringValue}"`;

        return stringValue;
    }

    handleBrandLogoClick() {
        clearTimeout(this.easterEggClickResetTimer);
        this.easterEggClickCount += 1;
        this.easterEggClickResetTimer = setTimeout(() => {
            this.easterEggClickCount = 0;
        }, 1200);

        if (this.easterEggClickCount >= 10) {
            this.easterEggClickCount = 0;
            this.startEasterEggGame();
        }
    }

    startEasterEggGame() {
        if (!this.elements.easterEggOverlay || !this.elements.easterEggCanvas)
            return;

        this.elements.easterEggOverlay.classList.remove('hidden');
        this.elements.easterEggOverlay.setAttribute('aria-hidden', 'false');

        const game = this.easterEggGame;
        const canvas = this.elements.easterEggCanvas;
        game.running = true;
        game.score = 0;
        game.keys = {};
        game.lastTime = null;
        game.player.x = canvas.width * 0.2;
        game.player.y = canvas.height * 0.5;
        this.spawnEasterEggTarget();
        this.updateEasterEggScore();
        this.toggleEasterEggListeners(true);

        const loop = timestamp => {
            if (!game.running)
                return;

            if (!game.lastTime)
                game.lastTime = timestamp;
            const delta = Math.min((timestamp - game.lastTime) / 16.67, 3);
            game.lastTime = timestamp;

            this.updateEasterEggPhysics(delta);
            this.drawEasterEggFrame();
            game.frameId = window.requestAnimationFrame(loop);
        };

        game.frameId = window.requestAnimationFrame(loop);
    }

    stopEasterEggGame() {
        const game = this.easterEggGame;
        game.running = false;
        game.keys = {};
        if (game.frameId) {
            window.cancelAnimationFrame(game.frameId);
            game.frameId = null;
        }

        if (this.elements.easterEggOverlay) {
            this.elements.easterEggOverlay.classList.add('hidden');
            this.elements.easterEggOverlay.setAttribute('aria-hidden', 'true');
        }

        this.toggleEasterEggListeners(false);
    }

    toggleEasterEggListeners(enable) {
        if (!this._easterEggKeyDownHandler) {
            this._easterEggKeyDownHandler = event => this.handleEasterEggKey(event, true);
            this._easterEggKeyUpHandler = event => this.handleEasterEggKey(event, false);
        }

        if (enable) {
            document.addEventListener('keydown', this._easterEggKeyDownHandler);
            document.addEventListener('keyup', this._easterEggKeyUpHandler);
        } else {
            document.removeEventListener('keydown', this._easterEggKeyDownHandler);
            document.removeEventListener('keyup', this._easterEggKeyUpHandler);
        }
    }

    handleEasterEggKey(event, isDown) {
        if (!this.easterEggGame.running)
            return;

        const key = event.key ? event.key.toLowerCase() : '';
        const movableKeys = ['arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'w', 'a', 's', 'd'];
        if (key === 'escape') {
            this.stopEasterEggGame();
            return;
        }

        if (movableKeys.includes(key)) {
            event.preventDefault();
            this.easterEggGame.keys[key] = isDown;
        }
    }

    updateEasterEggPhysics(delta) {
        const canvas = this.elements.easterEggCanvas;
        if (!canvas)
            return;

        const game = this.easterEggGame;
        const { player, target } = game;
        const input = {
            x: (game.keys.arrowright || game.keys.d ? 1 : 0) - (game.keys.arrowleft || game.keys.a ? 1 : 0),
            y: (game.keys.arrowdown || game.keys.s ? 1 : 0) - (game.keys.arrowup || game.keys.w ? 1 : 0)
        };

        if (input.x !== 0 || input.y !== 0) {
            const length = Math.hypot(input.x, input.y) || 1;
            const speed = player.speed * delta;
            player.x += (input.x / length) * speed;
            player.y += (input.y / length) * speed;
        }

        const minX = player.size;
        const maxX = canvas.width - player.size;
        const minY = player.size;
        const maxY = canvas.height - player.size;
        player.x = Math.min(Math.max(player.x, minX), maxX);
        player.y = Math.min(Math.max(player.y, minY), maxY);

        const dx = player.x - target.x;
        const dy = player.y - target.y;
        const distance = Math.hypot(dx, dy);
        if (distance <= player.size + target.size) {
            game.score += 1;
            player.speed = Math.min(player.speed + 0.15, 7.5);
            this.updateEasterEggScore();
            this.spawnEasterEggTarget();
        }
    }

    drawEasterEggFrame() {
        const canvas = this.elements.easterEggCanvas;
        if (!canvas)
            return;

        const ctx = canvas.getContext('2d');
        const game = this.easterEggGame;

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        gradient.addColorStop(0, '#0f1c3d');
        gradient.addColorStop(1, '#092037');
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        ctx.strokeStyle = 'rgba(255,255,255,0.08)';
        ctx.lineWidth = 1;
        for (let x = 20; x < canvas.width; x += 40) {
            ctx.beginPath();
            ctx.moveTo(x, 0);
            ctx.lineTo(x, canvas.height);
            ctx.stroke();
        }
        for (let y = 20; y < canvas.height; y += 40) {
            ctx.beginPath();
            ctx.moveTo(0, y);
            ctx.lineTo(canvas.width, y);
            ctx.stroke();
        }

        const { player, target } = game;
        ctx.fillStyle = '#f4b400';
        ctx.beginPath();
        ctx.moveTo(target.x, target.y - target.size);
        ctx.lineTo(target.x - target.size * 0.6, target.y + target.size * 0.2);
        ctx.lineTo(target.x - target.size * 0.2, target.y + target.size * 0.2);
        ctx.lineTo(target.x - target.size, target.y + target.size);
        ctx.lineTo(target.x + target.size * 0.2, target.y + target.size * 0.2);
        ctx.lineTo(target.x + target.size * 0.6, target.y - target.size * 0.8);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = 'rgba(0,0,0,0.2)';
        ctx.lineWidth = 1.2;
        ctx.stroke();

        const carWidth = player.size * 2.4;
        const carHeight = player.size * 1.4;
        const carX = player.x - carWidth / 2;
        const carY = player.y - carHeight / 2;

        const carGradient = ctx.createLinearGradient(carX, carY, carX + carWidth, carY + carHeight);
        carGradient.addColorStop(0, '#e30a18');
        carGradient.addColorStop(1, '#f48221');
        ctx.fillStyle = carGradient;

        ctx.beginPath();
        ctx.moveTo(carX + carWidth * 0.15, carY + carHeight);
        ctx.lineTo(carX + carWidth * 0.15, carY + carHeight * 0.55);
        ctx.lineTo(carX + carWidth * 0.35, carY + carHeight * 0.25);
        ctx.lineTo(carX + carWidth * 0.65, carY + carHeight * 0.25);
        ctx.lineTo(carX + carWidth * 0.85, carY + carHeight * 0.55);
        ctx.lineTo(carX + carWidth * 0.85, carY + carHeight);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = 'rgba(255,255,255,0.35)';
        ctx.lineWidth = 2;
        ctx.stroke();

        const wheelRadius = player.size * 0.35;
        const wheelY = carY + carHeight;
        ctx.fillStyle = '#0d1221';
        ctx.beginPath();
        ctx.arc(carX + carWidth * 0.28, wheelY, wheelRadius, 0, Math.PI * 2);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(carX + carWidth * 0.72, wheelY, wheelRadius, 0, Math.PI * 2);
        ctx.fill();

        ctx.fillStyle = 'rgba(255,255,255,0.35)';
        ctx.fillRect(carX + carWidth * 0.42, carY + carHeight * 0.32, carWidth * 0.22, carHeight * 0.2);
    }

    spawnEasterEggTarget() {
        const canvas = this.elements.easterEggCanvas;
        if (!canvas)
            return;

        const target = this.easterEggGame.target;
        const player = this.easterEggGame.player;
        const padding = target.size + 12;
        let attempts = 0;
        do {
            target.x = padding + Math.random() * (canvas.width - padding * 2);
            target.y = padding + Math.random() * (canvas.height - padding * 2);
            attempts++;
        } while (Math.hypot(player.x - target.x, player.y - target.y) < player.size * 2 && attempts < 12);
    }

    updateEasterEggScore() {
        if (!this.elements.easterEggScore)
            return;
        this.elements.easterEggScore.textContent = this.t('easterEgg.score', { score: this.easterEggGame.score });
    }

    sanitizeFilename(value) {
        if (typeof value !== 'string')
            return '';

        const trimmed = value.trim();
        if (!trimmed.length)
            return '';

        return trimmed
            .replace(/[^a-z0-9-_]+/gi, '-')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '')
            .toLowerCase();
    }

    updateConnectionStatus(text, state) {
        if (this.elements.connectionStatus)
            this.elements.connectionStatus.textContent = text;

        if (!this.elements.statusDot)
            return;

        const dot = this.elements.statusDot;
        dot.classList.remove('connecting', 'connected', 'authenticating', 'error');
        dot.classList.add(state);
    }

    updateSessionUser() {
        if (!this.elements.sessionUsername)
            return;

        const defaultLabel = document.body && document.body.dataset.mode === 'help'
            ? this.t('header.authenticateHint')
            : this.t('header.awaitingLogin');

        if (!this.token || !this.username) {
            this.elements.sessionUsername.textContent = defaultLabel;
            this.toggleLogoutButton(false);
            return;
        }

        this.elements.sessionUsername.textContent = this.username;
        this.toggleLogoutButton(true);
    }

    showLoginOverlay(message) {
        this.setAuthLayout(true);
        if (this.elements.loginOverlay)
            this.elements.loginOverlay.classList.remove('hidden');
        if (typeof message === 'string' && message.length > 0)
            this.showLoginError(message);
        else
            this.hideLoginError();

        if (this.elements.username)
            setTimeout(() => this.elements.username.focus(), 50);
    }

    hideLoginOverlay() {
        this.setAuthLayout(false);
        if (this.elements.loginOverlay)
            this.elements.loginOverlay.classList.add('hidden');
        this.hideLoginError();
    }

    showLoginError(message) {
        if (!this.elements.loginError)
            return;
        this.elements.loginError.textContent = message;
        this.elements.loginError.classList.remove('hidden');
    }

    hideLoginError() {
        if (!this.elements.loginError)
            return;
        this.elements.loginError.textContent = '';
        this.elements.loginError.classList.add('hidden');
    }

    setLoginLoading(loading) {
        if (!this.elements.loginButton)
            return;
        this.elements.loginButton.disabled = loading;
        this.elements.loginButton.textContent = loading ? this.t('login.signingIn') : this.t('login.signIn');
    }

    generateRequestId() {
        if (window.crypto && window.crypto.randomUUID)
            return window.crypto.randomUUID();

        return `req-${Date.now()}-${Math.random().toString(16).slice(2)}`;
    }

    setAuthLayout(requireAuth) {
        const body = document.body;
        if (!body)
            return;
        body.classList.toggle('needs-auth', requireAuth);
    }

    toggleLogoutButton(visible) {
        if (!this.elements.logoutButton)
            return;
        this.elements.logoutButton.classList.toggle('hidden', !visible);
    }

    logout() {
        this.clearSession();
        if (this.socket && this.socket.readyState === WebSocket.OPEN)
            this.socket.close();
        this.updateConnectionStatus(this.t('connection.loggedOut'), 'connecting');
        this.updateSessionUser();
        this.showLoginOverlay(this.t('connection.loggedOutOverlay'));
    }

    scheduleTokenRefresh() {
        clearTimeout(this.tokenRefreshTimer);
        this.tokenRefreshTimer = null;

        if (!this.token || !this.tokenExpiry)
            return;

        const now = Date.now();
        const expiryTime = this.tokenExpiry.getTime();
        const leadTimeMs = 60 * 1000; // refresh one minute before expiry
        const delay = Math.max(expiryTime - leadTimeMs - now, 5 * 1000);

        this.tokenRefreshTimer = setTimeout(() => {
            this.refreshToken();
        }, delay);
    }

    async refreshToken() {
        if (!this.token || this.refreshInFlight)
            return;

        this.refreshInFlight = true;

        try {
            const response = await fetch('/evdash/api/refresh', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ token: this.token })
            });

            const data = await response.json();
            if (!response.ok || !data.success)
                throw new Error(data && data.error ? data.error : 'refreshFailed');

            if (!data.token || !data.expiresAt)
                throw new Error(this.t('login.invalidResponse'));

            this.persistSession({
                token: data.token,
                expiresAt: data.expiresAt,
                username: this.username
            });
            this.updateSessionUser();
        } catch (error) {
            console.warn('Token refresh failed', error);
            this.clearSession();
            this.updateConnectionStatus(this.t('connection.authenticationRequired'), 'error');
            if (this.socket && this.socket.readyState === WebSocket.OPEN)
                this.socket.close();
            this.showLoginOverlay(this.t('connection.sessionExpired'));
        } finally {
            this.refreshInFlight = false;
        }
    }
}

window.app = new DashboardApp();
