WME Easy Storage Manager

Easy Storage Manager is a handy script that allows you to easily export and import local storage data for WME.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         WME Easy Storage Manager
// @namespace    https://greasyfork.org/en/scripts/466806-easy-storage-manager
// @author       DevlinDelFuego, Hiwi234
// @version      2026.04.13
// @description  Easy Storage Manager is a handy script that allows you to easily export and import local storage data for WME.
// @match        *://*.waze.com/*editor*
// @exclude      *://*.waze.com/user/editor*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      api.dropboxapi.com
// @connect      content.dropboxapi.com
// @connect      www.googleapis.com
// @connect      oauth2.googleapis.com
// @connect      accounts.google.com
// @connect      esm-token-proxy.devlinlab.com
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @license      GPLv3
// @run-at      document-start
// ==/UserScript==

(function () {
    'use strict';

  const ESM_DIAG = {
    log: (...args) => console.log('[ESM]', ...args),
    warn: (...args) => console.warn('[ESM]', ...args),
    error: (...args) => console.error('[ESM]', ...args)
  };
  window.addEventListener('error', (e) => {
    try { sessionStorage.setItem('ESM_DIAG_LAST_ERROR', `${e.message} at ${e.filename}:${e.lineno}`); } catch (err) {}
    ESM_DIAG.error('Unhandled error:', e.message);
  });
  window.addEventListener('unhandledrejection', (e) => {
    try { sessionStorage.setItem('ESM_DIAG_LAST_REJECTION', String(e.reason)); } catch (err) {}
    ESM_DIAG.error('Unhandled rejection:', e.reason);
  });
  // Compact UI styles to avoid blowing up the layout
  (function injectCompactStyles(){
    const css = `
      /* Compact buttons across all ESM variants */
      #esm-import, #esm-export,
      #esm-cloud-backup, #esm-cloud-restore,
      #esm-gdrive-backup, #esm-gdrive-restore,
      #esm-import-btn, #esm-export-btn,
      #esm-drive-backup-btn, #esm-drive-restore-btn,
      #esm-gdrive-backup-btn, #esm-gdrive-restore-btn {
        padding: 6px 10px !important;
        font-size: 12px !important;
        width: 100% !important;
        flex: 1 1 0 !important;
        min-width: 0 !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        box-sizing: border-box !important;
        margin: 0 !important;
      }
      /* Row containers: use CSS Grid for perfect equality with 2 equal columns */
      #esm-tab > div, #easy-storage-manager-tab > div {
        display: grid !important;
        width: 100% !important;
        grid-template-columns: 1fr 1fr !important;
        gap: 8px !important;
        align-items: stretch !important;
        box-sizing: border-box !important;
      }
      /* Fallback panel button container - also 3 equal columns */
      #esm-fallback-panel .btnRow,
      #esm-fallback-panel > div[style*="display: flex"] {
        display: grid !important;
        width: 100% !important;
        grid-template-columns: 1fr 1fr 1fr !important;
        gap: 8px !important;
        align-items: stretch !important;
        box-sizing: border-box !important;
        margin: 0 !important;
      }
      @media (max-width: 768px) {
        #esm-tab > div, #easy-storage-manager-tab > div,
        #esm-fallback-panel .btnRow,
        #esm-fallback-panel > div[style*="display: flex"] {
          grid-template-columns: 1fr 1fr !important;
        }
      }
      @media (max-width: 480px) {
        #esm-tab > div, #easy-storage-manager-tab > div,
        #esm-fallback-panel .btnRow,
        #esm-fallback-panel > div[style*="display: flex"] {
          grid-template-columns: 1fr !important;
        }
      }
    `;
    try {
      const style = document.createElement('style');
      style.id = 'esm-compact-styles';
      style.textContent = css;
      if (document.head) {
        document.head.appendChild(style);
      } else {
        document.addEventListener('DOMContentLoaded', function(){
          try { document.head.appendChild(style); } catch (_) {}
        });
      }
    } catch (_) {}
  })();

    let importedData; // Imported JSON data
    let applyButton; // Apply button element
    let autoSaveInterval = null;
    let autoSaveEnabled = false;
    const AUTO_SAVE_ENABLED_KEY = 'ESM_AUTO_SAVE_ENABLED';
    const AUTO_SAVE_INTERVAL_KEY = 'ESM_AUTO_SAVE_INTERVAL';
    const AUTO_SAVE_TARGET_KEY = 'ESM_AUTO_SAVE_TARGET';
    const AUTO_SAVE_MAX_BACKUPS_KEY = 'ESM_AUTO_SAVE_MAX_BACKUPS';
    let scriptVersion = (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : 'dev-local';
    const updateMessage = "<b>Changelog</b><br><br> - Added Dropbox cloud backup and restore functionality. <br> - Added Google Drive cloud backup and restore functionality. <br> - Improved backend security when connecting to cloud backup servers. <br><br>";
    const REAPPLY_STASH_KEY = 'ESM_POST_RELOAD';
    // Sprachunterstützung (DE/EN) für UI-Texte
    const ESM_LANG = ((navigator.language || 'en').toLowerCase().startsWith('de')) ? 'de' : 'en';
    const ESM_I18N = {
      de: {
        panelTitle: 'Cloud-Backup (Dropbox) – Anleitung',
        show: 'Anzeigen',
        hide: 'Ausblenden',
        howTo: 'So aktivierst du die Online-Cloud:',
        step1: 'Bei Dropbox anmelden.',
        step2: 'App erstellen (kostenlos):',
        step3: 'In deiner App einen Generated access token erzeugen.',
        step4: 'Token unten eingeben und speichern.',
        genTokenLabel: 'Generated access token:',
        saveTokenBtn: 'Token speichern',
        clearTokenBtn: 'Abmelden',
        statusEnterToken: 'Bitte Token eingeben.',
        statusSavedValidated: 'Token gespeichert und validiert. Dropbox ist bereit.',
        statusSignedOut: 'Abgemeldet. Bitte neuen Token eingeben.',
        cloudBackup: '☁️ Dropbox Sichern',
        cloudRestore: '☁️ Dropbox Laden',
        cloudBackupTitle: '☁️ Backup in Dropbox sichern',
        cloudRestoreTitle: '☁️ Aus Dropbox wiederherstellen',
        gdriveBackup: '📁 Google Sichern',
        gdriveRestore: '📁 Google Laden',
        gdriveBackupTitle: '📁 Backup in Google Drive sichern',
        gdriveRestoreTitle: '📁 Aus Google Drive wiederherstellen',
        gdrivePanelTitle: 'Google Drive – Verbindung',
        gdriveHowTo: 'So aktivierst du Google Drive:',
        gdriveStep1: 'Google Cloud Console öffnen.',
        gdriveStep2: 'Projekt erstellen und Google Drive API aktivieren.',
        gdriveStep3: 'OAuth 2.0 Client-ID erstellen (Web-App).',
        gdriveStep4: 'API-Key unten eingeben und speichern.',
        gdriveTokenLabel: 'Google Drive API Key / Access Token:',
        gdriveSaveTokenBtn: 'Token speichern',
        gdriveClearTokenBtn: 'Abmelden',
        gdriveStatusEnterToken: 'Bitte Google Drive Token eingeben.',
        gdriveStatusSaved: 'Token gespeichert. Google Drive ist bereit.',
        gdriveStatusSignedOut: 'Abgemeldet. Bitte neuen Token eingeben.',
        gdriveSaveSuccess: '✅ Backup erfolgreich in Google Drive gespeichert!',
        gdriveSaveFailed: '❌ Google Drive Backup fehlgeschlagen:',
        gdriveNoBackups: '❌ Keine Backup-Dateien in Google Drive gefunden.',
        gdriveLoadSuccess: '✅ Google Drive Backup erfolgreich geladen!',
        gdriveRestoreFailed: '❌ Google Drive Wiederherstellung fehlgeschlagen:',
        gdriveExportUnavailable: 'Google Drive Backup-Funktion nicht verfügbar',
        gdriveImportUnavailable: 'Google Drive Wiederherstellungs-Funktion nicht verfügbar'
      },
      en: {
        panelTitle: 'Cloud Backup (Dropbox) – Guide',
        show: 'Show',
        hide: 'Hide',
        howTo: 'How to enable cloud backup:',
        step1: 'Sign in to Dropbox.',
        step2: 'Create an app (free):',
        step3: 'Generate a personal access token in your app.',
        step4: 'Enter the token below and save it.',
        genTokenLabel: 'Generated access token:',
        saveTokenBtn: 'Save Token',
        clearTokenBtn: 'Sign out',
        statusEnterToken: 'Please enter a token.',
        statusSavedValidated: 'Token saved and validated. Dropbox is ready.',
        statusSignedOut: 'Signed out. Please enter a new token.',
        cloudBackup: '☁️ Dropbox Backup',
        cloudRestore: '☁️ Dropbox Restore',
        cloudBackupTitle: '☁️ Save backup to Dropbox',
        cloudRestoreTitle: '☁️ Restore from Dropbox',
        gdriveBackup: '📁 Google Backup',
        gdriveRestore: '📁 Google Restore',
        gdriveBackupTitle: '📁 Save backup to Google Drive',
        gdriveRestoreTitle: '📁 Restore from Google Drive',
        gdrivePanelTitle: 'Google Drive – Connection',
        gdriveHowTo: 'How to enable Google Drive backup:',
        gdriveStep1: 'Open Google Cloud Console.',
        gdriveStep2: 'Create a project and enable Google Drive API.',
        gdriveStep3: 'Create an OAuth 2.0 Client ID (Web App).',
        gdriveStep4: 'Enter the token below and save it.',
        gdriveTokenLabel: 'Google Drive API Key / Access Token:',
        gdriveSaveTokenBtn: 'Save Token',
        gdriveClearTokenBtn: 'Sign out',
        gdriveStatusEnterToken: 'Please enter a Google Drive token.',
        gdriveStatusSaved: 'Token saved. Google Drive is ready.',
        gdriveStatusSignedOut: 'Signed out. Please enter a new token.',
        gdriveSaveSuccess: '✅ Backup saved to Google Drive successfully!',
        gdriveSaveFailed: '❌ Google Drive backup failed:',
        gdriveNoBackups: '❌ No backup files found in Google Drive.',
        gdriveLoadSuccess: '✅ Google Drive backup loaded successfully!',
        gdriveRestoreFailed: '❌ Google Drive restore failed:',
        gdriveExportUnavailable: 'Google Drive backup function not available',
        gdriveImportUnavailable: 'Google Drive restore function not available'
      }
    };
    function t(key) {
      const langVal = (ESM_I18N[ESM_LANG] && ESM_I18N[ESM_LANG][key]) || null;
      const enVal = (ESM_I18N.en && ESM_I18N.en[key]) || null;
      const deVal = (ESM_I18N.de && ESM_I18N.de[key]) || null;
      return langVal || enVal || deVal || key;
    }
    // Extend language maps with additional UI texts for Import/Export & Drive
    try {
      Object.assign(ESM_I18N.de, {
        importExportDesc: 'Importiere eine Backup-JSON-Datei oder exportiere ein vollständiges Backup (localStorage, sessionStorage, Cookies, IndexedDB).',
        importBackup: '♻️ Wiederherstellen',
        exportBackup: '💾 Lokal Speichern',
        importBackupTitle: '♻️ Backup importieren',
        exportBackupTitle: '💾 Backup exportieren',
        driveBackupTitle: '☁️ Backup in Dropbox sichern',
        driveRestoreTitle: '☁️ Aus Dropbox wiederherstellen',
        importFunctionUnavailable: 'Import-Funktion nicht verfügbar',
        exportFunctionUnavailable: 'Export-Funktion nicht verfügbar',
        dropboxExportUnavailable: 'Dropbox Backup-Funktion nicht verfügbar',
        dropboxImportUnavailable: 'Dropbox Wiederherstellungs-Funktion nicht verfügbar',
        driveExportUnavailable: 'Dropbox Backup-Funktion nicht verfügbar',
        driveImportUnavailable: 'Dropbox Wiederherstellungs-Funktion nicht verfügbar',
        selectAll: 'Alle auswählen',
        deselectAll: 'Auswahl aufheben',
        apply: 'Anwenden',
        scriptTitle: 'Easy Storage Manager',
        fallbackDesc: 'Fallback-Panel aktiv. Importiere/Exportiere Backups und wähle Schlüssel zur Wiederherstellung.',
        autoSaveLabel: '⏱️ Auto Save',
        autoSaveEnable: 'Auto Save aktivieren',
        autoSaveInterval: 'Intervall:',
        autoSaveTarget: 'Ziel:',
        autoSaveTargetLocal: 'Lokal (Download)',
        autoSaveTargetDropbox: 'Dropbox',
        autoSaveTargetGdrive: 'Google Drive',
        autoSaveRunning: 'Auto Save läuft...',
        autoSaveDone: '✅ Auto Save abgeschlossen.',
        autoSaveFailed: '❌ Auto Save fehlgeschlagen:',
        autoSaveMaxBackups: 'Backups:',
        autoSaveMaxBackupsHint1: '1 = überschreiben',
        autoSaveMaxBackupsHint10: '10 = rotierend'
      });
      Object.assign(ESM_I18N.en, {
        importExportDesc: 'Import a backup JSON file or export a full backup (localStorage, sessionStorage, cookies, IndexedDB).',
        importBackup: '♻️ Restore',
        exportBackup: '💾 Save Local',
        importBackupTitle: '♻️ Import Backup',
        exportBackupTitle: '💾 Export Backup',
        driveBackupTitle: '☁️ Save backup to Dropbox',
        driveRestoreTitle: '☁️ Restore from Dropbox',
        importFunctionUnavailable: 'Import function not available',
        exportFunctionUnavailable: 'Export function not available',
        dropboxExportUnavailable: 'Dropbox export function not available',
        dropboxImportUnavailable: 'Dropbox restore function not available',
        driveExportUnavailable: 'Dropbox export function not available',
        driveImportUnavailable: 'Dropbox import function not available',
        selectAll: 'Select All',
        deselectAll: 'Deselect All',
        apply: 'Apply',
        scriptTitle: 'Easy Storage Manager',
        fallbackDesc: 'Fallback panel active. Import/export backups and choose keys to restore.',
        autoSaveLabel: '⏱️ Auto Save',
        autoSaveEnable: 'Enable Auto Save',
        autoSaveInterval: 'Interval:',
        autoSaveTarget: 'Target:',
        autoSaveTargetLocal: 'Local (Download)',
        autoSaveTargetDropbox: 'Dropbox',
        autoSaveTargetGdrive: 'Google Drive',
        autoSaveRunning: 'Auto Save running...',
        autoSaveDone: '✅ Auto Save completed.',
        autoSaveFailed: '❌ Auto Save failed:',
        autoSaveMaxBackups: 'Backups:',
        autoSaveMaxBackupsHint1: '1 = overwrite',
        autoSaveMaxBackupsHint10: '10 = rotating'
      });
    } catch (_) {}
    // Zusätzliche Texte für vollständige UI-Übersetzung (Alerts/Labels)
    try {
      Object.assign(ESM_I18N.de, {
        dropboxSaveSuccessPrefix: '✅ Backup erfolgreich in Dropbox gespeichert!',
        dropboxSaveFailedPrefix: '❌ Dropbox Backup fehlgeschlagen:',
        dropboxNoBackups: '❌ Keine Backup-Dateien in Dropbox gefunden.',
        invalidSelection: '❌ Ungültige Auswahl.',
        foreignBackupHint: 'Hinweis: Das Backup stammt von einer anderen Quelle. Aus Sicherheitsgründen werden Cookies und Session Storage standardmäßig nicht importiert.',
        dropboxLoadSuccessPrefix: '✅ Dropbox Backup erfolgreich geladen!',
        foundEntriesLabel: 'Gefundene Einträge:',
        invalidJson: '❌ Backup-Datei konnte nicht gelesen werden: Ungültiges JSON-Format.',
        dropboxRestoreFailedPrefix: '❌ Dropbox Wiederherstellung fehlgeschlagen:',
        fileReadSuccess: 'Datei erfolgreich gelesen',
        fileReadError: 'Fehler beim Lesen der Datei. Bitte erneut versuchen.',
        noKeysSelected: 'Keine Schlüssel ausgewählt. Nichts zu importieren.',
        fileLabel: 'Datei:',
        pathLabel: 'Pfad:',
        sizeLabel: 'Größe:',
        kb: 'KB'
      });
      Object.assign(ESM_I18N.en, {
        dropboxSaveSuccessPrefix: '✅ Backup saved to Dropbox successfully!',
        dropboxSaveFailedPrefix: '❌ Dropbox backup failed:',
        dropboxNoBackups: '❌ No backup files found in Dropbox.',
        invalidSelection: '❌ Invalid selection.',
        foreignBackupHint: 'Note: The backup originates from a different source. For security reasons, cookies and session storage are not imported by default.',
        dropboxLoadSuccessPrefix: '✅ Dropbox backup loaded successfully!',
        foundEntriesLabel: 'Found entries:',
        invalidJson: '❌ Could not read backup file: Invalid JSON format.',
        dropboxRestoreFailedPrefix: '❌ Dropbox restore failed:',
        fileReadSuccess: 'File read successfully',
        fileReadError: 'Error occurred while reading the file. Please try again.',
        noKeysSelected: 'No keys selected. Nothing to import.',
        fileLabel: 'File:',
        pathLabel: 'Path:',
        sizeLabel: 'Size:',
        kb: 'KB'
      });
    } catch (_) {}
    try {
      Object.assign(ESM_I18N.de, {
        restorePrompt: 'Welche Datei möchten Sie wiederherstellen?'
      });
      Object.assign(ESM_I18N.en, {
        restorePrompt: 'Which file would you like to restore?'
      });
    } catch (_) {}

    // Dropbox API Configuration
    const DROPBOX_CONFIG = {
      APP_KEY: '9fxl4soww5di6qt',
      ACCESS_TOKEN: null,
      // Use same-origin Waze Dropbox endpoint to satisfy CSP
      PROXY_BASE_URL: '/dropbox',
      API_BASE_URL: 'https://api.dropboxapi.com/2',
      CONTENT_API_URL: 'https://content.dropboxapi.com/2'
    };

    let dropboxAuth = null;
    const DROPBOX_TOKEN_KEY = 'ESM_DROPBOX_TOKEN';
    const DROPBOX_ACCOUNT_CACHE_KEY = 'ESM_DROPBOX_ACCOUNT';
    const DROPBOX_REFRESH_TOKEN_KEY = 'ESM_DROPBOX_REFRESH_TOKEN';
    const DROPBOX_TOKEN_EXPIRES_KEY = 'ESM_DROPBOX_TOKEN_EXPIRES';

    // Sandboxed storage for OAuth refresh tokens (GM_setValue when available, localStorage fallback).
    // Access tokens use sessionStorage — cleared on tab close.
    const gmStorage = {
      get(key) {
        try { if (typeof GM_getValue === 'function') return GM_getValue(key, null); } catch (_) {}
        return localStorage.getItem(key);
      },
      set(key, value) {
        try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); return; } } catch (_) {}
        localStorage.setItem(key, value);
      },
      remove(key) {
        try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); return; } } catch (_) {}
        localStorage.removeItem(key);
      }
    };

// Hook alerts to display bilingual messages for known keys
(function(){
  const origAlert = window.alert;
  const map = {
    'Import function not available': t('importFunctionUnavailable'),
    'Export function not available': t('exportFunctionUnavailable'),
    'Dropbox Backup-Funktion nicht verfügbar': t('dropboxExportUnavailable'),
    'Dropbox Wiederherstellungs-Funktion nicht verfügbar': t('dropboxImportUnavailable'),
  };
  window.alert = function(msg){
    const key = String(msg);
    const repl = map[key] || key;
    return origAlert(repl);
  };
})();

    function setDropboxToken(token) {
      try {
        sessionStorage.setItem(DROPBOX_TOKEN_KEY, token);
        dropboxAuth = token;
        ESM_DIAG.log('Dropbox token gespeichert.');
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Token nicht speichern:', e);
      }
    }

    function clearDropboxToken() {
      try {
        sessionStorage.removeItem(DROPBOX_TOKEN_KEY);
        sessionStorage.removeItem(DROPBOX_TOKEN_EXPIRES_KEY);
        gmStorage.remove(DROPBOX_REFRESH_TOKEN_KEY);
        dropboxAuth = null;
        sessionStorage.removeItem(DROPBOX_ACCOUNT_CACHE_KEY);
        ESM_DIAG.log('Dropbox-Token gelöscht.');
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Token nicht löschen:', e);
      }
    }

    async function promptForDropboxToken() {
      const hint = 'Bitte persönliches Dropbox Access Token eingeben (Bearer Token).\n' +
        'Anleitung: Öffne https://www.dropbox.com/developers/apps, wähle deine App,\n' +
        'erzeuge ein Access Token und füge es hier ein.';
      const token = prompt(hint);
      if (!token) throw new Error('Kein Dropbox-Token eingegeben');
      setDropboxToken(token.trim());
      // Validierung
      await getDropboxAccount(token.trim());
      return token.trim();
    }

    async function getDropboxAccount(accessToken) {
      // Cache lesen
      try {
        const cached = sessionStorage.getItem(DROPBOX_ACCOUNT_CACHE_KEY);
        if (cached) return JSON.parse(cached);
      } catch (_) {}
      if (typeof GM_xmlhttpRequest !== 'function') {
        // Ohne GM können wir den Account nicht bequem validieren; Return minimal
        return { account_id: 'unknown', email: 'unknown' };
      }
      const res = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL}/users/get_current_account`, {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${accessToken}` },
        // Body muss leer sein
      });
      if (res.status < 200 || res.status >= 300) {
        throw new Error(`Dropbox-Token ungültig (${res.status})`);
      }
      let info;
      try { info = JSON.parse(res.responseText); } catch (_) { info = { account_id: 'unknown', email: 'unknown' }; }
      try { sessionStorage.setItem(DROPBOX_ACCOUNT_CACHE_KEY, JSON.stringify(info)); } catch (_) {}
      return info;
    }

    // ===== OAuth 2.0 (PKCE) Hilfsfunktionen =====
    function base64urlFromBytes(bytes) {
      let bin = '';
      for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
      return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
    }
    function generateCodeVerifier() {
      const bytes = new Uint8Array(64);
      crypto.getRandomValues(bytes);
      return base64urlFromBytes(bytes);
    }
    async function sha256Base64Url(input) {
      const data = new TextEncoder().encode(input);
      const hash = await crypto.subtle.digest('SHA-256', data);
      return base64urlFromBytes(new Uint8Array(hash));
    }
    // Wrapper around GM_xmlhttpRequest to bypass page CSP when available
    function gmFetch(url, opts = {}) {
      const method = opts.method || 'GET';
      const headers = opts.headers || {};
      // Only include a request body if the caller provided one.
      // This avoids sending the literal string "null" to endpoints (e.g. Dropbox files/download)
      // that require an entirely empty request body.
      const hasBody = Object.prototype.hasOwnProperty.call(opts, 'body');
      let body = null;
      if (hasBody) {
        if (typeof opts.body === 'string') body = opts.body;
        else if (opts.body != null) body = JSON.stringify(opts.body);
      }
      const responseType = opts.responseType || 'text';
      return new Promise((resolve, reject) => {
        if (typeof GM_xmlhttpRequest !== 'function') {
          reject(new Error('GM_xmlhttpRequest not available'));
          return;
        }
        try {
          const req = {
            url,
            method,
            headers,
            responseType,
            onload: (res) => resolve(res),
            onerror: (err) => reject(new Error(err && err.error ? err.error : 'GM request failed')),
            ontimeout: () => reject(new Error('GM request timeout'))
          };
          if (body != null) {
            req.data = body;
          }
          GM_xmlhttpRequest(req);
        } catch (e) {
          reject(e);
        }
      });
    }

    // Get safe, preferably native storage methods from a fresh iframe context (in case other scripts patch the prototype)
    function captureNativeStorage() {
      try {
        const iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        iframe.src = 'about:blank';
        document.documentElement.appendChild(iframe);
        const win = iframe.contentWindow;
        const methods = {
          getItem: win.Storage.prototype.getItem,
          setItem: win.Storage.prototype.setItem,
          removeItem: win.Storage.prototype.removeItem,
          key: win.Storage.prototype.key
        };
        iframe.parentNode.removeChild(iframe);
        return methods;
      } catch (e) {
        // Fallback to current (possibly patched) prototype
        return {
          getItem: Storage.prototype.getItem,
          setItem: Storage.prototype.setItem,
          removeItem: Storage.prototype.removeItem,
          key: Storage.prototype.key
        };
      }
    }
    const NativeStorage = captureNativeStorage();

    // ===== Einfache Anleitung & Token-Input (UI) für Dropbox Cloud =====
    // parent: optional Container; wenn gesetzt, wird das Panel inline im Skripte-Tab gerendert
    function injectDropboxHelpPanel(parent) {
      try {
        if (document.getElementById('esm-dropbox-help-panel')) return;
        const inline = !!parent;
        const hiddenKey = inline ? 'ESM_DROPBOX_HELP_TAB_HIDDEN' : 'ESM_DROPBOX_HELP_HIDDEN';
        const hidden = localStorage.getItem(hiddenKey) === '1';
        const wrapper = document.createElement('div');
        wrapper.id = 'esm-dropbox-help-panel';
        wrapper.style.fontFamily = "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif";
        wrapper.style.color = '#111827';
        if (!inline) {
          wrapper.style.position = 'fixed';
          wrapper.style.bottom = '16px';
          wrapper.style.right = '16px';
          wrapper.style.zIndex = '2147483646';
        } else {
          wrapper.style.marginTop = '10px';
          wrapper.style.width = '100%';
        }

        const card = document.createElement('div');
        card.style.background = '#ffffff';
        card.style.border = '1px solid #e5e7eb';
        card.style.borderRadius = '12px';
        card.style.overflow = 'hidden';
        if (!inline) {
          card.style.boxShadow = '0 6px 18px rgba(0,0,0,0.12)';
          card.style.width = '340px';
          card.style.maxWidth = '90vw';
        } else {
          card.style.boxShadow = 'none';
          card.style.width = '100%';
          card.style.maxWidth = '100%';
        }

        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.alignItems = 'center';
        header.style.justifyContent = 'space-between';
        header.style.padding = '10px 12px';
        header.style.background = '#f9fafb';
        header.style.borderBottom = '1px solid #e5e7eb';
        const title = document.createElement('div');
        title.textContent = t('panelTitle');
        title.style.fontSize = '13px';
        title.style.fontWeight = '600';
        const controls = document.createElement('div');
        const btnHide = document.createElement('button');
        btnHide.textContent = hidden ? t('show') : t('hide');
        btnHide.style.fontSize = '12px';
        btnHide.style.border = '1px solid #d1d5db';
        btnHide.style.background = '#ffffff';
        btnHide.style.borderRadius = '8px';
        btnHide.style.padding = '4px 8px';
        btnHide.style.cursor = 'pointer';
        btnHide.addEventListener('click', () => {
          const contentVisible = content.style.display !== 'none';
          content.style.display = contentVisible ? 'none' : 'block';
          btnHide.textContent = contentVisible ? t('show') : t('hide');
          try { localStorage.setItem(hiddenKey, contentVisible ? '1' : '0'); } catch (_) {}
        });
        controls.appendChild(btnHide);
        header.appendChild(title);
        header.appendChild(controls);

        const content = document.createElement('div');
        content.style.padding = '12px';
        content.style.display = hidden ? 'none' : 'block';
        const inner = document.createElement('div');
        inner.style.fontSize = '12px';
        inner.style.lineHeight = '1.5';

        const howToP = document.createElement('p');
        howToP.style.margin = '0 0 8px';
        const howToBold = document.createElement('b');
        howToBold.textContent = t('howTo');
        howToP.appendChild(howToBold);
        inner.appendChild(howToP);

        const ol = document.createElement('ol');
        ol.style.margin = '0 0 10px 18px';
        ol.style.padding = '0';
        const steps = [t('step1'), null, t('step3'), t('step4')];
        steps.forEach((stepText, i) => {
          const li = document.createElement('li');
          if (i === 1) {
            li.textContent = t('step2') + ' ';
            const a = document.createElement('a');
            a.href = 'https://www.dropbox.com/developers/apps';
            a.target = '_blank';
            a.rel = 'noopener';
            a.textContent = 'https://www.dropbox.com/developers/apps';
            li.appendChild(a);
          } else {
            li.textContent = stepText;
          }
          ol.appendChild(li);
        });
        inner.appendChild(ol);

        const tokenLabel = document.createElement('div');
        tokenLabel.style.margin = '8px 0 4px';
        tokenLabel.textContent = t('genTokenLabel');
        inner.appendChild(tokenLabel);

        const input = document.createElement('input');
        input.id = 'esm-dropbox-token-input';
        input.type = 'text';
        input.placeholder = 'Dropbox Access Token';
        input.style.width = '100%';
        input.style.boxSizing = 'border-box';
        input.style.fontSize = '12px';
        input.style.padding = '6px 8px';
        input.style.border = '1px solid #d1d5db';
        input.style.borderRadius = '8px';
        inner.appendChild(input);

        const btnRow = document.createElement('div');
        btnRow.style.display = 'flex';
        btnRow.style.gap = '8px';
        btnRow.style.marginTop = '8px';

        const saveBtn = document.createElement('button');
        saveBtn.id = 'esm-dropbox-token-save';
        saveBtn.textContent = t('saveTokenBtn');
        saveBtn.style.flex = '1';
        saveBtn.style.fontSize = '12px';
        saveBtn.style.border = '1px solid #10b981';
        saveBtn.style.background = '#10b981';
        saveBtn.style.color = '#fff';
        saveBtn.style.borderRadius = '8px';
        saveBtn.style.padding = '6px 8px';
        saveBtn.style.cursor = 'pointer';
        btnRow.appendChild(saveBtn);

        const clearBtn = document.createElement('button');
        clearBtn.id = 'esm-dropbox-token-clear';
        clearBtn.textContent = t('clearTokenBtn');
        clearBtn.style.flex = '1';
        clearBtn.style.fontSize = '12px';
        clearBtn.style.border = '1px solid #ef4444';
        clearBtn.style.background = '#ef4444';
        clearBtn.style.color = '#fff';
        clearBtn.style.borderRadius = '8px';
        clearBtn.style.padding = '6px 8px';
        clearBtn.style.cursor = 'pointer';
        btnRow.appendChild(clearBtn);
        inner.appendChild(btnRow);

        const statusEl = document.createElement('div');
        statusEl.id = 'esm-dropbox-token-status';
        statusEl.style.marginTop = '8px';
        statusEl.style.fontSize = '12px';
        statusEl.style.color = '#374151';
        inner.appendChild(statusEl);

        content.appendChild(inner);

        card.appendChild(header);
        card.appendChild(content);
        wrapper.appendChild(card);
        if (inline && parent) {
          parent.appendChild(wrapper);
        } else {
          document.documentElement.appendChild(wrapper);
        }
        try {
          const stored = sessionStorage.getItem(DROPBOX_TOKEN_KEY);
          if (stored) input.value = stored;
        } catch (_) {}

        async function setStatus(msg, type) {
          statusEl.textContent = msg || '';
          statusEl.style.color = type === 'error' ? '#b91c1c' : (type === 'success' ? '#065f46' : '#374151');
        }

        saveBtn.addEventListener('click', async () => {
          const token = String(input.value || '').trim();
          if (!token) { setStatus(t('statusEnterToken'), 'error'); return; }
          try {
            setDropboxToken(token);
            // optional: validate account
            await getDropboxAccount(token);
            setStatus(t('statusSavedValidated'), 'success');
          } catch (e) {
            setStatus(`Fehler beim Speichern/Validieren: ${e && e.message ? e.message : e}`, 'error');
          }
        });

        clearBtn.addEventListener('click', async () => {
          try {
            clearDropboxToken();
            input.value = '';
            setStatus(t('statusSignedOut'), '');
          } catch (e) {
            setStatus(`Fehler beim Abmelden: ${e && e.message ? e.message : e}`, 'error');
          }
        });
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Hilfspanel nicht einfügen:', e);
      }
    }
    // Hinweis: Die Hilfe wird jetzt im Skripte-Tab inline eingefügt (siehe addScriptTab)

    // Authenticate with Dropbox (ohne OAuth, nur gespeicherter Access-Token)
    async function authenticateDropbox() {
      try {
        ESM_DIAG.log('Dropbox-Authentifizierung (nur Token) starten...');
        // Direkt gespeicherten Token verwenden
        const token = (dropboxAuth && String(dropboxAuth).trim()) || sessionStorage.getItem(DROPBOX_TOKEN_KEY);
        if (token && String(token).trim()) {
          try { await getDropboxAccount(token); } catch (_) {}
          dropboxAuth = String(token).trim();
          return dropboxAuth;
        }
        // Kein Token vorhanden -> Nutzer um persönlichen Access-Token bitten
        const manual = await promptForDropboxToken();
        dropboxAuth = manual;
        return manual;
      } catch (error) {
        ESM_DIAG.error('Dropbox-Authentifizierung (Token) fehlgeschlagen:', error);
        throw error;
      }
    }

    // Attempt to attach CSRF headers expected by waze.com endpoints
    function getCsrfHeaders() {
      try {
        const cookies = document.cookie.split(';').map(s => s.trim()).reduce((acc, cur) => {
          const idx = cur.indexOf('=');
          if (idx > -1) {
            try { acc[cur.slice(0, idx)] = decodeURIComponent(cur.slice(idx + 1)); } catch (_) {}
          }
          return acc;
        }, {});
        const rawToken = cookies['XSRF-TOKEN'] || cookies['xsrf-token'] || cookies['_csrf'] || cookies['csrf'] || cookies['csrf_token'] || cookies['X-CSRF-TOKEN'];
        // Validate token contains only safe characters before putting it in headers
        const token = (rawToken && /^[a-zA-Z0-9\-_.~+/=]{8,}$/.test(rawToken)) ? rawToken : null;
        if (!token) {
          ESM_DIAG.warn('No CSRF token cookie found for waze.com Dropbox endpoint');
          return {};
        }
        const headers = {
          'X-XSRF-TOKEN': token,
          'X-CSRF-TOKEN': token,
          'X-CSRF-Token': token
        };
        ESM_DIAG.log('Attached CSRF headers for waze.com Dropbox endpoint');
        return headers;
      } catch (e) {
        ESM_DIAG.warn('Failed to build CSRF headers:', e);
        return {};
      }
    }


    // Export backup to Dropbox
    const COOKIE_WARNING_KEY = 'ESM_COOKIE_WARNING_SHOWN';
    function warnCookiesOnce() {
      try { if (sessionStorage.getItem(COOKIE_WARNING_KEY)) return true; } catch (_) {}
      const msg = ESM_LANG === 'de'
        ? 'Dieses Backup enthält Cookies (inkl. Sitzungs- und Authentifizierungs-Cookies).\n\nBewahre die Datei sicher auf – wer sie besitzt, kann deine Sitzung übernehmen.\n\nFortfahren?'
        : 'This backup includes cookies (including session and authentication cookies).\n\nKeep the file secure — anyone who has it can replay your session.\n\nProceed?';
      const ok = confirm(msg);
      if (ok) { try { sessionStorage.setItem(COOKIE_WARNING_KEY, '1'); } catch (_) {} }
      return ok;
    }

    async function exportToDropbox() {
      if (!warnCookiesOnce()) return;
      try {
        ESM_DIAG.log('Starting Dropbox backup...');

        // Token ermitteln (kein OAuth)
        const accessToken = await authenticateDropbox();

        // Generate backup data (same as local export)
        const backup = {
          meta: {
            exportedAt: new Date().toISOString(),
            origin: location.origin,
            scriptVersion,
            backupType: 'dropbox'
          },
          localStorage: (() => {
            const out = {};
            try {
              const len = window.localStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null) {
                  out[k] = NativeStorage.getItem.call(window.localStorage, k);
                }
              }
            } catch (e) {
              Object.keys(window.localStorage).forEach(k => {
                try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; }
              });
            }
            return out;
          })(),
          sessionStorage: (() => {
            const out = {};
            try {
              const len = window.sessionStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.sessionStorage, i);
                if (k != null) {
                  out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
                }
              }
            } catch (e) {
              Object.keys(window.sessionStorage).forEach(k => {
                try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; }
              });
            }
            return out;
          })(),
          cookies: document.cookie
            .split(';')
            .map(c => {
              const [name, ...rest] = c.trim().split('=');
              return { name, value: rest.join('=') };
            })
            .filter(c => c.name),
          indexedDB: await backupIndexedDB()
        };

        const backupData = JSON.stringify(backup, null, 2);
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const fileName = `wme_settings_backup_${timestamp}.json`;
        const account = await getDropboxAccount(accessToken);
        const userFolder = `/WME_Backups/${account && account.account_id ? account.account_id : 'unknown'}`;

        // Upload to Dropbox
        let result;
        if (typeof GM_xmlhttpRequest === 'function') {
          // Use GM_xmlhttpRequest to bypass CSP and talk to Dropbox directly
          ESM_DIAG.log('Uploading backup to Dropbox via GM_xmlhttpRequest...');
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.CONTENT_API_URL}/files/upload`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/octet-stream',
              'Authorization': `Bearer ${accessToken}`,
              'Dropbox-API-Arg': JSON.stringify({
                path: `${userFolder}/${fileName}`,
                mode: 'add',
                autorename: true
              })
            },
            body: backupData
          }).catch(err => {
            ESM_DIAG.error('Dropbox upload (GM) failed:', err);
            throw new Error(`Network error during Dropbox upload (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox upload (GM) failed:', gmRes.status, errorText);
            throw new Error(`Upload failed: ${gmRes.status} - ${errorText}`);
          }
          try {
            result = JSON.parse(gmRes.responseText);
          } catch (err) {
            ESM_DIAG.error('Failed to parse upload response (GM):', err);
            throw new Error(`Invalid response format from Dropbox upload (GM): ${err.message}`);
          }
        } else {
          // Fallback to same-origin proxy (requires CSP + CSRF compliance in WME)
          ESM_DIAG.log('Uploading backup to Dropbox via same-origin proxy...');
          const response = await fetch(`${DROPBOX_CONFIG.PROXY_BASE_URL}/files/upload`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/octet-stream',
              'Dropbox-API-Arg': JSON.stringify({
                path: `${userFolder}/${fileName}`,
                mode: 'add',
                autorename: true
              }),
              ...getCsrfHeaders()
            },
            credentials: 'include',
            body: backupData
          }).catch(err => {
            ESM_DIAG.error('Dropbox upload fetch failed:', err);
            throw new Error(`Network error during Dropbox upload: ${err.message}`);
          });

          if (!response.ok) {
            const errorText = await response.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox upload failed:', response.status, response.statusText, errorText);
            throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`);
          }

          result = await response.json().catch(err => {
            ESM_DIAG.error('Failed to parse upload response:', err);
            throw new Error(`Invalid response format from Dropbox upload: ${err.message}`);
          });
        }
        ESM_DIAG.log('Backup uploaded to Dropbox:', result);

        alert(`${t('dropboxSaveSuccessPrefix')}\n\n${t('fileLabel')} ${fileName}\n${t('pathLabel')} /WME_Backups/${fileName}\n${t('sizeLabel')} ${Math.round(backupData.length / 1024)} ${t('kb')}`);

      } catch (error) {
        ESM_DIAG.error('Dropbox backup failed:', error);
        alert(`${t('dropboxSaveFailedPrefix')}\n\n${error.message}`);
      }
    }

    // Import backup from Dropbox
    async function importFromDropbox() {
      try {
        ESM_DIAG.log('Starting Dropbox restore...');

        // Token ermitteln (kein OAuth)
        let listJson;
        const accessToken = await authenticateDropbox();
        const account = await getDropboxAccount(accessToken);
        const userFolder = `/WME_Backups/${account && account.account_id ? account.account_id : 'unknown'}`;
        if (typeof GM_xmlhttpRequest === 'function') {
          ESM_DIAG.log('Listing backup files from Dropbox via GM_xmlhttpRequest...');
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL}/files/list_folder`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${accessToken}`
            },
            body: { path: userFolder, recursive: false }
          }).catch(err => {
            ESM_DIAG.error('Dropbox list (GM) failed:', err);
            throw new Error(`Network error during Dropbox file listing (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox list (GM) failed:', gmRes.status, errorText);
            throw new Error(`List request failed: ${gmRes.status} - ${errorText}`);
          }
          try {
            listJson = JSON.parse(gmRes.responseText);
          } catch (err) {
            ESM_DIAG.error('Failed to parse list response (GM):', err);
            throw new Error(`Invalid response format from Dropbox list (GM): ${err.message}`);
          }
        } else {
          ESM_DIAG.log('Listing backup files from Dropbox via same-origin proxy...');
          const listUrl = `${DROPBOX_CONFIG.PROXY_BASE_URL}/files/list_folder`;
          const listResponse = await fetch(listUrl, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              ...getCsrfHeaders()
            },
            credentials: 'include',
            body: JSON.stringify({
              path: userFolder,
              recursive: false
            })
          }).catch(err => {
            ESM_DIAG.error('Dropbox list fetch failed:', err);
            throw new Error(`Network error during Dropbox file listing: ${err.message}`);
          });
          if (!listResponse.ok) {
            const errorText = await listResponse.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox list request failed:', listResponse.status, listResponse.statusText, errorText);
            throw new Error(`List request failed: ${listResponse.status} ${listResponse.statusText} - ${errorText}`);
          }
          listJson = await listResponse.json().catch(err => {
            ESM_DIAG.error('Failed to parse list response:', err);
            throw new Error(`Invalid response format from Dropbox list: ${err.message}`);
          });
        }

        const files = listJson.entries ? listJson.entries.filter(entry =>
          entry['.tag'] === 'file' && entry.name.includes('wme_settings_backup')
        ) : [];

        if (!files || files.length === 0) {
          alert(t('dropboxNoBackups'));
          return;
        }

        // Show file selection dialog
        let fileList = 'Verfügbare Backups in Dropbox:\n\n';
        files.forEach((file, index) => {
          const date = new Date(file.client_modified).toLocaleString('de-DE');
          const size = file.size ? `${Math.round(file.size / 1024)} KB` : 'Unbekannt';
          fileList += `${index + 1}. ${file.name}\n   Erstellt: ${date}\n   Größe: ${size}\n\n`;
        });

        const selection = prompt(`${fileList}${t('restorePrompt')} (1-${files.length})`);
        if (!selection) return;

        const fileIndex = parseInt(selection, 10) - 1;
        if (fileIndex < 0 || fileIndex >= files.length) {
          alert(t('invalidSelection'));
          return;
        }

        const selectedFile = files[fileIndex];

        // Download the selected file from Dropbox
        let backupData;
        if (typeof GM_xmlhttpRequest === 'function') {
          ESM_DIAG.log('Downloading backup file from Dropbox via GM_xmlhttpRequest:', selectedFile.name);
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.CONTENT_API_URL}/files/download`, {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${accessToken}`,
              'Dropbox-API-Arg': JSON.stringify({ path: selectedFile.path_lower })
            },
            responseType: 'text'
          }).catch(err => {
            ESM_DIAG.error('Dropbox download (GM) failed:', err);
            throw new Error(`Network error during Dropbox download (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox download (GM) failed:', gmRes.status, errorText);
            throw new Error(`Download failed: ${gmRes.status} - ${errorText}`);
          }
          backupData = gmRes.responseText;
        } else {
          ESM_DIAG.log('Downloading backup file from Dropbox via same-origin proxy:', selectedFile.name);
          const downloadUrl = `${DROPBOX_CONFIG.PROXY_BASE_URL}/files/download`;
          const downloadResponse = await fetch(downloadUrl, {
            method: 'POST',
            headers: {
              'Dropbox-API-Arg': JSON.stringify({ path: selectedFile.path_lower }),
              ...getCsrfHeaders()
            },
            credentials: 'include'
          }).catch(err => {
            ESM_DIAG.error('Dropbox download fetch failed:', err);
            throw new Error(`Network error during Dropbox download: ${err.message}`);
          });
          if (!downloadResponse.ok) {
            const errorText = await downloadResponse.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox download failed:', downloadResponse.status, downloadResponse.statusText, errorText);
            throw new Error(`Download failed: ${downloadResponse.status} ${downloadResponse.statusText} - ${errorText}`);
          }
          backupData = await downloadResponse.text().catch(err => {
            ESM_DIAG.error('Failed to read download response:', err);
            throw new Error(`Failed to read backup data: ${err.message}`);
          });
        }

        try {
          const parsed = JSON.parse(backupData);
          importedData = parsed;
          let keyValuePairs = [];
          const originOk = !(parsed && parsed.meta && parsed.meta.origin) || parsed.meta.origin === location.origin;

          if (parsed && typeof parsed === 'object' && (parsed.localStorage || parsed.sessionStorage || parsed.cookies || parsed.indexedDB)) {
            if (parsed.localStorage) {
              for (const [k, v] of Object.entries(parsed.localStorage)) {
                keyValuePairs.push([`localStorage:${k}`, v]);
              }
            }
            if (parsed.sessionStorage && originOk) {
              for (const [k, v] of Object.entries(parsed.sessionStorage)) {
                keyValuePairs.push([`sessionStorage:${k}`, v]);
              }
            }
            if (Array.isArray(parsed.cookies) && originOk) {
              for (const c of parsed.cookies) {
                if (c && c.name != null) {
                  keyValuePairs.push([`cookie:${c.name}`, (c && typeof c.value !== 'undefined' && c.value !== null) ? c.value : '']);
                }
              }
            }
            if (Array.isArray(parsed.indexedDB)) {
              for (const dbBackup of parsed.indexedDB) {
                const dbName = (dbBackup && dbBackup.name) ? dbBackup.name : undefined;
                if (!dbName || !Array.isArray(dbBackup.stores)) continue;
                for (const storeBackup of dbBackup.stores) {
                  const storeName = (storeBackup && storeBackup.name) ? storeBackup.name : undefined;
                  if (!storeName || !Array.isArray(storeBackup.entries)) continue;
                  for (const entry of storeBackup.entries) {
                    let keyStr;
                    try { keyStr = JSON.stringify(entry.key); } catch (e) { ESM_DIAG.warn('Skipping IDB entry with non-serializable key:', e); continue; }
                    const keyLabel = `indexedDB:${dbName}/${storeName}:${keyStr}`;
                    const valueObj = {
                      db: dbName,
                      store: storeName,
                      key: entry.key,
                      value: entry.value,
                      keyPath: (storeBackup && typeof storeBackup.keyPath !== 'undefined') ? storeBackup.keyPath : null,
                      autoIncrement: !!storeBackup.autoIncrement,
                      indexes: Array.isArray(storeBackup.indexes) ? storeBackup.indexes : []
                    };
                    keyValuePairs.push([keyLabel, valueObj]);
                  }
                }
              }
            }
          } else {
            keyValuePairs = Object.entries(parsed);
          }

          displayKeyList(keyValuePairs);
          if (applyButton) applyButton.style.display = 'block';

          if (!originOk) {
            alert(t('foreignBackupHint'));
          } else {
            alert(`${t('dropboxLoadSuccessPrefix')}\n\n${t('fileLabel')} ${selectedFile.name}\n${t('foundEntriesLabel')} ${keyValuePairs.length}`);
          }

        } catch (parseError) {
          ESM_DIAG.error('Failed to parse backup data:', parseError);
          alert(t('invalidJson'));
        }

      } catch (error) {
        ESM_DIAG.error('Dropbox restore failed:', error);
        alert(`${t('dropboxRestoreFailedPrefix')}\n\n${error.message}`);
      }
    }

    // ===== Google Drive API Configuration & Functions =====
    const GDRIVE_CONFIG = {
      // OAuth Client ID (public — visible in redirect URLs anyway)
      // Create at https://console.cloud.google.com/apis/credentials
      //   Type: Web application
      //   Authorized JavaScript Origins: https://www.waze.com
      //   Authorized Redirect URI: https://www.waze.com/oauth2callback
      CLIENT_ID: '43613994100-ku10bn3ajmnfs1p7162mahraq402q6j7.apps.googleusercontent.com',
      // CLIENT_SECRET is NOT stored here — it lives in the Cloudflare Worker (token-proxy/).
      // Set TOKEN_PROXY_URL to your deployed worker URL.
      TOKEN_PROXY_URL: 'https://esm-token-proxy.devlinlab.com',
      SCOPES: 'https://www.googleapis.com/auth/drive.file',
      UPLOAD_URL: 'https://www.googleapis.com/upload/drive/v3/files',
      FILES_URL: 'https://www.googleapis.com/drive/v3/files'
    };
    const GDRIVE_TOKEN_KEY = 'ESM_GDRIVE_TOKEN';
    const GDRIVE_TOKEN_EXPIRES_KEY = 'ESM_GDRIVE_TOKEN_EXPIRES';
    const GDRIVE_TOKEN_SCOPE_KEY = 'ESM_GDRIVE_TOKEN_SCOPE';
    const GDRIVE_REFRESH_TOKEN_KEY = 'ESM_GDRIVE_REFRESH_TOKEN';
    function loadGdriveToken() {
      try {
        const token = sessionStorage.getItem(GDRIVE_TOKEN_KEY);
        const exp = parseInt(sessionStorage.getItem(GDRIVE_TOKEN_EXPIRES_KEY) || '0', 10);
        const savedScope = sessionStorage.getItem(GDRIVE_TOKEN_SCOPE_KEY) || '';
        if (token && exp && Date.now() < (exp - 60000) && savedScope === GDRIVE_CONFIG.SCOPES) { return token; }
        if (savedScope && savedScope !== GDRIVE_CONFIG.SCOPES) {
          ESM_DIAG.log('Google Drive Scope geändert, alter Token wird gelöscht.');
          clearGdriveToken();
        }
      } catch (_) {}
      return null;
    }

    function saveGdriveToken(token, expiresIn, refreshToken) {
      try {
        sessionStorage.setItem(GDRIVE_TOKEN_KEY, token);
        const expiresAt = expiresIn ? (Date.now() + (expiresIn * 1000)) : (Date.now() + 3600000);
        sessionStorage.setItem(GDRIVE_TOKEN_EXPIRES_KEY, String(expiresAt));
        sessionStorage.setItem(GDRIVE_TOKEN_SCOPE_KEY, GDRIVE_CONFIG.SCOPES);
        if (refreshToken) gmStorage.set(GDRIVE_REFRESH_TOKEN_KEY, refreshToken);
        ESM_DIAG.log('Google Drive Token gespeichert.');
      } catch (e) { ESM_DIAG.warn('Konnte Google Drive Token nicht speichern:', e); }
    }

    function clearGdriveToken() {
      try {
        sessionStorage.removeItem(GDRIVE_TOKEN_KEY);
        sessionStorage.removeItem(GDRIVE_TOKEN_EXPIRES_KEY);
        sessionStorage.removeItem(GDRIVE_TOKEN_SCOPE_KEY);
        gmStorage.remove(GDRIVE_REFRESH_TOKEN_KEY);
        gdriveFolderIdCache = null;
        ESM_DIAG.log('Google Drive Token gelöscht.');
      } catch (e) { ESM_DIAG.warn('Konnte Google Drive Token nicht löschen:', e); }
    }

    async function refreshGdriveToken() {
      const refreshToken = gmStorage.get(GDRIVE_REFRESH_TOKEN_KEY);
      if (!refreshToken) throw new Error('No Google refresh token available');
      const res = await gmFetch(`${GDRIVE_CONFIG.TOKEN_PROXY_URL}/refresh`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Origin': location.origin },
        body: JSON.stringify({ refresh_token: refreshToken })
      });
      if (res.status < 200 || res.status >= 300) {
        throw new Error(`Google token refresh failed (${res.status}): ${res.responseText || ''}`);
      }
      const json = JSON.parse(res.responseText);
      if (!json.access_token || typeof json.access_token !== 'string') {
        throw new Error('Google token refresh returned no access_token');
      }
      // Refresh response does not include a new refresh_token — keep the existing one
      saveGdriveToken(json.access_token, json.expires_in);
      return json.access_token;
    }

    // OAuth 2.0 Authorization Code + PKCE via Popup
    function oauthGooglePKCE() {
      return new Promise(async (resolve, reject) => {
        try {
          const verifier = generateCodeVerifier();
          const challenge = await sha256Base64Url(verifier);
          const state = base64urlFromBytes(crypto.getRandomValues(new Uint8Array(32)));
          try {
            sessionStorage.setItem('ESM_GOOGLE_OAUTH_STATE', state);
            sessionStorage.setItem('ESM_GOOGLE_OAUTH_VERIFIER', verifier);
          } catch (_) {}

          const redirectUri = location.origin + '/oauth2callback';
          const params = new URLSearchParams({
            client_id: GDRIVE_CONFIG.CLIENT_ID,
            redirect_uri: redirectUri,
            response_type: 'code',
            scope: GDRIVE_CONFIG.SCOPES,
            state,
            code_challenge: challenge,
            code_challenge_method: 'S256',
            access_type: 'offline',
            prompt: 'consent'
          }).toString();

          const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
          const popup = window.open(authUrl, 'esm_google_oauth', 'width=500,height=600');
          if (!popup) { reject(new Error(ESM_LANG === 'de' ? 'Popup blockiert – bitte Popup-Blocker deaktivieren' : 'Popup blocked – please disable popup blocker')); return; }

          const timer = setTimeout(() => { clearInterval(pollInterval); reject(new Error('Google OAuth timeout (3 min)')); }, 180000);

          const pollInterval = setInterval(async () => {
            try {
              if (!popup || popup.closed) { clearInterval(pollInterval); clearTimeout(timer); reject(new Error(ESM_LANG === 'de' ? 'OAuth-Fenster wurde geschlossen' : 'OAuth window was closed')); return; }
              const popupUrl = popup.location.href;
              if (popupUrl && popupUrl.startsWith(location.origin)) {
                clearInterval(pollInterval); clearTimeout(timer); popup.close();
                const urlObj = new URL(popupUrl);
                const code = urlObj.searchParams.get('code');
                const returnedState = urlObj.searchParams.get('state');
                const error = urlObj.searchParams.get('error');
                if (error) { reject(new Error(`Google OAuth: ${error}`)); return; }
                const savedState = sessionStorage.getItem('ESM_GOOGLE_OAUTH_STATE');
                if (!savedState || !returnedState || savedState !== returnedState) { reject(new Error('Invalid OAuth state')); return; }
                if (!code) { reject(new Error('No authorization code in response')); return; }
                // Exchange code for tokens
                const ver = sessionStorage.getItem('ESM_GOOGLE_OAUTH_VERIFIER') || verifier;
                try {
                  const tokenRes = await gmFetch(`${GDRIVE_CONFIG.TOKEN_PROXY_URL}/token`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Origin': location.origin },
                    body: JSON.stringify({
                      code,
                      code_verifier: ver,
                      redirect_uri: redirectUri
                    })
                  });
                  if (tokenRes.status < 200 || tokenRes.status >= 300) {
                    reject(new Error(`Token exchange failed (${tokenRes.status}): ${tokenRes.responseText || ''}`)); return;
                  }
                  const json = JSON.parse(tokenRes.responseText);
                  if (!json.access_token || typeof json.access_token !== 'string') {
                    reject(new Error('Token exchange returned no access_token')); return;
                  }
                  saveGdriveToken(json.access_token, json.expires_in, json.refresh_token);
                  resolve(json.access_token);
                } catch (err) { reject(err); }
              }
            } catch (e) { /* cross-origin – popup still on Google, keep polling */ }
          }, 500);
        } catch (err) { reject(err); }
      });
    }

    async function authenticateGdrive() {
      // 1. Valid cached access token
      const existing = loadGdriveToken();
      if (existing) return existing;
      // 2. Refresh token available — get a new access token silently
      if (gmStorage.get(GDRIVE_REFRESH_TOKEN_KEY)) {
        try { return await refreshGdriveToken(); } catch (e) {
          ESM_DIAG.warn('Google token refresh failed, falling back to login:', e);
          clearGdriveToken();
        }
      }
      // 3. Full PKCE login flow
      return await oauthGooglePKCE();
    }

    // Find or create the WME_Backups folder in Google Drive (visible in Drive UI)
    const GDRIVE_FOLDER_NAME = 'WME_Backups';
    let gdriveFolderIdCache = null;

    async function getOrCreateGdriveFolder(accessToken) {
      if (gdriveFolderIdCache) return gdriveFolderIdCache;
      // Search for existing folder
      const searchUrl = `${GDRIVE_CONFIG.FILES_URL}?q=${encodeURIComponent(`name='${GDRIVE_FOLDER_NAME}' and mimeType='application/vnd.google-apps.folder' and trashed=false`)}&fields=files(id,name)&spaces=drive`;
      const searchRes = await gmFetch(searchUrl, {
        method: 'GET',
        headers: { 'Authorization': `Bearer ${accessToken}` }
      });
      if (searchRes.status >= 200 && searchRes.status < 300) {
        const data = JSON.parse(searchRes.responseText);
        if (data.files && data.files.length > 0) {
          gdriveFolderIdCache = data.files[0].id;
          return gdriveFolderIdCache;
        }
      }
      // Create the folder
      const createRes = await gmFetch(GDRIVE_CONFIG.FILES_URL, {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: GDRIVE_FOLDER_NAME, mimeType: 'application/vnd.google-apps.folder' })
      });
      if (createRes.status < 200 || createRes.status >= 300) {
        throw new Error(`Failed to create WME_Backups folder: ${createRes.status}`);
      }
      const folder = JSON.parse(createRes.responseText);
      gdriveFolderIdCache = folder.id;
      return gdriveFolderIdCache;
    }

    // Export backup to Google Drive
    async function exportToGoogleDrive() {
      if (!warnCookiesOnce()) return;
      try {
        ESM_DIAG.log('Starting Google Drive backup...');
        if (typeof GM_xmlhttpRequest !== 'function') {
          throw new Error('GM_xmlhttpRequest not available – Google Drive requires Userscript manager');
        }
        const accessToken = await authenticateGdrive();

        // Generate backup data
        const backup = {
          meta: {
            exportedAt: new Date().toISOString(),
            origin: location.origin,
            scriptVersion,
            backupType: 'googledrive'
          },
          localStorage: (() => {
            const out = {};
            try {
              const len = window.localStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null) out[k] = NativeStorage.getItem.call(window.localStorage, k);
              }
            } catch (e) {
              Object.keys(window.localStorage).forEach(k => { try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; } });
            }
            return out;
          })(),
          sessionStorage: (() => {
            const out = {};
            try {
              const len = window.sessionStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.sessionStorage, i);
                if (k != null) out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
              }
            } catch (e) {
              Object.keys(window.sessionStorage).forEach(k => { try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; } });
            }
            return out;
          })(),
          cookies: document.cookie.split(';').map(c => { const [name, ...rest] = c.trim().split('='); return { name, value: rest.join('=') }; }).filter(c => c.name),
          indexedDB: await backupIndexedDB()
        };

        const backupData = JSON.stringify(backup, null, 2);
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const fileName = `wme_settings_backup_${timestamp}.json`;

        // Multipart upload to Google Drive
        const folderId = await getOrCreateGdriveFolder(accessToken);
        const boundary = 'ESM' + Array.from(crypto.getRandomValues(new Uint8Array(12))).map(b => b.toString(16).padStart(2, '0')).join('');
        const metadata = JSON.stringify({ name: fileName, parents: [folderId], mimeType: 'application/json' });
        const multipartBody =
          `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n` +
          `--${boundary}\r\nContent-Type: application/json\r\n\r\n${backupData}\r\n` +
          `--${boundary}--`;

        const uploadRes = await gmFetch(`${GDRIVE_CONFIG.UPLOAD_URL}?uploadType=multipart`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': `multipart/related; boundary=${boundary}`
          },
          body: multipartBody
        });

        if (uploadRes.status < 200 || uploadRes.status >= 300) {
          const errorText = uploadRes.responseText || 'Unknown error';
          throw new Error(`Upload failed: ${uploadRes.status} - ${errorText}`);
        }

        const result = JSON.parse(uploadRes.responseText);
        ESM_DIAG.log('Backup uploaded to Google Drive:', result);
        alert(`${t('gdriveSaveSuccess')}\n\n${t('fileLabel')} ${fileName}\n${t('sizeLabel')} ${Math.round(backupData.length / 1024)} ${t('kb')}`);
      } catch (error) {
        ESM_DIAG.error('Google Drive backup failed:', error);
        alert(`${t('gdriveSaveFailed')}\n\n${error.message}`);
      }
    }

    // Import backup from Google Drive
    async function importFromGoogleDrive() {
      try {
        ESM_DIAG.log('Starting Google Drive restore...');
        if (typeof GM_xmlhttpRequest !== 'function') {
          throw new Error('GM_xmlhttpRequest not available – Google Drive requires Userscript manager');
        }
        const accessToken = await authenticateGdrive();

        // List backup files in WME_Backups folder
        const folderId = await getOrCreateGdriveFolder(accessToken);
        const listUrl = `${GDRIVE_CONFIG.FILES_URL}?spaces=drive&q=${encodeURIComponent(`'${folderId}' in parents and name contains 'wme_settings_backup' and trashed=false`)}&fields=files(id,name,size,modifiedTime)&orderBy=modifiedTime%20desc`;
        ESM_DIAG.log('Google Drive list URL:', listUrl);
        const listRes = await gmFetch(listUrl, {
          method: 'GET',
          headers: { 'Authorization': `Bearer ${accessToken}` }
        });

        if (listRes.status < 200 || listRes.status >= 300) {
          throw new Error(`List request failed: ${listRes.status} - ${listRes.responseText || ''}`);
        }

        const listData = JSON.parse(listRes.responseText);
        const files = (listData.files || []).filter(f => f.name.includes('wme_settings_backup'));

        if (!files.length) {
          alert(t('gdriveNoBackups'));
          return;
        }

        let fileList = (ESM_LANG === 'de' ? 'Verfügbare Backups in Google Drive:\n\n' : 'Available backups in Google Drive:\n\n');
        files.forEach((file, index) => {
          const date = new Date(file.modifiedTime).toLocaleString(ESM_LANG === 'de' ? 'de-DE' : 'en-US');
          const size = file.size ? `${Math.round(parseInt(file.size) / 1024)} KB` : '?';
          fileList += `${index + 1}. ${file.name}\n   ${ESM_LANG === 'de' ? 'Geändert' : 'Modified'}: ${date}\n   ${t('sizeLabel')} ${size}\n\n`;
        });

        const selection = prompt(`${fileList}${t('restorePrompt')} (1-${files.length})`);
        if (!selection) return;

        const fileIndex = parseInt(selection, 10) - 1;
        if (fileIndex < 0 || fileIndex >= files.length) {
          alert(t('invalidSelection'));
          return;
        }

        const selectedFile = files[fileIndex];

        // Download file content
        const downloadRes = await gmFetch(`${GDRIVE_CONFIG.FILES_URL}/${selectedFile.id}?alt=media`, {
          method: 'GET',
          headers: { 'Authorization': `Bearer ${accessToken}` },
          responseType: 'text'
        });

        if (downloadRes.status < 200 || downloadRes.status >= 300) {
          throw new Error(`Download failed: ${downloadRes.status} - ${downloadRes.responseText || ''}`);
        }

        const backupData = downloadRes.responseText;
        try {
          const parsed = JSON.parse(backupData);
          importedData = parsed;
          let keyValuePairs = [];
          const originOk = !(parsed && parsed.meta && parsed.meta.origin) || parsed.meta.origin === location.origin;

          if (parsed && typeof parsed === 'object' && (parsed.localStorage || parsed.sessionStorage || parsed.cookies || parsed.indexedDB)) {
            if (parsed.localStorage) {
              for (const [k, v] of Object.entries(parsed.localStorage)) keyValuePairs.push([`localStorage:${k}`, v]);
            }
            if (parsed.sessionStorage && originOk) {
              for (const [k, v] of Object.entries(parsed.sessionStorage)) keyValuePairs.push([`sessionStorage:${k}`, v]);
            }
            if (Array.isArray(parsed.cookies) && originOk) {
              for (const c of parsed.cookies) {
                if (c && c.name != null) keyValuePairs.push([`cookie:${c.name}`, (c && typeof c.value !== 'undefined' && c.value !== null) ? c.value : '']);
              }
            }
            if (Array.isArray(parsed.indexedDB)) {
              for (const dbBackup of parsed.indexedDB) {
                const dbName = (dbBackup && dbBackup.name) ? dbBackup.name : undefined;
                if (!dbName || !Array.isArray(dbBackup.stores)) continue;
                for (const storeBackup of dbBackup.stores) {
                  const storeName = (storeBackup && storeBackup.name) ? storeBackup.name : undefined;
                  if (!storeName || !Array.isArray(storeBackup.entries)) continue;
                  for (const entry of storeBackup.entries) {
                    let keyStr;
                    try { keyStr = JSON.stringify(entry.key); } catch (e) { ESM_DIAG.warn('Skipping IDB entry with non-serializable key:', e); continue; }
                    const keyLabel = `indexedDB:${dbName}/${storeName}:${keyStr}`;
                    keyValuePairs.push([keyLabel, { db: dbName, store: storeName, key: entry.key, value: entry.value, keyPath: storeBackup.keyPath || null, autoIncrement: !!storeBackup.autoIncrement, indexes: Array.isArray(storeBackup.indexes) ? storeBackup.indexes : [] }]);
                  }
                }
              }
            }
          } else {
            keyValuePairs = Object.entries(parsed);
          }

          displayKeyList(keyValuePairs);
          if (applyButton) applyButton.style.display = 'block';

          if (!originOk) {
            alert(t('foreignBackupHint'));
          } else {
            alert(`${t('gdriveLoadSuccess')}\n\n${t('fileLabel')} ${selectedFile.name}\n${t('foundEntriesLabel')} ${keyValuePairs.length}`);
          }
        } catch (parseError) {
          ESM_DIAG.error('Failed to parse Google Drive backup data:', parseError);
          alert(t('invalidJson'));
        }
      } catch (error) {
        ESM_DIAG.error('Google Drive restore failed:', error);
        alert(`${t('gdriveRestoreFailed')}\n\n${error.message}`);
      }
    }

    // ===== Google Drive Help Panel (UI) – mit Login-Button statt Token-Eingabe =====
    function injectGdriveHelpPanel(parent) {
      try {
        if (document.getElementById('esm-gdrive-help-panel')) return;
        const inline = !!parent;
        const hiddenKey = inline ? 'ESM_GDRIVE_HELP_TAB_HIDDEN' : 'ESM_GDRIVE_HELP_HIDDEN';
        const hidden = localStorage.getItem(hiddenKey) === '1';
        const wrapper = document.createElement('div');
        wrapper.id = 'esm-gdrive-help-panel';
        wrapper.style.fontFamily = "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif";
        wrapper.style.color = '#111827';
        if (!inline) {
          wrapper.style.position = 'fixed'; wrapper.style.bottom = '16px'; wrapper.style.right = '16px'; wrapper.style.zIndex = '2147483645';
        } else {
          wrapper.style.marginTop = '10px'; wrapper.style.width = '100%';
        }
        const card = document.createElement('div');
        card.style.background = '#ffffff'; card.style.border = '1px solid #e5e7eb'; card.style.borderRadius = '12px'; card.style.overflow = 'hidden';
        if (!inline) { card.style.boxShadow = '0 6px 18px rgba(0,0,0,0.12)'; card.style.width = '340px'; card.style.maxWidth = '90vw'; }
        else { card.style.boxShadow = 'none'; card.style.width = '100%'; card.style.maxWidth = '100%'; }

        const header = document.createElement('div');
        header.style.display = 'flex'; header.style.alignItems = 'center'; header.style.justifyContent = 'space-between';
        header.style.padding = '10px 12px'; header.style.background = '#f9fafb'; header.style.borderBottom = '1px solid #e5e7eb';
        const title = document.createElement('div');
        title.textContent = t('gdrivePanelTitle'); title.style.fontSize = '13px'; title.style.fontWeight = '600';
        const btnHide = document.createElement('button');
        btnHide.textContent = hidden ? t('show') : t('hide');
        btnHide.style.fontSize = '12px'; btnHide.style.border = '1px solid #d1d5db'; btnHide.style.background = '#ffffff';
        btnHide.style.borderRadius = '8px'; btnHide.style.padding = '4px 8px'; btnHide.style.cursor = 'pointer';
        btnHide.addEventListener('click', () => {
          const vis = content.style.display !== 'none';
          content.style.display = vis ? 'none' : 'block';
          btnHide.textContent = vis ? t('show') : t('hide');
          try { localStorage.setItem(hiddenKey, vis ? '1' : '0'); } catch (_) {}
        });
        header.appendChild(title); header.appendChild(btnHide);

        const content = document.createElement('div');
        content.style.padding = '12px'; content.style.display = hidden ? 'none' : 'block';

        const statusEl = document.createElement('div');
        statusEl.id = 'esm-gdrive-status';
        statusEl.style.fontSize = '12px'; statusEl.style.marginBottom = '8px'; statusEl.style.color = '#374151';

        // Google-Style Login Button
        const loginBtn = document.createElement('button');
        loginBtn.style.display = 'flex'; loginBtn.style.alignItems = 'center'; loginBtn.style.gap = '8px';
        loginBtn.style.width = '100%'; loginBtn.style.padding = '8px 12px';
        loginBtn.style.fontSize = '13px'; loginBtn.style.fontWeight = '500';
        loginBtn.style.border = '1px solid #dadce0'; loginBtn.style.borderRadius = '8px';
        loginBtn.style.background = '#fff'; loginBtn.style.cursor = 'pointer';
        loginBtn.style.transition = 'background 150ms ease, box-shadow 150ms ease';
        loginBtn.style.boxShadow = '0 1px 3px rgba(0,0,0,0.08)';
        loginBtn.addEventListener('mouseenter', () => { loginBtn.style.background = '#f8f9fa'; loginBtn.style.boxShadow = '0 2px 6px rgba(0,0,0,0.12)'; });
        loginBtn.addEventListener('mouseleave', () => { loginBtn.style.background = '#fff'; loginBtn.style.boxShadow = '0 1px 3px rgba(0,0,0,0.08)'; });
        const gLogo = document.createElement('span');
        (function() {
          const NS = 'http://www.w3.org/2000/svg';
          const svg = document.createElementNS(NS, 'svg');
          svg.setAttribute('width', '18'); svg.setAttribute('height', '18'); svg.setAttribute('viewBox', '0 0 48 48');
          const paths = [
            ['#EA4335', 'M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z'],
            ['#4285F4', 'M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z'],
            ['#FBBC05', 'M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z'],
            ['#34A853', 'M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z']
          ];
          paths.forEach(([fill, d]) => {
            const p = document.createElementNS(NS, 'path');
            p.setAttribute('fill', fill); p.setAttribute('d', d);
            svg.appendChild(p);
          });
          gLogo.appendChild(svg);
        })();
        loginBtn.appendChild(gLogo);
        const loginText = document.createElement('span');
        loginBtn.appendChild(loginText);

        // Logout Button
        const logoutBtn = document.createElement('button');
        logoutBtn.style.width = '100%'; logoutBtn.style.marginTop = '6px';
        logoutBtn.style.fontSize = '12px'; logoutBtn.style.padding = '6px 8px';
        logoutBtn.style.border = '1px solid #ef4444'; logoutBtn.style.background = '#ef4444'; logoutBtn.style.color = '#fff';
        logoutBtn.style.borderRadius = '8px'; logoutBtn.style.cursor = 'pointer';

        function updateGdriveUI() {
          const token = loadGdriveToken();
          if (token) {
            statusEl.textContent = ESM_LANG === 'de' ? '✅ Mit Google Drive verbunden.' : '✅ Connected to Google Drive.';
            statusEl.style.color = '#065f46';
            loginBtn.style.display = 'none';
            logoutBtn.style.display = 'block';
            logoutBtn.textContent = ESM_LANG === 'de' ? 'Abmelden' : 'Sign out';
          } else {
            statusEl.textContent = ESM_LANG === 'de' ? 'Nicht angemeldet.' : 'Not signed in.';
            statusEl.style.color = '#374151';
            loginBtn.style.display = 'flex';
            loginText.textContent = ESM_LANG === 'de' ? 'Mit Google anmelden' : 'Sign in with Google';
            logoutBtn.style.display = 'none';
          }
        }

        loginBtn.addEventListener('click', async () => {
          try {
            statusEl.textContent = ESM_LANG === 'de' ? 'Anmeldung läuft...' : 'Signing in...';
            statusEl.style.color = '#374151';
            await oauthGooglePKCE();
            updateGdriveUI();
          } catch (e) {
            statusEl.textContent = `${ESM_LANG === 'de' ? 'Fehler' : 'Error'}: ${e.message}`;
            statusEl.style.color = '#b91c1c';
          }
        });

        logoutBtn.addEventListener('click', () => { clearGdriveToken(); updateGdriveUI(); });

        content.appendChild(statusEl);
        content.appendChild(loginBtn);
        content.appendChild(logoutBtn);
        updateGdriveUI();

        card.appendChild(header); card.appendChild(content); wrapper.appendChild(card);
        if (inline && parent) parent.appendChild(wrapper);
        else document.documentElement.appendChild(wrapper);
      } catch (e) {
        ESM_DIAG.warn('Konnte Google Drive Hilfspanel nicht einfügen:', e);
      }
    }

    // Export local storage data to a JSON file
    async function backupIndexedDB() {
      const result = [];
      let dbs = [];
      try {
        if (indexedDB.databases) {
          dbs = await indexedDB.databases();
        }
      } catch (e) {
        dbs = [];
      }
      for (const info of dbs) {
        if (!info || !info.name) continue;
        const name = info.name;
        const metaVersion = info.version;
        const backupForDb = await new Promise((resolve) => {
          const req = indexedDB.open(name);
          req.onerror = () => resolve(null);
          req.onupgradeneeded = () => {
            // No schema changes on read
          };
          req.onsuccess = () => {
            const db = req.result;
            const storeBackups = [];
            const stores = Array.from(db.objectStoreNames);
            const perStorePromises = stores.map((storeName) => new Promise((res) => {
              try {
                const tx = db.transaction([storeName], 'readonly');
                const store = tx.objectStore(storeName);
                const keyPath = store.keyPath || null;
                const autoIncrement = store.autoIncrement || false;
                const indexes = Array.from(store.indexNames).map((indexName) => {
                  const idx = store.index(indexName);
                  return { name: indexName, keyPath: idx.keyPath, unique: !!idx.unique, multiEntry: !!idx.multiEntry };
                });
                const out = { name: storeName, keyPath, autoIncrement, indexes, entries: [] };
                if (store.getAll && store.getAllKeys) {
                  const keysReq = store.getAllKeys();
                  const valsReq = store.getAll();
                  keysReq.onsuccess = () => {
                    const keys = keysReq.result || [];
                    valsReq.onsuccess = () => {
                      const vals = valsReq.result || [];
                      const len = Math.max(keys.length, vals.length);
                      for (let i = 0; i < len; i++) {
                        out.entries.push({ key: keys[i], value: vals[i] });
                      }
                      storeBackups.push(out);
                      res(true);
                    };
                    valsReq.onerror = () => { storeBackups.push(out); res(true); };
                  };
                  keysReq.onerror = () => { storeBackups.push(out); res(true); };
                } else {
                  const request = store.openCursor();
                  request.onsuccess = (e) => {
                    const cursor = e.target.result;
                    if (cursor) {
                      out.entries.push({ key: cursor.key, value: cursor.value });
                      cursor.continue();
                    } else {
                      storeBackups.push(out);
                      res(true);
                    }
                  };
                  request.onerror = () => { storeBackups.push(out); res(true); };
                }
              } catch (err) {
                res(true);
              }
            }));
            Promise.all(perStorePromises).then(() => {
              const backupObj = { name, version: db.version || metaVersion || null, stores: storeBackups };
              db.close();
              resolve(backupObj);
            });
          };
        });
        if (backupForDb) result.push(backupForDb);
      }
      return result;
    }
    // ===== Toast Notification =====
    function esmToast(message, type) {
      // type: 'success', 'error', 'info'
      try {
        // Altes Toast entfernen falls vorhanden
        var old = document.getElementById('esm-toast');
        if (old) old.remove();

        var colors = {
          success: { bg: '#1b5e20', border: '#4caf50', icon: '✅' },
          error:   { bg: '#b71c1c', border: '#f44336', icon: '❌' },
          info:    { bg: '#0d47a1', border: '#2196f3', icon: 'ℹ️' }
        };
        var c = colors[type] || colors.info;

        var toast = document.createElement('div');
        toast.id = 'esm-toast';
        toast.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:2147483647;'
          + 'background:' + c.bg + ';color:#fff;border:1px solid ' + c.border + ';'
          + 'border-radius:10px;padding:10px 16px;font-size:13px;font-family:system-ui,sans-serif;'
          + 'box-shadow:0 4px 16px rgba(0,0,0,0.35);display:flex;align-items:center;gap:8px;'
          + 'max-width:360px;opacity:0;transform:translateY(20px);'
          + 'transition:opacity 300ms ease,transform 300ms ease;pointer-events:auto;';

        var icon = document.createElement('span');
        icon.textContent = c.icon;
        icon.style.fontSize = '16px';
        toast.appendChild(icon);

        var text = document.createElement('span');
        text.textContent = message;
        text.style.flex = '1';
        toast.appendChild(text);

        var closeBtn = document.createElement('span');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText = 'cursor:pointer;opacity:0.7;font-size:14px;margin-left:8px;';
        closeBtn.addEventListener('click', function() { toast.remove(); });
        toast.appendChild(closeBtn);

        document.body.appendChild(toast);

        // Einblenden
        requestAnimationFrame(function() {
          requestAnimationFrame(function() {
            toast.style.opacity = '1';
            toast.style.transform = 'translateY(0)';
          });
        });

        // Auto-Ausblenden nach 4s (Fehler 6s)
        var duration = type === 'error' ? 6000 : 4000;
        setTimeout(function() {
          if (toast.parentNode) {
            toast.style.opacity = '0';
            toast.style.transform = 'translateY(20px)';
            setTimeout(function() { if (toast.parentNode) toast.remove(); }, 350);
          }
        }, duration);
      } catch (_) {
        // Fallback: console
        ESM_DIAG.log('Toast (' + type + '):', message);
      }
    }

    // ===== Auto Save Functions =====
    function toggleAutoSave() {
      var cb = document.getElementById('esm-autosave-cb');
      var sel = document.getElementById('esm-autosave-interval');
      if (!cb) return;

      autoSaveEnabled = cb.checked;
      try { localStorage.setItem(AUTO_SAVE_ENABLED_KEY, autoSaveEnabled ? '1' : '0'); } catch (_) {}

      if (sel) sel.disabled = !autoSaveEnabled;

      if (autoSaveInterval) { clearInterval(autoSaveInterval); autoSaveInterval = null; }

      if (autoSaveEnabled) {
        var ms = parseInt(sel ? sel.value : '300000');
        try { localStorage.setItem(AUTO_SAVE_INTERVAL_KEY, String(ms)); } catch (_) {}
        ESM_DIAG.log('Auto Save enabled (' + (ms / 60000) + ' min)');
        esmToast('⏱️ Auto Save ' + (ESM_LANG === 'de' ? 'aktiviert' : 'enabled') + ' (' + (ms / 60000) + ' min)', 'success');
        autoSaveInterval = setInterval(runAutoSave, ms);
      } else {
        ESM_DIAG.log('Auto Save disabled');
        esmToast('⏱️ Auto Save ' + (ESM_LANG === 'de' ? 'deaktiviert' : 'disabled'), 'info');
      }
    }

    function onAutoSaveIntervalChange() {
      var sel = document.getElementById('esm-autosave-interval');
      if (sel) { try { localStorage.setItem(AUTO_SAVE_INTERVAL_KEY, sel.value); } catch (_) {} }
      if (autoSaveEnabled) toggleAutoSave();
    }

    function onAutoSaveTargetChange() {
      var sel = document.getElementById('esm-autosave-target');
      if (sel) { try { localStorage.setItem(AUTO_SAVE_TARGET_KEY, sel.value); } catch (_) {} }
    }

    function getAutoSaveMaxBackups() {
      try {
        var v = parseInt(localStorage.getItem(AUTO_SAVE_MAX_BACKUPS_KEY) || '1', 10);
        return (!isNaN(v) && v >= 1 && v <= 10) ? v : 1;
      } catch (_) { return 1; }
    }

    // Dropbox: alte Auto-Save Backups auflisten und älteste löschen
    async function cleanupOldDropboxBackups(accessToken, userFolder, maxBackups) {
      try {
        var listRes = await gmFetch(DROPBOX_CONFIG.API_BASE_URL + '/files/list_folder', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + accessToken },
          body: { path: userFolder, recursive: false }
        });
        if (listRes.status < 200 || listRes.status >= 300) return;
        var listData = JSON.parse(listRes.responseText);
        var autoFiles = (listData.entries || []).filter(function(e) {
          return e['.tag'] === 'file' && e.name.indexOf('wme_autosave_') === 0;
        });
        autoFiles.sort(function(a, b) { return (a.client_modified || '').localeCompare(b.client_modified || ''); });
        var toDelete = autoFiles.length - maxBackups;
        for (var i = 0; i < toDelete; i++) {
          await gmFetch(DROPBOX_CONFIG.API_BASE_URL + '/files/delete_v2', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + accessToken },
            body: { path: autoFiles[i].path_lower }
          });
          ESM_DIAG.log('Deleted old Dropbox backup:', autoFiles[i].name);
        }
      } catch (e) { ESM_DIAG.warn('Cleanup old Dropbox backups failed:', e); }
    }

    // Google Drive: alte Auto-Save Backups auflisten und älteste löschen
    async function cleanupOldGdriveBackups(accessToken, maxBackups) {
      try {
        var folderId = await getOrCreateGdriveFolder(accessToken);
        var listUrl = GDRIVE_CONFIG.FILES_URL + '?spaces=drive&q=' + encodeURIComponent("'" + folderId + "' in parents and name contains 'wme_autosave_' and trashed=false") + '&fields=files(id,name,modifiedTime)&orderBy=modifiedTime%20asc';
        var listRes = await gmFetch(listUrl, {
          method: 'GET',
          headers: { 'Authorization': 'Bearer ' + accessToken }
        });
        if (listRes.status < 200 || listRes.status >= 300) return;
        var listData = JSON.parse(listRes.responseText);
        var files = listData.files || [];
        var toDelete = files.length - maxBackups;
        for (var i = 0; i < toDelete; i++) {
          await gmFetch(GDRIVE_CONFIG.FILES_URL + '/' + files[i].id, {
            method: 'DELETE',
            headers: { 'Authorization': 'Bearer ' + accessToken }
          });
          ESM_DIAG.log('Deleted old GDrive backup:', files[i].name);
        }
      } catch (e) { ESM_DIAG.warn('Cleanup old GDrive backups failed:', e); }
    }

    async function runAutoSave() {
      var targetSel = document.getElementById('esm-autosave-target');
      var target = (targetSel ? targetSel.value : 'local');
      var maxBackups = getAutoSaveMaxBackups();
      var targetNames = { local: t('autoSaveTargetLocal'), dropbox: t('autoSaveTargetDropbox'), gdrive: t('autoSaveTargetGdrive') };
      var targetName = targetNames[target] || target;
      ESM_DIAG.log('Auto Save running, target:', target, 'maxBackups:', maxBackups);
      esmToast(t('autoSaveRunning') + ' → ' + targetName, 'info');
      try {
        if (target === 'local') {
          await exportLocalStorage();
        } else if (target === 'dropbox') {
          await autoSaveToDropbox(maxBackups);
        } else if (target === 'gdrive') {
          await autoSaveToGdrive(maxBackups);
        }
        ESM_DIAG.log('Auto Save completed.');
        var time = new Date().toLocaleTimeString();
        esmToast(t('autoSaveDone') + ' (' + targetName + ', ' + time + ')', 'success');
      } catch (e) {
        ESM_DIAG.error('Auto Save failed:', e);
        esmToast(t('autoSaveFailed') + ' ' + (e.message || e), 'error');
      }
    }

    async function autoSaveToDropbox(maxBackups) {
      var accessToken = await authenticateDropbox();
      var account = await getDropboxAccount(accessToken);
      var userFolder = '/WME_Backups/' + (account && account.account_id ? account.account_id : 'unknown');

      var backup = await buildBackupData('dropbox');
      var backupData = JSON.stringify(backup, null, 2);

      var fileName;
      var uploadMode;
      if (maxBackups <= 1) {
        fileName = 'wme_autosave_latest.json';
        uploadMode = { '.tag': 'overwrite' };
      } else {
        var timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        fileName = 'wme_autosave_' + timestamp + '.json';
        uploadMode = 'add';
      }

      if (typeof GM_xmlhttpRequest === 'function') {
        var gmRes = await gmFetch(DROPBOX_CONFIG.CONTENT_API_URL + '/files/upload', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/octet-stream',
            'Authorization': 'Bearer ' + accessToken,
            'Dropbox-API-Arg': JSON.stringify({ path: userFolder + '/' + fileName, mode: uploadMode, autorename: true })
          },
          body: backupData
        });
        if (gmRes.status < 200 || gmRes.status >= 300) throw new Error('Dropbox upload failed: ' + gmRes.status);
      } else {
        var response = await fetch(DROPBOX_CONFIG.PROXY_BASE_URL + '/files/upload', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/octet-stream',
            'Dropbox-API-Arg': JSON.stringify({ path: userFolder + '/' + fileName, mode: uploadMode, autorename: true }),
            ...getCsrfHeaders()
          },
          credentials: 'include',
          body: backupData
        });
        if (!response.ok) throw new Error('Dropbox upload failed: ' + response.status);
      }

      if (maxBackups > 1) {
        await cleanupOldDropboxBackups(accessToken, userFolder, maxBackups);
      }
      ESM_DIAG.log('Auto Save Dropbox done:', fileName);
    }

    async function autoSaveToGdrive(maxBackups) {
      if (typeof GM_xmlhttpRequest !== 'function') throw new Error('GM_xmlhttpRequest not available');
      var accessToken = await authenticateGdrive();

      var backup = await buildBackupData('googledrive');
      var backupData = JSON.stringify(backup, null, 2);

      var fileName;
      if (maxBackups <= 1) {
        // Bei Überschreiben: bestehende Datei suchen und updaten
        var folderId = await getOrCreateGdriveFolder(accessToken);
        var listUrl = GDRIVE_CONFIG.FILES_URL + '?spaces=drive&q=' + encodeURIComponent("'" + folderId + "' in parents and name = 'wme_autosave_latest.json' and trashed=false") + '&fields=files(id,name)';
        var listRes = await gmFetch(listUrl, { method: 'GET', headers: { 'Authorization': 'Bearer ' + accessToken } });
        var existingId = null;
        if (listRes.status >= 200 && listRes.status < 300) {
          var listData = JSON.parse(listRes.responseText);
          if (listData.files && listData.files.length > 0) existingId = listData.files[0].id;
        }

        if (existingId) {
          // Update existing file
          var updateRes = await gmFetch(GDRIVE_CONFIG.UPLOAD_URL + '/' + existingId + '?uploadType=media', {
            method: 'PATCH',
            headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'application/json' },
            body: backupData
          });
          if (updateRes.status < 200 || updateRes.status >= 300) throw new Error('GDrive update failed: ' + updateRes.status);
        } else {
          fileName = 'wme_autosave_latest.json';
          var boundary = 'ESM' + Array.from(crypto.getRandomValues(new Uint8Array(12))).map(function(b){return b.toString(16).padStart(2,'0');}).join('');
          var metadata = JSON.stringify({ name: fileName, parents: [folderId], mimeType: 'application/json' });
          var multipartBody = '--' + boundary + '\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n' + metadata + '\r\n--' + boundary + '\r\nContent-Type: application/json\r\n\r\n' + backupData + '\r\n--' + boundary + '--';
          var uploadRes = await gmFetch(GDRIVE_CONFIG.UPLOAD_URL + '?uploadType=multipart', {
            method: 'POST',
            headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'multipart/related; boundary=' + boundary },
            body: multipartBody
          });
          if (uploadRes.status < 200 || uploadRes.status >= 300) throw new Error('GDrive upload failed: ' + uploadRes.status);
        }
      } else {
        var timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        fileName = 'wme_autosave_' + timestamp + '.json';
        var boundary2 = 'ESM' + Array.from(crypto.getRandomValues(new Uint8Array(12))).map(function(b){return b.toString(16).padStart(2,'0');}).join('');
        var folderId2 = await getOrCreateGdriveFolder(accessToken);
        var metadata2 = JSON.stringify({ name: fileName, parents: [folderId2], mimeType: 'application/json' });
        var multipartBody2 = '--' + boundary2 + '\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n' + metadata2 + '\r\n--' + boundary2 + '\r\nContent-Type: application/json\r\n\r\n' + backupData + '\r\n--' + boundary2 + '--';
        var uploadRes2 = await gmFetch(GDRIVE_CONFIG.UPLOAD_URL + '?uploadType=multipart', {
          method: 'POST',
          headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'multipart/related; boundary=' + boundary2 },
          body: multipartBody2
        });
        if (uploadRes2.status < 200 || uploadRes2.status >= 300) throw new Error('GDrive upload failed: ' + uploadRes2.status);
        await cleanupOldGdriveBackups(accessToken, maxBackups);
      }
      ESM_DIAG.log('Auto Save GDrive done.');
    }

    // Shared backup data builder for auto-save
    async function buildBackupData(backupType) {
      return {
        meta: {
          exportedAt: new Date().toISOString(),
          origin: location.origin,
          scriptVersion: scriptVersion,
          backupType: backupType || 'autosave'
        },
        localStorage: (function() {
          var out = {};
          try {
            var len = window.localStorage.length;
            for (var i = 0; i < len; i++) {
              var k = NativeStorage.key.call(window.localStorage, i);
              if (k != null) out[k] = NativeStorage.getItem.call(window.localStorage, k);
            }
          } catch (e) {
            Object.keys(window.localStorage).forEach(function(k) { try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; } });
          }
          return out;
        })(),
        sessionStorage: (function() {
          var out = {};
          try {
            var len = window.sessionStorage.length;
            for (var i = 0; i < len; i++) {
              var k = NativeStorage.key.call(window.sessionStorage, i);
              if (k != null) out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
            }
          } catch (e) {
            Object.keys(window.sessionStorage).forEach(function(k) { try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; } });
          }
          return out;
        })(),
        cookies: document.cookie.split(';').map(function(c) { var parts = c.trim().split('='); return { name: parts[0], value: parts.slice(1).join('=') }; }).filter(function(c) { return c.name; }),
        indexedDB: await backupIndexedDB()
      };
    }

    function loadAutoSaveSettings() {
      var cb = document.getElementById('esm-autosave-cb');
      var sel = document.getElementById('esm-autosave-interval');
      var targetSel = document.getElementById('esm-autosave-target');
      var slider = document.getElementById('esm-autosave-maxbackups');
      var sliderVal = document.getElementById('esm-autosave-maxbackups-val');
      try {
        var savedInterval = localStorage.getItem(AUTO_SAVE_INTERVAL_KEY) || '300000';
        if (sel) sel.value = savedInterval;
        var savedTarget = localStorage.getItem(AUTO_SAVE_TARGET_KEY) || 'local';
        if (targetSel) targetSel.value = savedTarget;
        var savedMax = localStorage.getItem(AUTO_SAVE_MAX_BACKUPS_KEY) || '1';
        if (slider) { slider.value = savedMax; }
        if (sliderVal) { sliderVal.textContent = savedMax; }
        var savedEnabled = localStorage.getItem(AUTO_SAVE_ENABLED_KEY) === '1';
        if (cb) {
          cb.checked = savedEnabled;
          if (savedEnabled) toggleAutoSave();
        }
      } catch (_) {}
    }

    function createAutoSaveUI() {
      var wrapper = document.createElement('div');
      wrapper.id = 'esm-autosave-section';
      wrapper.style.marginTop = '12px';
      wrapper.style.padding = '10px 12px';
      wrapper.style.background = '#f0f4ff';
      wrapper.style.border = '1px solid #c5cae9';
      wrapper.style.borderRadius = '10px';

      var title = document.createElement('div');
      title.textContent = t('autoSaveLabel');
      title.style.fontWeight = '700';
      title.style.fontSize = '13px';
      title.style.marginBottom = '8px';
      wrapper.appendChild(title);

      var cbLabel = document.createElement('label');
      cbLabel.style.display = 'flex';
      cbLabel.style.alignItems = 'center';
      cbLabel.style.gap = '6px';
      cbLabel.style.fontSize = '12px';
      cbLabel.style.cursor = 'pointer';
      cbLabel.style.marginBottom = '8px';
      var cb = document.createElement('input');
      cb.type = 'checkbox';
      cb.id = 'esm-autosave-cb';
      cb.addEventListener('change', toggleAutoSave);
      cbLabel.appendChild(cb);
      cbLabel.appendChild(document.createTextNode(t('autoSaveEnable')));
      wrapper.appendChild(cbLabel);

      var row = document.createElement('div');
      row.style.display = 'flex';
      row.style.gap = '10px';
      row.style.alignItems = 'center';
      row.style.flexWrap = 'wrap';

      var intLabel = document.createElement('label');
      intLabel.textContent = t('autoSaveInterval');
      intLabel.style.fontSize = '12px';
      intLabel.style.fontWeight = '500';
      row.appendChild(intLabel);

      var sel = document.createElement('select');
      sel.id = 'esm-autosave-interval';
      sel.disabled = true;
      sel.style.padding = '4px 6px';
      sel.style.border = '1px solid #bbb';
      sel.style.borderRadius = '6px';
      sel.style.fontSize = '12px';
      var intervals = [['300000','5 min'],['600000','10 min'],['900000','15 min'],['1800000','30 min'],['3600000','1 h']];
      intervals.forEach(function(iv) {
        var opt = document.createElement('option');
        opt.value = iv[0];
        opt.textContent = iv[1];
        sel.appendChild(opt);
      });
      sel.addEventListener('change', onAutoSaveIntervalChange);
      row.appendChild(sel);

      var tgtLabel = document.createElement('label');
      tgtLabel.textContent = t('autoSaveTarget');
      tgtLabel.style.fontSize = '12px';
      tgtLabel.style.fontWeight = '500';
      row.appendChild(tgtLabel);

      var tgtSel = document.createElement('select');
      tgtSel.id = 'esm-autosave-target';
      tgtSel.style.padding = '4px 6px';
      tgtSel.style.border = '1px solid #bbb';
      tgtSel.style.borderRadius = '6px';
      tgtSel.style.fontSize = '12px';
      var targets = [['local', t('autoSaveTargetLocal')],['dropbox', t('autoSaveTargetDropbox')],['gdrive', t('autoSaveTargetGdrive')]];
      targets.forEach(function(tg) {
        var opt = document.createElement('option');
        opt.value = tg[0];
        opt.textContent = tg[1];
        tgtSel.appendChild(opt);
      });
      tgtSel.addEventListener('change', onAutoSaveTargetChange);
      row.appendChild(tgtSel);

      wrapper.appendChild(row);

      // Slider für max. Backups
      var sliderRow = document.createElement('div');
      sliderRow.style.display = 'flex';
      sliderRow.style.gap = '8px';
      sliderRow.style.alignItems = 'center';
      sliderRow.style.marginTop = '10px';

      var sliderLabel = document.createElement('label');
      sliderLabel.textContent = t('autoSaveMaxBackups');
      sliderLabel.style.fontSize = '12px';
      sliderLabel.style.fontWeight = '500';
      sliderLabel.style.minWidth = '55px';
      sliderRow.appendChild(sliderLabel);

      var hintLeft = document.createElement('span');
      hintLeft.textContent = t('autoSaveMaxBackupsHint1');
      hintLeft.style.fontSize = '10px';
      hintLeft.style.color = '#888';
      hintLeft.style.whiteSpace = 'nowrap';
      sliderRow.appendChild(hintLeft);

      var slider = document.createElement('input');
      slider.type = 'range';
      slider.id = 'esm-autosave-maxbackups';
      slider.min = '1';
      slider.max = '10';
      slider.value = '1';
      slider.style.flex = '1';
      slider.style.cursor = 'pointer';
      slider.style.accentColor = '#3f51b5';
      sliderRow.appendChild(slider);

      var hintRight = document.createElement('span');
      hintRight.textContent = t('autoSaveMaxBackupsHint10');
      hintRight.style.fontSize = '10px';
      hintRight.style.color = '#888';
      hintRight.style.whiteSpace = 'nowrap';
      sliderRow.appendChild(hintRight);

      var sliderVal = document.createElement('span');
      sliderVal.id = 'esm-autosave-maxbackups-val';
      sliderVal.textContent = '1';
      sliderVal.style.fontSize = '13px';
      sliderVal.style.fontWeight = '700';
      sliderVal.style.minWidth = '20px';
      sliderVal.style.textAlign = 'center';
      sliderRow.appendChild(sliderVal);

      slider.addEventListener('input', function() {
        sliderVal.textContent = slider.value;
        try { localStorage.setItem(AUTO_SAVE_MAX_BACKUPS_KEY, slider.value); } catch (_) {}
      });

      wrapper.appendChild(sliderRow);

      return wrapper;
    }

    async function exportLocalStorage() {
      if (!warnCookiesOnce()) return;
      const backup = {
        meta: {
          exportedAt: new Date().toISOString(),
          origin: location.origin,
          scriptVersion
        },
        // Robust: Determine keys and read values ​​via the native storage API
        localStorage: (() => {
          const out = {};
          try {
            const len = window.localStorage.length;
            for (let i = 0; i < len; i++) {
              const k = NativeStorage.key.call(window.localStorage, i);
              if (k != null) {
                out[k] = NativeStorage.getItem.call(window.localStorage, k);
              }
            }
          } catch (e) {
            // Fallback if another script has patched key()/getItem
            Object.keys(window.localStorage).forEach(k => {
              try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; }
            });
          }
          return out;
        })(),
        sessionStorage: (() => {
          const out = {};
          try {
            const len = window.sessionStorage.length;
            for (let i = 0; i < len; i++) {
              const k = NativeStorage.key.call(window.sessionStorage, i);
              if (k != null) {
                out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
              }
            }
          } catch (e) {
            Object.keys(window.sessionStorage).forEach(k => {
              try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; }
            });
          }
          return out;
        })(),
        cookies: document.cookie
          .split(';')
          .map(c => {
            const [name, ...rest] = c.trim().split('=');
            return { name, value: rest.join('=') };
          })
          .filter(c => c.name),
        indexedDB: await backupIndexedDB()
      };
      const data = JSON.stringify(backup, null, 2);
      const file = new Blob([data], { type: 'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(file);
      const ts = new Date().toISOString().replace(/[:.]/g, '-');
      a.download = `wme_settings_backup_${ts}.json`;
      a.click();
    }

    // Import local storage data from a JSON file
    function importLocalStorage() {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = 'application/json';
      input.onchange = function (event) {
        const file = event.target.files[0];
        const reader = new FileReader();
        reader.onload = function () {
          try {
            const parsed = JSON.parse(reader.result);
            importedData = parsed;
            let keyValuePairs = [];
            const originOk = !(parsed && parsed.meta && parsed.meta.origin) || parsed.meta.origin === location.origin;
            if (parsed && typeof parsed === 'object' && (parsed.localStorage || parsed.sessionStorage || parsed.cookies || parsed.indexedDB)) {
              if (parsed.localStorage) {
                for (const [k, v] of Object.entries(parsed.localStorage)) {
                  keyValuePairs.push([`localStorage:${k}`, v]);
                }
              }
              if (parsed.sessionStorage && originOk) {
                for (const [k, v] of Object.entries(parsed.sessionStorage)) {
                  keyValuePairs.push([`sessionStorage:${k}`, v]);
                }
              }
              if (Array.isArray(parsed.cookies) && originOk) {
                for (const c of parsed.cookies) {
                  if (c && c.name != null) {
                    keyValuePairs.push([`cookie:${c.name}`, (c && typeof c.value !== 'undefined' && c.value !== null) ? c.value : '']);
                  }
                }
              }
              if (Array.isArray(parsed.indexedDB)) {
                for (const dbBackup of parsed.indexedDB) {
                  const dbName = (dbBackup && dbBackup.name) ? dbBackup.name : undefined;
                  if (!dbName || !Array.isArray(dbBackup.stores)) continue;
                  for (const storeBackup of dbBackup.stores) {
                    const storeName = (storeBackup && storeBackup.name) ? storeBackup.name : undefined;
                    if (!storeName || !Array.isArray(storeBackup.entries)) continue;
                    for (const entry of storeBackup.entries) {
                      let keyStr;
                      try { keyStr = JSON.stringify(entry.key); } catch (e) { ESM_DIAG.warn('Skipping IDB entry with non-serializable key:', e); continue; }
                      const keyLabel = `indexedDB:${dbName}/${storeName}:${keyStr}`;
                      const valueObj = {
                        db: dbName,
                        store: storeName,
                        key: entry.key,
                        value: entry.value,
                        keyPath: (storeBackup && typeof storeBackup.keyPath !== 'undefined') ? storeBackup.keyPath : null,
                        autoIncrement: !!storeBackup.autoIncrement,
                        indexes: Array.isArray(storeBackup.indexes) ? storeBackup.indexes : []
                      };
                      keyValuePairs.push([keyLabel, valueObj]);
                    }
                  }
                }
              }
            } else {
              keyValuePairs = Object.entries(parsed);
            }
            displayKeyList(keyValuePairs);
            if (applyButton) applyButton.style.display = 'block';
            if (!originOk) {
              alert(t('foreignBackupHint'));
            } else {
              alert(t('fileReadSuccess'));
            }
          } catch (error) {
            // Only display the error message if the import fails
            console.error(error);
            alert(t('invalidJson'));
          };
        };
        reader.onerror = function () {
          alert(t('fileReadError'));
        };
        reader.readAsText(file);
      };
      input.click();
    }

    // Display the list of keys for selection
    function displayKeyList(keyValuePairs) {
      const container = document.getElementById('key-list-container');
      container.innerHTML = ''; // Clear existing list

      // Select All button
      const selectAllButton = document.createElement('button');
    selectAllButton.textContent = t('selectAll');
      selectAllButton.addEventListener('click', function () {
        const checkboxes = container.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach((checkbox) => {
          checkbox.checked = true;
        });
      });
      container.appendChild(selectAllButton);

      // Deselect All button
      const deselectAllButton = document.createElement('button');
    deselectAllButton.textContent = t('deselectAll');
      deselectAllButton.addEventListener('click', function () {
        const checkboxes = container.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach((checkbox) => {
          checkbox.checked = false;
        });
      });
      container.appendChild(deselectAllButton);

      container.appendChild(document.createElement('br'));

      // Key checkboxes
      keyValuePairs.forEach(([key, value]) => {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = key;
        checkbox.value = key;
        checkbox.checked = true;
        container.appendChild(checkbox);

        const label = document.createElement('label');
        label.htmlFor = key;
        label.textContent = key;
        container.appendChild(label);

        const hiddenValue = document.createElement('input');
        hiddenValue.type = 'hidden';
        hiddenValue.value = typeof value === 'string' ? value : JSON.stringify(value);
        container.appendChild(hiddenValue);

        container.appendChild(document.createElement('br'));
      });

      // Apply button
      applyButton = document.createElement('button');
    applyButton.textContent = t('apply');
      applyButton.addEventListener('click', applyImport);
      container.appendChild(applyButton);
    }

    // Apply the selected key-value pairs from the JSON file
    async function applyImport() {
      const selectedPairs = getSelectedPairs();
      if (selectedPairs.length === 0) {
        alert('No keys selected. Nothing to import.');
        return;
      }

      const { counts, failures } = await applyPairs(selectedPairs);
      importedData = null; // Clear sensitive backup data from memory after apply

      const summary = `Import Completed\n- localStorage: ${counts.local}\n- sessionStorage: ${counts.session}\n- Cookies: ${counts.cookie}\n- IndexedDB: ${counts.idb}` + (failures.length ? `\n\nError (first ${Math.min(5, failures.length)}):\n${failures.slice(0,5).join('\n')}${failures.length > 5 ? `\n... (${failures.length - 5} more)` : ''}` : '');
      alert(summary);

      // Prompt to refresh the page
      if (confirm('The import was successful. Press ok to refresh page.')) {
        try {
          // Stash for post-reload reapply to prevent other scripts from overwriting restored values during startup
          sessionStorage.setItem(REAPPLY_STASH_KEY, JSON.stringify({ origin: location.origin, items: selectedPairs }));
        } catch (e) {
          // ignore stash errors
        }
        location.reload();
      }
    }

    // Get the selected key-value pairs
    function getSelectedPairs() {
      const checkboxes = document.querySelectorAll('#key-list-container input[type="checkbox"]');
      const selectedPairs = [];
      checkboxes.forEach((checkbox) => {
        if (checkbox.checked) {
          const key = checkbox.value;
          const valueStr = checkbox.nextElementSibling.nextElementSibling.value;
          // For IndexedDB entries, the value is JSON-encoded; for all other storages, the string must remain unchanged
          if (key.startsWith('indexedDB:')) {
            let parsedValue;
            try {
              parsedValue = JSON.parse(valueStr);
            } catch (e) {
              parsedValue = valueStr; // Fallback in case something unexpected happens
            }
            selectedPairs.push([key, parsedValue]);
          } else {
            selectedPairs.push([key, valueStr]);
          }
        }
      });
      return selectedPairs;
    }

    // Helper to apply selected pairs across storage types and IndexedDB (inside IIFE)
    const KEY_MAX_LEN = 512;
    const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
    function isValidStorageKey(key) {
      return typeof key === 'string' && key.length > 0 && key.length <= KEY_MAX_LEN && !DANGEROUS_KEYS.has(key);
    }

    async function applyPairs(selectedPairs) {
      const counts = { local: 0, session: 0, cookie: 0, idb: 0 };
      const failures = [];
      const sourceOrigin = (importedData && importedData.meta && importedData.meta.origin) ? importedData.meta.origin : undefined;
      const sameOrigin = !sourceOrigin || sourceOrigin === location.origin;

      for (const [fullKey, value] of selectedPairs) {
        try {
          const colonIdx = fullKey.indexOf(':');
          if (colonIdx < 0) {
            // Fallback: without type prefix we treat it as localStorage
            if (!isValidStorageKey(fullKey)) {
              failures.push(`${fullKey} skipped (invalid key)`);
              ESM_DIAG.warn('Skipping invalid key:', fullKey);
              continue;
            }
            localStorage.setItem(fullKey, value);
            counts.local++;
            continue;
          }
          const type = fullKey.slice(0, colonIdx);
          const rest = fullKey.slice(colonIdx + 1);

          if (type !== 'indexedDB' && !isValidStorageKey(rest)) {
            failures.push(`${fullKey} skipped (invalid key)`);
            ESM_DIAG.warn('Skipping invalid key:', fullKey);
            continue;
          }

          if (type === 'localStorage') {
            try {
              NativeStorage.setItem.call(window.localStorage, rest, value);
            } catch (_) {
              window.localStorage.setItem(rest, value);
            }
            counts.local++;
          } else if (type === 'sessionStorage') {
            if (!sameOrigin) {
              failures.push(`sessionStorage:${rest} skipped (Origin differs)`);
            } else {
              try {
                NativeStorage.setItem.call(window.sessionStorage, rest, value);
              } catch (_) {
                window.sessionStorage.setItem(rest, value);
              }
              counts.session++;
            }
          } else if (type === 'cookie') {
            if (!sameOrigin) {
              failures.push(`cookie:${rest} skipped (Origin differs)`);
            } else {
              // Sanitize the cookie name and validate it contains only legal cookie-name chars.
              const safeCookieName = String(rest).split(';')[0].trim();
              if (!safeCookieName || !/^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/.test(safeCookieName)) {
                failures.push(`cookie:${rest} skipped (invalid cookie name characters)`);
                ESM_DIAG.warn('Skipping cookie with invalid name:', rest);
                continue;
              }
              const safeCookieValue = encodeURIComponent(String(value));
              const secureFlag = location.protocol === 'https:' ? '; Secure' : '';
              document.cookie = `${safeCookieName}=${safeCookieValue}; path=/; SameSite=Strict${secureFlag}`;
              counts.cookie++;
            }
          } else if (type === 'indexedDB') {
            try {
              // The value is already a complete record object (db, store, key, value, keyPath, autoIncrement, indexes)
              await writeIndexedDBRecord(value);
              counts.idb++;
            } catch (e) {
              failures.push(`indexedDB:${rest} -> ${e && e.message ? e.message : e}`);
            }
          } else {
            // Unknown type: treat as localStorage
            localStorage.setItem(rest, value);
            counts.local++;
          }
        } catch (err) {
          failures.push(`${fullKey} -> ${err && err.message ? err.message : err}`);
        }
      }

      return { counts, failures };
    }

    // Create and add the tab for the script (robust multi-path)
    function addScriptTab() {
      if (typeof window !== 'undefined' && window.uiMounted) {
        ESM_DIAG.log('UI already mounted, skipping addScriptTab');
        return;
      }

      const haveWUserscripts = typeof W !== 'undefined' && W && W.userscripts && typeof W.userscripts.registerSidebarTab === 'function';
      const haveWazeWrapTab = typeof WazeWrap !== 'undefined' && WazeWrap && WazeWrap.Interface && typeof WazeWrap.Interface.Tab === 'function';

      if (haveWUserscripts) {
        try {
          const scriptId = 'easy-storage-manager-tab';
          const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(scriptId);

          tabLabel.innerText = '💾';
          tabLabel.title = t('scriptTitle');

          const description = document.createElement('p');
          description.style.fontWeight = 'bold';
    description.textContent = t('scriptTitle');
          tabPane.appendChild(description);

const text = document.createElement('p');
text.textContent = t('importExportDesc');
          tabPane.appendChild(text);

const importButton = document.createElement('button');
importButton.textContent = t('importBackup');
importButton.title = t('importBackupTitle');
          importButton.addEventListener('click', function() {
            if (typeof window.importLocalStorage === 'function') {
              window.importLocalStorage();
            } else if (typeof importLocalStorage === 'function') {
              importLocalStorage();
            } else {
              try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {}
              alert('Import function not available');
            }
          });
    // keep title already set
          importButton.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
          importButton.style.color = '#fff';
          importButton.style.border = 'none';
          importButton.style.borderRadius = '10px';
          importButton.style.padding = '8px 12px';
          importButton.style.fontWeight = '600';
          importButton.style.cursor = 'pointer';
          importButton.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
          importButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          importButton.style.whiteSpace = 'nowrap';
          importButton.style.display = 'inline-flex';
          importButton.style.alignItems = 'center';
          importButton.style.flex = '0 0 auto';
          importButton.style.gap = '4px';
          importButton.style.fontSize = '13px';
          importButton.style.lineHeight = '18px';
          importButton.style.width = 'auto';
          importButton.addEventListener('mouseenter', () => { importButton.style.filter = 'brightness(1.08)'; importButton.style.boxShadow = '0 6px 14px rgba(25,118,210,0.45)'; });
          importButton.addEventListener('mouseleave', () => { importButton.style.filter = ''; importButton.style.boxShadow = '0 3px 8px rgba(25,118,210,0.35)'; });

const exportButton = document.createElement('button');
exportButton.textContent = t('exportBackup');
exportButton.title = t('exportBackupTitle');
          exportButton.addEventListener('click', function() {
            if (typeof window.exportLocalStorage === 'function') {
              window.exportLocalStorage();
            } else if (typeof exportLocalStorage === 'function') {
              exportLocalStorage();
            } else {
              try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {}
              alert('Export function not available');
            }
          });
    // keep title already set
          exportButton.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
          exportButton.style.color = '#fff';
          exportButton.style.border = 'none';
          exportButton.style.borderRadius = '10px';
          exportButton.style.padding = '8px 12px';
          exportButton.style.fontWeight = '600';
          exportButton.style.cursor = 'pointer';
          exportButton.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
          exportButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          exportButton.style.whiteSpace = 'nowrap';
          exportButton.style.display = 'inline-flex';
          exportButton.style.alignItems = 'center';
          exportButton.style.flex = '0 0 auto';
          exportButton.style.gap = '4px';
          exportButton.style.fontSize = '13px';
          exportButton.style.lineHeight = '18px';
          exportButton.style.width = 'auto';
          exportButton.addEventListener('mouseenter', () => { exportButton.style.filter = 'brightness(1.08)'; exportButton.style.boxShadow = '0 6px 14px rgba(46,125,50,0.45)'; });
          exportButton.addEventListener('mouseleave', () => { exportButton.style.filter = ''; exportButton.style.boxShadow = '0 3px 8px rgba(46,125,50,0.35)'; });

          const buttonContainer = document.createElement('div');
          buttonContainer.style.display = 'flex';
          buttonContainer.style.gap = '8px';
          buttonContainer.style.marginTop = '8px';
          buttonContainer.style.justifyContent = 'center';
          buttonContainer.style.alignItems = 'center';
          buttonContainer.style.width = '100%';
          // Reihenfolge: zuerst Lokal Speichern (Export), dann Wiederherstellen (Import)
          exportButton.style.order = '1';
          importButton.style.order = '2';
          buttonContainer.appendChild(exportButton);
          buttonContainer.appendChild(importButton);
          tabPane.appendChild(buttonContainer);

          // Dropbox Cloud Backup Buttons
const cloudBackupButton = document.createElement('button');
cloudBackupButton.textContent = t('cloudBackup');
cloudBackupButton.title = t('cloudBackupTitle');
          cloudBackupButton.addEventListener('click', function() {
            if (typeof exportToDropbox === 'function') {
          exportToDropbox();
            } else {
try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox export function not available'); } catch (_) {}
alert(t('dropboxExportUnavailable'));
            }
          });
    // title already set
          cloudBackupButton.style.backgroundImage = 'linear-gradient(180deg, #ff9800, #f57c00)';
          cloudBackupButton.style.color = '#fff';
          cloudBackupButton.style.border = 'none';
          cloudBackupButton.style.borderRadius = '10px';
          cloudBackupButton.style.padding = '8px 12px';
          cloudBackupButton.style.fontWeight = '600';
          cloudBackupButton.style.cursor = 'pointer';
          cloudBackupButton.style.boxShadow = '0 3px 8px rgba(245, 124, 0, 0.35)';
          cloudBackupButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          cloudBackupButton.style.whiteSpace = 'nowrap';
          cloudBackupButton.style.display = 'inline-flex';
          cloudBackupButton.style.alignItems = 'center';
          cloudBackupButton.style.flex = '0 0 auto';
          cloudBackupButton.style.gap = '4px';
          cloudBackupButton.style.fontSize = '13px';
          cloudBackupButton.style.lineHeight = '18px';
          cloudBackupButton.style.width = 'auto';
          cloudBackupButton.addEventListener('mouseenter', () => { cloudBackupButton.style.filter = 'brightness(1.08)'; cloudBackupButton.style.boxShadow = '0 6px 14px rgba(245,124,0,0.45)'; });
          cloudBackupButton.addEventListener('mouseleave', () => { cloudBackupButton.style.filter = ''; cloudBackupButton.style.boxShadow = '0 3px 8px rgba(245,124,0,0.35)'; });

const cloudRestoreButton = document.createElement('button');
cloudRestoreButton.textContent = t('cloudRestore');
cloudRestoreButton.title = t('cloudRestoreTitle');
          cloudRestoreButton.addEventListener('click', function() {
            if (typeof importFromDropbox === 'function') {
          importFromDropbox();
            } else {
try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox restore function not available'); } catch (_) {}
alert(t('dropboxImportUnavailable'));
            }
          });
    // title already set
          cloudRestoreButton.style.backgroundImage = 'linear-gradient(180deg, #9c27b0, #7b1fa2)';
          cloudRestoreButton.style.color = '#fff';
          cloudRestoreButton.style.border = 'none';
          cloudRestoreButton.style.borderRadius = '10px';
          cloudRestoreButton.style.padding = '8px 12px';
          cloudRestoreButton.style.fontWeight = '600';
          cloudRestoreButton.style.cursor = 'pointer';
          cloudRestoreButton.style.boxShadow = '0 3px 8px rgba(123, 31, 162, 0.35)';
          cloudRestoreButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          cloudRestoreButton.style.whiteSpace = 'nowrap';
          cloudRestoreButton.style.display = 'inline-flex';
          cloudRestoreButton.style.alignItems = 'center';
          cloudRestoreButton.style.flex = '0 0 auto';
          cloudRestoreButton.style.gap = '4px';
          cloudRestoreButton.style.fontSize = '13px';
          cloudRestoreButton.style.lineHeight = '18px';
          cloudRestoreButton.style.width = 'auto';
          cloudRestoreButton.addEventListener('mouseenter', () => { cloudRestoreButton.style.filter = 'brightness(1.08)'; cloudRestoreButton.style.boxShadow = '0 6px 14px rgba(123,31,162,0.45)'; });
          cloudRestoreButton.addEventListener('mouseleave', () => { cloudRestoreButton.style.filter = ''; cloudRestoreButton.style.boxShadow = '0 3px 8px rgba(123,31,162,0.35)'; });

          const cloudButtonContainer = document.createElement('div');
          cloudButtonContainer.style.display = 'flex';
          cloudButtonContainer.style.gap = '8px';
          cloudButtonContainer.style.marginTop = '8px';
          cloudButtonContainer.style.justifyContent = 'center';
          cloudButtonContainer.style.alignItems = 'center';
          cloudButtonContainer.style.width = '100%';
          cloudButtonContainer.appendChild(cloudBackupButton);
          cloudButtonContainer.appendChild(cloudRestoreButton);
          tabPane.appendChild(cloudButtonContainer);

          // Google Drive Backup Buttons
          const gdriveBackupButton = document.createElement('button');
          gdriveBackupButton.textContent = t('gdriveBackup');
          gdriveBackupButton.title = t('gdriveBackupTitle');
          gdriveBackupButton.addEventListener('click', function() {
            if (typeof exportToGoogleDrive === 'function') { exportToGoogleDrive(); }
            else { alert(t('gdriveExportUnavailable')); }
          });
          gdriveBackupButton.style.backgroundImage = 'linear-gradient(180deg, #4285f4, #1a73e8)';
          gdriveBackupButton.style.color = '#fff'; gdriveBackupButton.style.border = 'none'; gdriveBackupButton.style.borderRadius = '10px';
          gdriveBackupButton.style.padding = '8px 12px'; gdriveBackupButton.style.fontWeight = '600'; gdriveBackupButton.style.cursor = 'pointer';
          gdriveBackupButton.style.boxShadow = '0 3px 8px rgba(26, 115, 232, 0.35)';
          gdriveBackupButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          gdriveBackupButton.style.whiteSpace = 'nowrap'; gdriveBackupButton.style.display = 'inline-flex'; gdriveBackupButton.style.alignItems = 'center';
          gdriveBackupButton.style.flex = '0 0 auto'; gdriveBackupButton.style.gap = '4px'; gdriveBackupButton.style.fontSize = '13px';
          gdriveBackupButton.style.lineHeight = '18px'; gdriveBackupButton.style.width = 'auto';
          gdriveBackupButton.addEventListener('mouseenter', () => { gdriveBackupButton.style.filter = 'brightness(1.08)'; gdriveBackupButton.style.boxShadow = '0 6px 14px rgba(26,115,232,0.45)'; });
          gdriveBackupButton.addEventListener('mouseleave', () => { gdriveBackupButton.style.filter = ''; gdriveBackupButton.style.boxShadow = '0 3px 8px rgba(26,115,232,0.35)'; });

          const gdriveRestoreButton = document.createElement('button');
          gdriveRestoreButton.textContent = t('gdriveRestore');
          gdriveRestoreButton.title = t('gdriveRestoreTitle');
          gdriveRestoreButton.addEventListener('click', function() {
            if (typeof importFromGoogleDrive === 'function') { importFromGoogleDrive(); }
            else { alert(t('gdriveImportUnavailable')); }
          });
          gdriveRestoreButton.style.backgroundImage = 'linear-gradient(180deg, #34a853, #137333)';
          gdriveRestoreButton.style.color = '#fff'; gdriveRestoreButton.style.border = 'none'; gdriveRestoreButton.style.borderRadius = '10px';
          gdriveRestoreButton.style.padding = '8px 12px'; gdriveRestoreButton.style.fontWeight = '600'; gdriveRestoreButton.style.cursor = 'pointer';
          gdriveRestoreButton.style.boxShadow = '0 3px 8px rgba(19, 115, 51, 0.35)';
          gdriveRestoreButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          gdriveRestoreButton.style.whiteSpace = 'nowrap'; gdriveRestoreButton.style.display = 'inline-flex'; gdriveRestoreButton.style.alignItems = 'center';
          gdriveRestoreButton.style.flex = '0 0 auto'; gdriveRestoreButton.style.gap = '4px'; gdriveRestoreButton.style.fontSize = '13px';
          gdriveRestoreButton.style.lineHeight = '18px'; gdriveRestoreButton.style.width = 'auto';
          gdriveRestoreButton.addEventListener('mouseenter', () => { gdriveRestoreButton.style.filter = 'brightness(1.08)'; gdriveRestoreButton.style.boxShadow = '0 6px 14px rgba(19,115,51,0.45)'; });
          gdriveRestoreButton.addEventListener('mouseleave', () => { gdriveRestoreButton.style.filter = ''; gdriveRestoreButton.style.boxShadow = '0 3px 8px rgba(19,115,51,0.35)'; });

          const gdriveButtonContainer = document.createElement('div');
          gdriveButtonContainer.style.display = 'flex'; gdriveButtonContainer.style.gap = '8px'; gdriveButtonContainer.style.marginTop = '8px';
          gdriveButtonContainer.style.justifyContent = 'center'; gdriveButtonContainer.style.alignItems = 'center'; gdriveButtonContainer.style.width = '100%';
          gdriveButtonContainer.appendChild(gdriveBackupButton);
          gdriveButtonContainer.appendChild(gdriveRestoreButton);
          tabPane.appendChild(gdriveButtonContainer);

          // Dropbox-Hilfepanel direkt im Skripte-Tab anzeigen
          try { injectDropboxHelpPanel(tabPane); } catch (_) {}
          // Google Drive Hilfepanel im Skripte-Tab anzeigen
          try { injectGdriveHelpPanel(tabPane); } catch (_) {}

          // Auto Save UI
          try {
            var autoSaveUI = createAutoSaveUI();
            tabPane.appendChild(autoSaveUI);
            loadAutoSaveSettings();
          } catch (_) {}

          const keyListContainer = document.createElement('div');
          keyListContainer.id = 'key-list-container';
          keyListContainer.style.marginTop = '10px';
          tabPane.appendChild(keyListContainer);

          if (typeof window !== 'undefined') window.uiMounted = true;
          ESM_DIAG.log('UI mounted via W.userscripts.registerSidebarTab');
          return;
        } catch (e) {
          ESM_DIAG.error('Failed to mount via W.userscripts, falling back', e);
        }
      }

      if (haveWazeWrapTab) {
        try {
          const html = `
            <div id="esm-tab">
              <p id="esm-title" style="font-weight:700;margin:0 0 6px 0;">${t('scriptTitle')}</p>
              <p style="margin:0 0 8px 0;">${t('importExportDesc')}</p>
              <div style="display:flex;gap:8px;margin-bottom:8px;justify-content:center;align-items:center;width:100%;">
                <button id="esm-export">${t('exportBackup')}</button>
                <button id="esm-import">${t('importBackup')}</button>
              </div>
              <div style="display:flex;gap:8px;margin-bottom:8px;justify-content:center;align-items:center;width:100%;">
                <button id="esm-cloud-backup">${t('cloudBackup')}</button>
                <button id="esm-cloud-restore">${t('cloudRestore')}</button>
              </div>
              <div style="display:flex;gap:8px;margin-bottom:8px;justify-content:center;align-items:center;width:100%;">
                <button id="esm-gdrive-backup">${t('gdriveBackup')}</button>
                <button id="esm-gdrive-restore">${t('gdriveRestore')}</button>
              </div>
              <div id="key-list-container" style="border:1px solid rgba(0,0,0,0.1);border-radius:8px;padding:8px;max-height:320px;overflow:auto;"></div>
            </div>`;
          new WazeWrap.Interface.Tab(t('scriptTitle'), html, () => {
const importBtn = document.getElementById('esm-import');
if (importBtn) { importBtn.textContent = t('importBackup'); importBtn.title = t('importBackupTitle'); }
const exportBtn = document.getElementById('esm-export');
if (exportBtn) { exportBtn.textContent = t('exportBackup'); exportBtn.title = t('exportBackupTitle'); }
const cloudBackupBtn = document.getElementById('esm-cloud-backup');
if (cloudBackupBtn) { cloudBackupBtn.textContent = t('cloudBackup'); cloudBackupBtn.title = t('cloudBackupTitle'); }
const cloudRestoreBtn = document.getElementById('esm-cloud-restore');
if (cloudRestoreBtn) { cloudRestoreBtn.textContent = t('cloudRestore'); cloudRestoreBtn.title = t('cloudRestoreTitle'); }
            if (cloudBackupBtn) cloudBackupBtn.textContent = t('cloudBackup');
            if (cloudRestoreBtn) cloudRestoreBtn.textContent = t('cloudRestore');

            if (importBtn) importBtn.addEventListener('click', function() { if (typeof window.importLocalStorage === 'function') { window.importLocalStorage(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {} alert(t('importFunctionUnavailable')); } });
            if (exportBtn) exportBtn.addEventListener('click', function() { if (typeof window.exportLocalStorage === 'function') { window.exportLocalStorage(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {} alert(t('exportFunctionUnavailable')); } });
            if (cloudBackupBtn) cloudBackupBtn.addEventListener('click', function() { if (typeof exportToDropbox === 'function') { exportToDropbox(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox export function not available'); } catch (_) {} alert(t('dropboxExportUnavailable')); } });
            if (cloudRestoreBtn) cloudRestoreBtn.addEventListener('click', function() { if (typeof importFromDropbox === 'function') { importFromDropbox(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox import function not available'); } catch (_) {} alert(t('dropboxImportUnavailable')); } });
            // Reihenfolge im Flex-Container: zuerst Lokal Speichern (Export), dann Wiederherstellen (Import)
            if (typeof exportBtn !== 'undefined' && exportBtn) exportBtn.style.order = '1';
            if (typeof importBtn !== 'undefined' && importBtn) importBtn.style.order = '2';

            // Apply compact styles for WazeWrap tab buttons
            if (importBtn) {
              importBtn.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
              importBtn.style.color = '#fff';
              importBtn.style.border = 'none';
              importBtn.style.borderRadius = '10px';
              importBtn.style.padding = '8px 12px';
              importBtn.style.fontWeight = '600';
              importBtn.style.cursor = 'pointer';
              importBtn.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
              importBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              importBtn.style.whiteSpace = 'nowrap';
              importBtn.style.display = 'inline-flex';
              importBtn.style.alignItems = 'center';
              importBtn.style.gap = '4px';
              importBtn.style.fontSize = '13px';
              importBtn.style.lineHeight = '18px';
            }
            if (exportBtn) {
              exportBtn.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
              exportBtn.style.color = '#fff';
              exportBtn.style.border = 'none';
              exportBtn.style.borderRadius = '10px';
              exportBtn.style.padding = '8px 12px';
              exportBtn.style.fontWeight = '600';
              exportBtn.style.cursor = 'pointer';
              exportBtn.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
              exportBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              exportBtn.style.whiteSpace = 'nowrap';
              exportBtn.style.display = 'inline-flex';
              exportBtn.style.alignItems = 'center';
              exportBtn.style.gap = '4px';
              exportBtn.style.fontSize = '13px';
              exportBtn.style.lineHeight = '18px';
            }
            if (cloudBackupBtn) {
              cloudBackupBtn.style.backgroundImage = 'linear-gradient(180deg, #ff9800, #f57c00)';
              cloudBackupBtn.style.color = '#fff';
              cloudBackupBtn.style.border = 'none';
              cloudBackupBtn.style.borderRadius = '10px';
              cloudBackupBtn.style.padding = '8px 12px';
              cloudBackupBtn.style.fontWeight = '600';
              cloudBackupBtn.style.cursor = 'pointer';
              cloudBackupBtn.style.boxShadow = '0 3px 8px rgba(245, 124, 0, 0.35)';
              cloudBackupBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              cloudBackupBtn.style.whiteSpace = 'nowrap';
              cloudBackupBtn.style.display = 'inline-flex';
              cloudBackupBtn.style.alignItems = 'center';
              cloudBackupBtn.style.gap = '4px';
              cloudBackupBtn.style.fontSize = '13px';
              cloudBackupBtn.style.lineHeight = '18px';
    cloudBackupBtn.title = t('cloudBackupTitle');
            }
            if (cloudRestoreBtn) {
              cloudRestoreBtn.style.backgroundImage = 'linear-gradient(180deg, #9c27b0, #7b1fa2)';
              cloudRestoreBtn.style.color = '#fff';
              cloudRestoreBtn.style.border = 'none';
              cloudRestoreBtn.style.borderRadius = '10px';
              cloudRestoreBtn.style.padding = '8px 12px';
              cloudRestoreBtn.style.fontWeight = '600';
              cloudRestoreBtn.style.cursor = 'pointer';
              cloudRestoreBtn.style.boxShadow = '0 3px 8px rgba(123, 31, 162, 0.35)';
              cloudRestoreBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              cloudRestoreBtn.style.whiteSpace = 'nowrap';
              cloudRestoreBtn.style.display = 'inline-flex';
              cloudRestoreBtn.style.alignItems = 'center';
              cloudRestoreBtn.style.gap = '4px';
              cloudRestoreBtn.style.fontSize = '13px';
              cloudRestoreBtn.style.lineHeight = '18px';
    cloudRestoreBtn.title = t('cloudRestoreTitle');
            }
            // Google Drive buttons in WazeWrap tab
            const gdriveBackupBtn = document.getElementById('esm-gdrive-backup');
            const gdriveRestoreBtn = document.getElementById('esm-gdrive-restore');
            if (gdriveBackupBtn) { gdriveBackupBtn.textContent = t('gdriveBackup'); gdriveBackupBtn.title = t('gdriveBackupTitle'); }
            if (gdriveRestoreBtn) { gdriveRestoreBtn.textContent = t('gdriveRestore'); gdriveRestoreBtn.title = t('gdriveRestoreTitle'); }
            if (gdriveBackupBtn) gdriveBackupBtn.addEventListener('click', function() { if (typeof exportToGoogleDrive === 'function') { exportToGoogleDrive(); } else { alert(t('gdriveExportUnavailable')); } });
            if (gdriveRestoreBtn) gdriveRestoreBtn.addEventListener('click', function() { if (typeof importFromGoogleDrive === 'function') { importFromGoogleDrive(); } else { alert(t('gdriveImportUnavailable')); } });
            if (gdriveBackupBtn) {
              gdriveBackupBtn.style.backgroundImage = 'linear-gradient(180deg, #4285f4, #1a73e8)';
              gdriveBackupBtn.style.color = '#fff'; gdriveBackupBtn.style.border = 'none'; gdriveBackupBtn.style.borderRadius = '10px';
              gdriveBackupBtn.style.padding = '8px 12px'; gdriveBackupBtn.style.fontWeight = '600'; gdriveBackupBtn.style.cursor = 'pointer';
              gdriveBackupBtn.style.boxShadow = '0 3px 8px rgba(26, 115, 232, 0.35)';
              gdriveBackupBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              gdriveBackupBtn.style.whiteSpace = 'nowrap'; gdriveBackupBtn.style.display = 'inline-flex'; gdriveBackupBtn.style.alignItems = 'center';
              gdriveBackupBtn.style.gap = '4px'; gdriveBackupBtn.style.fontSize = '13px'; gdriveBackupBtn.style.lineHeight = '18px';
            }
            if (gdriveRestoreBtn) {
              gdriveRestoreBtn.style.backgroundImage = 'linear-gradient(180deg, #34a853, #137333)';
              gdriveRestoreBtn.style.color = '#fff'; gdriveRestoreBtn.style.border = 'none'; gdriveRestoreBtn.style.borderRadius = '10px';
              gdriveRestoreBtn.style.padding = '8px 12px'; gdriveRestoreBtn.style.fontWeight = '600'; gdriveRestoreBtn.style.cursor = 'pointer';
              gdriveRestoreBtn.style.boxShadow = '0 3px 8px rgba(19, 115, 51, 0.35)';
              gdriveRestoreBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              gdriveRestoreBtn.style.whiteSpace = 'nowrap'; gdriveRestoreBtn.style.display = 'inline-flex'; gdriveRestoreBtn.style.alignItems = 'center';
              gdriveRestoreBtn.style.gap = '4px'; gdriveRestoreBtn.style.fontSize = '13px'; gdriveRestoreBtn.style.lineHeight = '18px';
            }
            if (typeof window !== 'undefined') window.uiMounted = true;
            ESM_DIAG.log('UI mounted via WazeWrap.Interface.Tab');
            // Dropbox-Hilfepanel im WazeWrap-Tab platzieren
            try { const tabRoot = document.getElementById('esm-tab') || document.body; injectDropboxHelpPanel(tabRoot); } catch (_) {}
            // Google Drive Hilfepanel im WazeWrap-Tab platzieren
            try { const tabRoot2 = document.getElementById('esm-tab') || document.body; injectGdriveHelpPanel(tabRoot2); } catch (_) {}
            // Auto Save UI im WazeWrap-Tab
            try {
              const tabRoot3 = document.getElementById('esm-tab') || document.body;
              const autoSaveUI = createAutoSaveUI();
              tabRoot3.appendChild(autoSaveUI);
              loadAutoSaveSettings();
            } catch (_) {}
          });
          return;
        } catch (e) {
          ESM_DIAG.error('Failed to mount via WazeWrap.Interface.Tab, falling back', e);
        }
      }

      // Final fallback: floating panel
      createFallbackPanel();
    }

     // Initialize the script
  function initialize() {
    ESM_DIAG.log('initialize() called. document.readyState=', document.readyState);

    // Check if we're in a Waze environment
    const isWazeEnvironment = window.location.hostname.includes('waze.com');

    if (isWazeEnvironment && typeof W !== 'undefined' && W && W.userscripts && W.userscripts.state && W.userscripts.state.isReady) {
      ESM_DIAG.log('W.userscripts.state.isReady is true. Initializing UI.');
      addScriptTab();
      showScriptUpdate();
      reapplyAfterReload();
    } else if (isWazeEnvironment) {
      ESM_DIAG.log('Waiting for wme-ready event...');
      document.addEventListener('wme-ready', function () {
        ESM_DIAG.log('wme-ready event received. Initializing UI.');
        addScriptTab();
        showScriptUpdate();
        reapplyAfterReload();
      }, { once: true });
    } else {
      // Non-Waze environment - use fallback panel directly
      ESM_DIAG.log('Non-Waze environment detected. Using fallback panel.');
      createFallbackPanel();
      reapplyAfterReload();
    }
  }

  // Call the initialize function
  initialize();

  // Show script update notification
  function showScriptUpdate() {
    if (typeof WazeWrap !== 'undefined' && WazeWrap && WazeWrap.Interface && typeof WazeWrap.Interface.ShowScriptUpdate === 'function') {
      WazeWrap.Interface.ShowScriptUpdate(
        t('scriptTitle'),
        (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : scriptVersion,
        updateMessage,
        'https://greasyfork.org/en/scripts/466806-easy-storage-manager',
        'https://www.waze.com/forum/viewtopic.php?t=382966'
      );
    } else {
      ESM_DIAG.warn('ShowScriptUpdate skipped: WazeWrap.Interface.ShowScriptUpdate not available yet.');
    }
  }

  // Reapply after reload if a stash exists (inside IIFE)
  function reapplyAfterReload() {
    let stashStr = null;
    try {
      stashStr = sessionStorage.getItem(REAPPLY_STASH_KEY);
    } catch (e) {
      stashStr = null;
    }
    if (!stashStr) { ESM_DIAG.log('No reapply stash found; skipping.'); return; }
    sessionStorage.removeItem(REAPPLY_STASH_KEY);
    ESM_DIAG.log('Reapply stash found. Parsing...');
    try {
      const stash = JSON.parse(stashStr);
      ESM_DIAG.log('Parsed stash items count:', Array.isArray(stash.items) ? stash.items.length : 0);
      if (!(stash && stash.origin === location.origin && Array.isArray(stash.items) && stash.items.length)) { ESM_DIAG.warn('Stash invalid or empty; skipping.'); return; }

      applyPairs(stash.items).then(({ counts, failures }) => {
        ESM_DIAG.log('Initial applyPairs after reload complete.', counts, failures.slice(0, 3));
        const summary = `Restored after reload executed.\n- localStorage: ${counts.local}\n- sessionStorage: ${counts.session}\n- Cookies: ${counts.cookie}\n- IndexedDB: ${counts.idb}` + (failures.length ? `\n\nError (first ${Math.min(5, failures.length)}):\n${failures.slice(0,5).join('\n')}${failures.length > 5 ? `\n... (${failures.length - 5} more)` : ''}` : '');
        try { alert(summary); } catch (e) {}
      }).catch((ex) => { ESM_DIAG.error('applyPairs after reload failed:', ex); });

      const desiredLocal = new Map();
      for (const [fullKey, value] of stash.items) {
        const idx = fullKey.indexOf(':');
        const type = idx < 0 ? 'localStorage' : fullKey.slice(0, idx);
        const name = idx < 0 ? fullKey : fullKey.slice(idx + 1);
        if (type === 'localStorage') {
          desiredLocal.set(name, value);
        }
      }
      ESM_DIAG.log('Desired localStorage keys:', Array.from(desiredLocal.keys()));

      const scheduleRepairs = (delaysMs) => {
        ESM_DIAG.log('Scheduling repairs with delays:', delaysMs);
        delaysMs.forEach(ms => {
          setTimeout(() => {
            const repairs = [];
            desiredLocal.forEach((desired, name) => {
              try {
                const current = NativeStorage.getItem.call(window.localStorage, name);
                if (current !== desired) {
                  ESM_DIAG.log('Repair needed for key:', name, 'current=', current, 'desired=', desired);
                  repairs.push([`localStorage:${name}`, desired]);
                } else {
                  ESM_DIAG.log('Key already correct:', name);
                }
              } catch (e) {
                ESM_DIAG.warn('Error reading key during repair check:', name, e);
              }
            });
            if (repairs.length) {
              ESM_DIAG.log('Applying repairs count:', repairs.length);
              applyPairs(repairs).catch((ex) => { ESM_DIAG.error('applyPairs repairs failed:', ex); });
            }
          }, ms);
        });
      };

      const originalSetItem = localStorage.setItem.bind(localStorage);
      const originalRemoveItem = localStorage.removeItem.bind(localStorage);
      const originalClear = localStorage.clear.bind(localStorage);
      const protectMs = 120000; // 120s protection
      const protectUntil = Date.now() + protectMs;
      const protectedKeys = new Set(Array.from(desiredLocal.keys()));

      try {
        localStorage.setItem = function (key, value) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Protected setItem intercepted for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (e) { return; }
          }
          return originalSetItem(key, value);
        };

        localStorage.getItem = function (key) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            const desired = desiredLocal.get(key);
            ESM_DIAG.log('Protected getItem intercepted for key:', key);
            return desired != null ? String(desired) : null;
          }
          return NativeStorage.getItem.call(window.localStorage, key);
        };

        localStorage.removeItem = function (key) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Protected removeItem blocked for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (_) { return; }
          }
          return originalRemoveItem(key);
        };

        localStorage.clear = function () {
          if (Date.now() < protectUntil && protectedKeys.size) {
            ESM_DIAG.log('Protected clear intercepted; preserving keys:', Array.from(protectedKeys));
            try {
              const len = window.localStorage.length;
              const toRemove = [];
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null && !protectedKeys.has(k)) toRemove.push(k);
              }
              toRemove.forEach(k => { try { originalRemoveItem(k); } catch (_) {} });
              return;
            } catch (_) { /* fallback */ }
          }
          return originalClear();
        };

        ESM_DIAG.log('Protection overrides applied for keys:', Array.from(protectedKeys), 'until', new Date(protectUntil).toISOString());
      } catch (ex) {
        ESM_DIAG.error('Error applying protection overrides:', ex);
      }

      scheduleRepairs([500, 2000, 5000, 10000, 20000, 30000, 45000, 60000, 90000, 120000]);
    } catch (ex) {
      ESM_DIAG.error('Error parsing stash or scheduling repairs:', ex);
    }
  }

  // Export key functions to window for fallback usage
  if (typeof window !== 'undefined') {
    window.exportLocalStorage = exportLocalStorage;
    window.importLocalStorage = importLocalStorage;
    window.exportToGoogleDrive = exportToGoogleDrive;
    window.importFromGoogleDrive = importFromGoogleDrive;
  }

  async function writeIndexedDBRecord(record) {
    return new Promise((resolve, reject) => {
      if (typeof record.db !== 'string' || record.db.length === 0 || record.db.length > 255) {
        reject(new Error(`Invalid IndexedDB name: ${record.db}`)); return;
      }
      if (typeof record.store !== 'string' || record.store.length === 0 || record.store.length > 255) {
        reject(new Error(`Invalid store name: ${record.store}`)); return;
      }
      const openReq = indexedDB.open(record.db);
      openReq.onerror = () => reject(openReq.error);
      openReq.onupgradeneeded = () => { /* no-op */ };
      openReq.onsuccess = () => {
        const db = openReq.result;
        const proceedWrite = () => {
          try {
            const tx = db.transaction([record.store], 'readwrite');
            const st = tx.objectStore(record.store);
            let req;
            if (st.keyPath) {
              req = st.put(record.value);
            } else {
              req = st.put(record.value, record.key);
            }
            req.onerror = () => reject(req.error);
            tx.oncomplete = () => { db.close(); resolve(true); };
            tx.onerror = () => reject(tx.error);
          } catch (err) {
            reject(err);
          }
        };
        if (!db.objectStoreNames.contains(record.store)) {
          db.close();
          const bump = indexedDB.open(record.db, (db.version || 1) + 1);
          bump.onerror = () => reject(bump.error);
          bump.onupgradeneeded = (evt) => {
            const db2 = evt.target.result;
            if (!db2.objectStoreNames.contains(record.store)) {
              const opts = {};
              if (record.keyPath) opts.keyPath = record.keyPath;
              if (record.autoIncrement) opts.autoIncrement = true;
              const newStore = db2.createObjectStore(record.store, opts);
              if (Array.isArray(record.indexes)) {
                record.indexes.forEach(ix => {
                  try {
                    newStore.createIndex(ix.name, ix.keyPath, { unique: !!ix.unique, multiEntry: !!ix.multiEntry });
                  } catch (e) { /* ignore invalid index definitions */ }
                });
              }
            }
          };
          bump.onsuccess = () => {
            const db2 = bump.result;
            try {
              const tx = db2.transaction([record.store], 'readwrite');
              const st = tx.objectStore(record.store);
              let req;
              if (st.keyPath) {
                req = st.put(record.value);
              } else {
                req = st.put(record.value, record.key);
              }
              req.onerror = () => reject(req.error);
              tx.oncomplete = () => { db2.close(); resolve(true); };
              tx.onerror = () => reject(tx.error);
            } catch (err) {
              reject(err);
            }
          };
        } else {
          proceedWrite();
        }
      };
    });
  }

  // Properly close IIFE
  })();

  // Duplicate applyPairs removed; using the in-IIFE implementation.
  let uiMounted = false;
  function ensureDOMReady(cb) {
    if (document.readyState === 'interactive' || document.readyState === 'complete') {
      try { cb(); } catch (e) { if (window.ESM_DIAG) window.ESM_DIAG.error('ensureDOMReady callback error', e); }
    } else {
      document.addEventListener('DOMContentLoaded', () => {
        try { cb(); } catch (e) { if (window.ESM_DIAG) window.ESM_DIAG.error('ensureDOMReady DOMContentLoaded error', e); }
      }, { once: true });
    }
  }

  function createFallbackPanel() {
    ensureDOMReady(() => {
      if (window.uiMounted) return;
      const panel = document.createElement('div');
      panel.id = 'esm-fallback-panel';
      panel.style.position = 'fixed';
      panel.style.top = '72px';
      panel.style.right = '12px';
      panel.style.zIndex = '999999';
      panel.style.background = 'rgba(30, 41, 59, 0.93)';
      panel.style.backdropFilter = 'blur(4px)';
      panel.style.border = '1px solid rgba(255,255,255,0.12)';
      panel.style.borderRadius = '12px';
      panel.style.padding = '12px 14px';
      panel.style.boxShadow = '0 6px 18px rgba(0,0,0,0.35)';
      panel.style.color = '#eaeef5';
      panel.style.maxWidth = '420px';
      panel.style.fontFamily = 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif';

      const title = document.createElement('div');
    title.textContent = t('scriptTitle');
      title.style.fontWeight = '700';
      title.style.marginBottom = '6px';
      title.style.letterSpacing = '0.2px';
      panel.appendChild(title);

      const desc = document.createElement('div');
    desc.textContent = t('fallbackDesc');
      desc.style.fontSize = '12px';
      desc.style.opacity = '0.9';
      desc.style.marginBottom = '8px';
      panel.appendChild(desc);

      const btnRow = document.createElement('div');
      btnRow.className = 'btnRow';
      btnRow.style.display = 'grid';
      btnRow.style.gridTemplateColumns = '1fr 1fr 1fr';
      btnRow.style.gap = '8px';
      btnRow.style.marginBottom = '10px';
      btnRow.style.alignItems = 'stretch';
      btnRow.style.width = '100%';
      btnRow.style.boxSizing = 'border-box';
      const importBtn = document.createElement('button');
      importBtn.id = 'esm-import-btn';
    importBtn.textContent = t('importBackup');
    importBtn.title = t('importBackupTitle');
      importBtn.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
      importBtn.style.color = '#fff';
      importBtn.style.border = 'none';
      importBtn.style.borderRadius = '10px';
      importBtn.style.padding = '8px 12px';
      importBtn.style.fontWeight = '600';
      importBtn.style.cursor = 'pointer';
      importBtn.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
      importBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      importBtn.style.whiteSpace = 'nowrap';
      importBtn.style.display = 'inline-flex';
      importBtn.style.alignItems = 'center';
      importBtn.style.gap = '4px';
      importBtn.style.fontSize = '13px';
      importBtn.style.lineHeight = '18px';
      importBtn.style.width = '100%';
      importBtn.style.flex = '1 1 0';
      importBtn.style.minWidth = '0';
      importBtn.style.margin = '0';
      importBtn.addEventListener('mouseenter', () => { importBtn.style.filter = 'brightness(1.08)'; importBtn.style.boxShadow = '0 6px 14px rgba(25,118,210,0.45)'; });
      importBtn.addEventListener('mouseleave', () => { importBtn.style.filter = ''; importBtn.style.boxShadow = '0 3px 8px rgba(25,118,210,0.35)'; });
      importBtn.addEventListener('click', function() {
        if (typeof window.importLocalStorage === 'function') {
          window.importLocalStorage();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {}
          alert(t('importFunctionUnavailable'));
        }
      });

      const exportBtn = document.createElement('button');
      exportBtn.id = 'esm-export-btn';
    exportBtn.textContent = t('exportBackup');
    exportBtn.title = t('exportBackupTitle');
      exportBtn.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
      exportBtn.style.color = '#fff';
      exportBtn.style.border = 'none';
      exportBtn.style.borderRadius = '10px';
      exportBtn.style.padding = '8px 12px';
      exportBtn.style.fontWeight = '600';
      exportBtn.style.cursor = 'pointer';
      exportBtn.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
      exportBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      exportBtn.style.whiteSpace = 'nowrap';
      exportBtn.style.display = 'inline-flex';
      exportBtn.style.alignItems = 'center';
      exportBtn.style.gap = '4px';
      exportBtn.style.fontSize = '13px';
      exportBtn.style.lineHeight = '18px';
      exportBtn.style.width = '100%';
      exportBtn.style.flex = '1 1 0';
      exportBtn.style.minWidth = '0';
      exportBtn.style.margin = '0';
      exportBtn.addEventListener('mouseenter', () => { exportBtn.style.filter = 'brightness(1.08)'; exportBtn.style.boxShadow = '0 6px 14px rgba(46,125,50,0.45)'; });
      exportBtn.addEventListener('mouseleave', () => { exportBtn.style.filter = ''; exportBtn.style.boxShadow = '0 3px 8px rgba(46,125,50,0.35)'; });
      exportBtn.addEventListener('click', function() {
        if (typeof window.exportLocalStorage === 'function') {
          window.exportLocalStorage();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {}
          alert(t('exportFunctionUnavailable'));
        }
      });
      // Reihenfolge im Grid-Container: alle Buttons gleichberechtigt
      exportBtn.style.order = '1';
      importBtn.style.order = '2';
      // Dropbox Backup Button
      const driveBackupBtn = document.createElement('button');
      driveBackupBtn.id = 'esm-drive-backup-btn';
    driveBackupBtn.textContent = t('cloudBackup');
    driveBackupBtn.title = t('cloudBackupTitle');
      driveBackupBtn.style.backgroundImage = 'linear-gradient(180deg, #4285f4, #1a73e8)';
      driveBackupBtn.style.color = '#fff';
      driveBackupBtn.style.border = 'none';
      driveBackupBtn.style.borderRadius = '10px';
      driveBackupBtn.style.padding = '8px 12px';
      driveBackupBtn.style.fontWeight = '600';
      driveBackupBtn.style.cursor = 'pointer';
      driveBackupBtn.style.boxShadow = '0 3px 8px rgba(26, 115, 232, 0.35)';
      driveBackupBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      driveBackupBtn.style.whiteSpace = 'nowrap';
      driveBackupBtn.style.display = 'inline-flex';
      driveBackupBtn.style.alignItems = 'center';
      driveBackupBtn.style.gap = '4px';
      driveBackupBtn.style.fontSize = '13px';
      driveBackupBtn.style.lineHeight = '18px';
      driveBackupBtn.style.width = '100%';
      driveBackupBtn.style.flex = '1 1 0';
      driveBackupBtn.style.minWidth = '0';
      driveBackupBtn.style.margin = '0';
      driveBackupBtn.addEventListener('mouseenter', () => { driveBackupBtn.style.filter = 'brightness(1.08)'; driveBackupBtn.style.boxShadow = '0 6px 14px rgba(26,115,232,0.45)'; });
      driveBackupBtn.addEventListener('mouseleave', () => { driveBackupBtn.style.filter = ''; driveBackupBtn.style.boxShadow = '0 3px 8px rgba(26,115,232,0.35)'; });
      driveBackupBtn.addEventListener('click', function() {
        if (typeof window.exportToDropbox === 'function') {
          window.exportToDropbox();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox backup function not available'); } catch (_) {}
          alert(t('dropboxExportUnavailable'));
        }
      });

      // Dropbox Restore Button
      const driveRestoreBtn = document.createElement('button');
      driveRestoreBtn.id = 'esm-drive-restore-btn';
    driveRestoreBtn.textContent = t('cloudRestore');
    driveRestoreBtn.title = t('cloudRestoreTitle');
      driveRestoreBtn.style.backgroundImage = 'linear-gradient(180deg, #34a853, #137333)';
      driveRestoreBtn.style.color = '#fff';
      driveRestoreBtn.style.border = 'none';
      driveRestoreBtn.style.borderRadius = '10px';
      driveRestoreBtn.style.padding = '8px 12px';
      driveRestoreBtn.style.fontWeight = '600';
      driveRestoreBtn.style.cursor = 'pointer';
      driveRestoreBtn.style.boxShadow = '0 3px 8px rgba(19, 115, 51, 0.35)';
      driveRestoreBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      driveRestoreBtn.style.whiteSpace = 'nowrap';
      driveRestoreBtn.style.display = 'inline-flex';
      driveRestoreBtn.style.alignItems = 'center';
      driveRestoreBtn.style.gap = '4px';
      driveRestoreBtn.style.fontSize = '13px';
      driveRestoreBtn.style.lineHeight = '18px';
      driveRestoreBtn.style.width = '100%';
      driveRestoreBtn.style.flex = '1 1 0';
      driveRestoreBtn.style.minWidth = '0';
      driveRestoreBtn.style.margin = '0';
      driveRestoreBtn.addEventListener('mouseenter', () => { driveRestoreBtn.style.filter = 'brightness(1.08)'; driveRestoreBtn.style.boxShadow = '0 6px 14px rgba(19,115,51,0.45)'; });
      driveRestoreBtn.addEventListener('mouseleave', () => { driveRestoreBtn.style.filter = ''; driveRestoreBtn.style.boxShadow = '0 3px 8px rgba(19,115,51,0.35)'; });
      driveRestoreBtn.addEventListener('click', function() {
        if (typeof window.importFromDropbox === 'function') {
          window.importFromDropbox();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox restore function not available'); } catch (_) {}
          alert(t('dropboxImportUnavailable'));
        }
      });

      btnRow.appendChild(exportBtn);
      btnRow.appendChild(importBtn);
      btnRow.appendChild(driveBackupBtn);
      btnRow.appendChild(driveRestoreBtn);

      // Google Drive Backup Button (Fallback)
      const gdriveBackupFb = document.createElement('button');
      gdriveBackupFb.id = 'esm-gdrive-backup-btn';
      gdriveBackupFb.textContent = t('gdriveBackup');
      gdriveBackupFb.title = t('gdriveBackupTitle');
      gdriveBackupFb.style.backgroundImage = 'linear-gradient(180deg, #4285f4, #1a73e8)';
      gdriveBackupFb.style.color = '#fff'; gdriveBackupFb.style.border = 'none'; gdriveBackupFb.style.borderRadius = '10px';
      gdriveBackupFb.style.padding = '8px 12px'; gdriveBackupFb.style.fontWeight = '600'; gdriveBackupFb.style.cursor = 'pointer';
      gdriveBackupFb.style.boxShadow = '0 3px 8px rgba(26, 115, 232, 0.35)';
      gdriveBackupFb.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      gdriveBackupFb.style.whiteSpace = 'nowrap'; gdriveBackupFb.style.display = 'inline-flex'; gdriveBackupFb.style.alignItems = 'center';
      gdriveBackupFb.style.gap = '4px'; gdriveBackupFb.style.fontSize = '13px'; gdriveBackupFb.style.lineHeight = '18px';
      gdriveBackupFb.style.width = '100%'; gdriveBackupFb.style.flex = '1 1 0'; gdriveBackupFb.style.minWidth = '0'; gdriveBackupFb.style.margin = '0';
      gdriveBackupFb.addEventListener('mouseenter', () => { gdriveBackupFb.style.filter = 'brightness(1.08)'; gdriveBackupFb.style.boxShadow = '0 6px 14px rgba(26,115,232,0.45)'; });
      gdriveBackupFb.addEventListener('mouseleave', () => { gdriveBackupFb.style.filter = ''; gdriveBackupFb.style.boxShadow = '0 3px 8px rgba(26,115,232,0.35)'; });
      gdriveBackupFb.addEventListener('click', function() {
        if (typeof window.exportToGoogleDrive === 'function') { window.exportToGoogleDrive(); }
        else { alert(t('gdriveExportUnavailable')); }
      });

      // Google Drive Restore Button (Fallback)
      const gdriveRestoreFb = document.createElement('button');
      gdriveRestoreFb.id = 'esm-gdrive-restore-btn';
      gdriveRestoreFb.textContent = t('gdriveRestore');
      gdriveRestoreFb.title = t('gdriveRestoreTitle');
      gdriveRestoreFb.style.backgroundImage = 'linear-gradient(180deg, #34a853, #137333)';
      gdriveRestoreFb.style.color = '#fff'; gdriveRestoreFb.style.border = 'none'; gdriveRestoreFb.style.borderRadius = '10px';
      gdriveRestoreFb.style.padding = '8px 12px'; gdriveRestoreFb.style.fontWeight = '600'; gdriveRestoreFb.style.cursor = 'pointer';
      gdriveRestoreFb.style.boxShadow = '0 3px 8px rgba(19, 115, 51, 0.35)';
      gdriveRestoreFb.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      gdriveRestoreFb.style.whiteSpace = 'nowrap'; gdriveRestoreFb.style.display = 'inline-flex'; gdriveRestoreFb.style.alignItems = 'center';
      gdriveRestoreFb.style.gap = '4px'; gdriveRestoreFb.style.fontSize = '13px'; gdriveRestoreFb.style.lineHeight = '18px';
      gdriveRestoreFb.style.width = '100%'; gdriveRestoreFb.style.flex = '1 1 0'; gdriveRestoreFb.style.minWidth = '0'; gdriveRestoreFb.style.margin = '0';
      gdriveRestoreFb.addEventListener('mouseenter', () => { gdriveRestoreFb.style.filter = 'brightness(1.08)'; gdriveRestoreFb.style.boxShadow = '0 6px 14px rgba(19,115,51,0.45)'; });
      gdriveRestoreFb.addEventListener('mouseleave', () => { gdriveRestoreFb.style.filter = ''; gdriveRestoreFb.style.boxShadow = '0 3px 8px rgba(19,115,51,0.35)'; });
      gdriveRestoreFb.addEventListener('click', function() {
        if (typeof window.importFromGoogleDrive === 'function') { window.importFromGoogleDrive(); }
        else { alert(t('gdriveImportUnavailable')); }
      });

      btnRow.appendChild(gdriveBackupFb);
      btnRow.appendChild(gdriveRestoreFb);
      panel.appendChild(btnRow);

      // Auto Save UI im Fallback-Panel
      try {
        const autoSaveUI = createAutoSaveUI();
        autoSaveUI.style.background = 'rgba(255,255,255,0.08)';
        autoSaveUI.style.border = '1px solid rgba(255,255,255,0.15)';
        autoSaveUI.style.color = '#eaeef5';
        panel.appendChild(autoSaveUI);
        loadAutoSaveSettings();
      } catch (_) {}

      const container = document.createElement('div');
      container.id = 'key-list-container';
      container.style.background = 'rgba(255,255,255,0.05)';
      container.style.border = '1px solid rgba(255,255,255,0.10)';
      container.style.borderRadius = '10px';
      container.style.padding = '10px';
      container.style.maxHeight = '320px';
      container.style.overflow = 'auto';
      panel.appendChild(container);

      document.body.appendChild(panel);
      window.uiMounted = true;
      if (typeof window !== 'undefined' && window.ESM_DIAG) window.ESM_DIAG.log('UI mounted via floating fallback panel');
    });
  }

  // Duplicate global addScriptTab removed; using the IIFE-scoped implementation exclusively.
  // UI mounting is managed inside the IIFE via initialize(); removing duplicate global function to avoid scope issues.
  if (typeof window !== 'undefined') {
    if (typeof exportLocalStorage === 'function') {
      window.exportLocalStorage = exportLocalStorage;
    }
    if (typeof importLocalStorage === 'function') {
      window.importLocalStorage = importLocalStorage;
    }
    if (typeof exportToDropbox === 'function') {
      window.exportToDropbox = exportToDropbox;
    }
    if (typeof importFromDropbox === 'function') {
      window.importFromDropbox = importFromDropbox;
    }
    if (typeof exportToGoogleDrive === 'function') {
      window.exportToGoogleDrive = exportToGoogleDrive;
    }
    if (typeof importFromGoogleDrive === 'function') {
      window.importFromGoogleDrive = importFromGoogleDrive;
    }
  }