// app.jsx — Main App with state management and routing

const { useState, useEffect, useRef } = React;

// Environment-Detection: localhost / *.dev.haikara.it / Live. Steuert Hub-URL,
// Cookie-Domain (für Cross-Subdomain-SSO) und Logout-Redirect. Live = .haikara.it,
// Dev = .dev.haikara.it (separates Cookie-Realm gegen Live-Token-Pollution).
const HK_ENV = (() => {
  const h = window.location.hostname;
  if (h === 'localhost' || h === '127.0.0.1') return 'local';
  if (/\.dev\.haikara\.it$/i.test(h)) return 'dev';
  return 'prod';
})();
const HK_COOKIE_DOMAIN = HK_ENV === 'dev' ? '.dev.haikara.it' : '.haikara.it';
const HK_HUB_URL = HK_ENV === 'local'
  ? window.location.origin + '/hub/'
  : HK_ENV === 'dev'
    ? 'https://hub.dev.haikara.it/'
    : 'https://haikara.it/';
// Window-Global, damit sidebar.jsx + andere Module ohne extra Prop drauf zugreifen können.
window.HK_HUB_URL = HK_HUB_URL;
window.HK_ENV = HK_ENV;

const App = () => {
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "accentColor": "#9b5cf6",
    "sidebarWidth": 260,
    "fontSize": 13,
    "fontMono": "JetBrains Mono",
    "compactMode": false,
    "calendarView": "list"
  }/*EDITMODE-END*/;

  // Token kann aus localStorage ODER aus dem hk_token-Cookie kommen. Letzteres
  // ist der SSO-Pfad: User loggt sich auf haikara.it ein, Cookie wird auf
  // .haikara.it gesetzt, springt zurück auf Heronry — Heronry hat noch kein
  // localStorage, liest aber den Cookie und übernimmt ihn.
  const readInitialToken = () => {
    const ls = localStorage.getItem('tb_token');
    if (ls) return ls;
    const m = document.cookie.match(/(?:^|;\s*)hk_token=([^;]+)/);
    return m ? decodeURIComponent(m[1]) : null;
  };
  const [token, setToken] = useState(readInitialToken);
  const [user, setUser] = useState(null);
  const [bootstrapping, setBootstrapping] = useState(!!readInitialToken());
  const [darkMode, setDarkMode] = useState(() => {
    const saved = localStorage.getItem('tb_dark');
    return saved !== null ? saved === 'true' : true;
  });
  const [route, setRoute] = useState({ page: 'dashboard' });
  const [notes, setNotes] = useState([]);
  const [deadlines, setDeadlines] = useState([]);
  const [notesLoaded, setNotesLoaded] = useState(false);
  const [deadlinesLoaded, setDeadlinesLoaded] = useState(false);
  const [flashcards, setFlashcards] = useState([]);
  const [flashcardsLoaded, setFlashcardsLoaded] = useState(false);
  const [reminderSubs, setReminderSubs] = useState([]);
  const [releases, setReleases] = useState([]);
  const [roles, setRoles] = useState([]);
  const [rolesLoaded, setRolesLoaded] = useState(false);
  const [permissionCatalog, setPermissionCatalog] = useState([]);
  const [topicAreas, setTopicAreas] = useState([]);
  const [sections, setSections] = useState([]);
  const [structureLoaded, setStructureLoaded] = useState(false);
  const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);
  const [tweaksLoaded, setTweaksLoaded] = useState(false);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [whatsNewOpen, setWhatsNewOpen] = useState(false);
  const [onboardingOpen, setOnboardingOpen] = useState(false);
  const [publicSettings, setPublicSettings] = useState({ registrationEnabled: true });
  const [verifyResend, setVerifyResend] = useState({ state: 'idle', msg: '' });
  const skipNextPut = useRef(true);

  const handleResendVerify = async () => {
    setVerifyResend({ state: 'sending', msg: '' });
    const { ok, data } = await apiFetch('/api/auth/resend-verify', { method: 'POST' });
    if (ok) setVerifyResend({ state: 'sent', msg: data?.alreadyVerified ? 'Email ist bereits bestätigt.' : 'Mail erneut versendet.' });
    else setVerifyResend({ state: 'error', msg: data?.error || 'Fehler beim Senden.' });
    setTimeout(() => setVerifyResend({ state: 'idle', msg: '' }), 4000);
  };

  // Public settings (für LoginView + Maintenance-State): auf Mount holen, danach
  // alle 60s neu pollen — damit ein Admin-Toggle (Wartung an/Schedule ändern)
  // auch ohne Reload und ohne 503-Trigger sichtbar wird.
  useEffect(() => {
    let cancelled = false;
    const fetchPublic = () => {
      apiFetch('/api/settings/public').then(({ ok, data }) => {
        if (cancelled || !ok) return;
        setPublicSettings(s => ({ ...s, ...(data?.settings || {}) }));
      });
    };
    fetchPublic();
    const iv = setInterval(fetchPublic, 60_000);
    return () => { cancelled = true; clearInterval(iv); };
  }, []);

  // Maintenance-Handler: api.js ruft das auf, wenn ein Backend-Endpoint mit 503
  // + maintenance-Flag antwortet. Wir spiegeln den Server-State sofort in die
  // publicSettings, damit das nächste Render die MaintenanceView (oder den
  // Bypass-Banner) zeigt — ohne extra Round-Trip.
  useEffect(() => {
    window.onMaintenanceActivated = (info) => {
      setPublicSettings(s => ({
        ...s,
        maintenanceApps: Array.isArray(info?.apps) ? info.apps : (s.maintenanceApps || []),
        maintenanceMessage: info?.message ?? s.maintenanceMessage,
        maintenanceScheduledAt: info?.scheduledAt ?? s.maintenanceScheduledAt,
        maintenanceScheduledDuration: info?.scheduledDuration ?? s.maintenanceScheduledDuration,
      }));
    };
    return () => { delete window.onMaintenanceActivated; };
  }, []);

  const refreshPublicSettings = async () => {
    const { ok, data } = await apiFetch('/api/settings/public');
    if (ok) setPublicSettings(s => ({ ...s, ...(data?.settings || {}) }));
  };

  const refreshStructure = async () => {
    const [ta, sc] = await Promise.all([
      apiFetch('/api/topic-areas'),
      apiFetch('/api/sections'),
    ]);
    if (ta.ok) setTopicAreas(ta.data.topicAreas || []);
    if (sc.ok) setSections(sc.data.sections || []);
  };

  // Persist
  useEffect(() => {
    if (token) localStorage.setItem('tb_token', token);
    else localStorage.removeItem('tb_token');
  }, [token]);

  // SSO-Cookie aus localStorage spiegeln. Backend setzt den hk_token-Cookie nur
  // bei Login-Response; eine andere Subdomain kann ihn aber gelöscht haben (Hub-Logout
  // löscht den Cookie, aber nicht Heronrys localStorage). Damit Cross-Subdomain-Apps
  // konsistent bleiben, schreiben wir den Cookie auf jedem Bootstrap neu, solange
  // Heronry ein gültiges Token hat.
  useEffect(() => {
    if (!token) return;
    const hasCookie = /(?:^|;\s*)hk_token=/.test(document.cookie);
    if (!hasCookie) {
      const flags = HK_ENV === 'local'
        ? 'Path=/; SameSite=Lax'
        : `Domain=${HK_COOKIE_DOMAIN}; Path=/; Secure; SameSite=Lax`;
      document.cookie = `hk_token=${encodeURIComponent(token)}; ${flags}; Max-Age=${7 * 24 * 60 * 60}`;
    }
  }, [token]);
  // ?verify=… in URL einlösen wenn jemand eingeloggt drauf klickt (LoginView macht's für nicht-eingeloggte)
  useEffect(() => {
    if (!token) return;
    const params = new URLSearchParams(window.location.search);
    const verifyTok = params.get('verify');
    if (!verifyTok) return;
    window.history.replaceState({}, '', window.location.pathname);
    (async () => {
      const res = await apiFetch('/api/auth/verify-email', { method: 'POST', body: { token: verifyTok } });
      if (res.ok) {
        const me = await apiFetch('/api/auth/me');
        if (me.ok) setUser(me.data.user);
      }
    })();
  }, [token]);

  // ?return=… SSO-Redirect: nach Login auf eine *.haikara.it-Subdomain springen.
  // Nutzt eine strikte Whitelist gegen Open-Redirects. Feuert sobald User da ist
  // (egal ob via Login oder via persistierten Token aus localStorage).
  useEffect(() => {
    if (!user || bootstrapping) return;
    const params = new URLSearchParams(window.location.search);
    const returnUrl = params.get('return');
    if (!returnUrl) return;
    const ALLOWED = /^https:\/\/([a-z0-9-]+\.)?haikara\.it(\/.*)?$/i;
    const LOCAL = /^https?:\/\/localhost(:\d+)?(\/.*)?$/i;
    if (ALLOWED.test(returnUrl) || LOCAL.test(returnUrl)) {
      window.location.assign(returnUrl);
    } else {
      // Ungültige Return-URL → still verwerfen und im Heronry-Dashboard bleiben
      window.history.replaceState({}, '', window.location.pathname);
    }
  }, [user, bootstrapping]);

  // Wenn anonym und kein Auth-Flow im URL-Param → zum Hub-Login redirecten.
  // Hub übernimmt den Login, der ?return= bringt den User zurück auf diese URL.
  // verify-/reset-Email-Links bleiben auf Heronry (Backward-Compat — alte Mails
  // zeigen noch auf heronry.haikara.it).
  useEffect(() => {
    if (bootstrapping || token || user) return;
    const params = new URLSearchParams(window.location.search);
    if (params.has('verify') || params.has('reset')) return;
    const target = `${HK_HUB_URL}?login=1&return=${encodeURIComponent(window.location.href)}`;
    window.location.assign(target);
  }, [token, user, bootstrapping]);

  // Bootstrap: token vorhanden → User + Tweaks + Notes + Deadlines + Topic-Structure vom Server holen
  useEffect(() => {
    if (!token) {
      setUser(null); setBootstrapping(false);
      setTweaksLoaded(false); setNotesLoaded(false); setDeadlinesLoaded(false); setFlashcardsLoaded(false); setRolesLoaded(false); setStructureLoaded(false);
      return;
    }
    let cancelled = false;
    (async () => {
      const me = await apiFetch('/api/auth/me');
      if (cancelled) return;
      if (!me.ok) {
        setToken(null); setUser(null); setBootstrapping(false);
        setTweaksLoaded(false); setNotesLoaded(false); setDeadlinesLoaded(false); setFlashcardsLoaded(false); setRolesLoaded(false); setStructureLoaded(false);
        return;
      }
      setUser(me.data.user);
      const canWrite = can(me.data.user, 'notes:write');

      const [tw, nt, dl, fc, rs, pc, ta, sc, rm, rl] = await Promise.all([
        apiFetch('/api/tweaks'),
        apiFetch('/api/notes'),
        apiFetch('/api/deadlines'),
        apiFetch('/api/flashcards'),
        apiFetch('/api/roles'),
        apiFetch('/api/permissions'),
        apiFetch('/api/topic-areas'),
        apiFetch('/api/sections'),
        apiFetch('/api/reminders'),
        apiFetch('/api/releases'),
      ]);
      if (cancelled) return;
      if (rs.ok) setRoles(rs.data.roles || []);
      if (pc.ok) setPermissionCatalog(pc.data.permissions || []);
      if (ta.ok) setTopicAreas(ta.data.topicAreas || []);
      if (sc.ok) setSections(sc.data.sections || []);

      if (tw.ok) setTweaks({ ...TWEAK_DEFAULTS, ...(tw.data?.tweaks || {}) });
      else console.warn('Tweaks laden fehlgeschlagen, Defaults aktiv:', tw.status);

      let serverNotes = nt.ok ? (nt.data.notes || []) : [];
      let serverDeadlines = dl.ok ? (dl.data.deadlines || []) : [];
      const serverFlashcards = fc.ok ? (fc.data.flashcards || []) : [];

      // Einmalige Migration: localStorage → Backend, wenn Server leer ist und User schreiben darf
      if (canWrite && serverNotes.length === 0) {
        const localRaw = localStorage.getItem('tb_notes');
        if (localRaw) {
          try {
            const local = JSON.parse(localRaw);
            for (const note of local) {
              await apiFetch('/api/notes', { method: 'POST', body: {
                id: note.id, sectionId: note.sectionId || note.lfId || null, title: note.title || 'Unbenannt',
                tags: Array.isArray(note.tags) ? note.tags : [],
                content: note.content || '',
                connections: Array.isArray(note.connections) ? note.connections : [],
                isPrivate: !!note.isPrivate,
              }});
            }
            localStorage.removeItem('tb_notes');
            const fresh = await apiFetch('/api/notes');
            if (fresh.ok) serverNotes = fresh.data.notes || [];
          } catch (e) { console.warn('Notes-Migration fehlgeschlagen:', e); }
        }
      }
      if (canWrite && serverDeadlines.length === 0) {
        const localRaw = localStorage.getItem('tb_deadlines');
        if (localRaw) {
          try {
            const local = JSON.parse(localRaw);
            for (const d of local) {
              await apiFetch('/api/deadlines', { method: 'POST', body: {
                id: d.id, sectionId: d.sectionId || d.lfId || null, title: d.title || 'Unbenannt',
                date: d.date, type: d.type || 'task',
                description: d.description || '',
                isPrivate: !!d.isPrivate,
              }});
            }
            localStorage.removeItem('tb_deadlines');
            const fresh = await apiFetch('/api/deadlines');
            if (fresh.ok) serverDeadlines = fresh.data.deadlines || [];
          } catch (e) { console.warn('Deadlines-Migration fehlgeschlagen:', e); }
        }
      }

      if (cancelled) return;
      setNotes(serverNotes);
      setDeadlines(serverDeadlines);
      setFlashcards(serverFlashcards);
      setReminderSubs(rm.ok ? (rm.data.subscriptions || []) : []);
      setReleases(rl.ok ? (rl.data.releases || []) : []);
      skipNextPut.current = true;
      setTweaksLoaded(true);
      setNotesLoaded(true);
      setDeadlinesLoaded(true);
      setFlashcardsLoaded(true);
      setRolesLoaded(true);
      setStructureLoaded(true);
      setBootstrapping(false);
    })();
    return () => { cancelled = true; };
  }, [token]);
  useEffect(() => { localStorage.setItem('tb_dark', darkMode); }, [darkMode]);

  // Apply dark mode
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
    const link = document.getElementById('hljs-theme');
    if (link) link.href = darkMode
      ? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css'
      : 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
  }, [darkMode]);

  // Apply tweaks → CSS variables / data-attrs
  useEffect(() => {
    const root = document.documentElement;
    if (tweaks.accentColor)  root.style.setProperty('--accent', tweaks.accentColor);
    if (tweaks.sidebarWidth) root.style.setProperty('--sidebar-width', tweaks.sidebarWidth + 'px');
    if (tweaks.fontSize)     root.style.setProperty('--font-size-base', tweaks.fontSize + 'px');
    if (tweaks.fontMono)     root.style.setProperty('--font-mono', `'${tweaks.fontMono}', monospace`);
    root.setAttribute('data-compact', tweaks.compactMode ? '1' : '0');
  }, [tweaks]);

  // Persist tweaks → backend (debounced)
  useEffect(() => {
    if (!tweaksLoaded) return;
    if (skipNextPut.current) { skipNextPut.current = false; return; }
    const t = setTimeout(() => {
      apiFetch('/api/tweaks', { method: 'PUT', body: { tweaks } })
        .then(({ ok, status }) => { if (!ok) console.warn('Tweaks PUT fehlgeschlagen:', status); });
    }, 400);
    return () => clearTimeout(t);
  }, [tweaks, tweaksLoaded]);

  const [shortcutsOpen, setShortcutsOpen] = useState(false);

  // ChatClient-Lifecycle: bei eingeloggtem Token verbinden, sonst sauber trennen.
  // Reconnect (Backoff) übernimmt der Client selbst.
  useEffect(() => {
    if (token && window.ChatClient) {
      window.ChatClient.connect(token);
    }
    return () => {
      if (window.ChatClient) window.ChatClient.disconnect();
    };
  }, [token]);

  // Hotkeys
  // - mit Modifier (Ctrl/Cmd): immer aktiv
  // - ohne Modifier: nur wenn der Fokus nicht in Inputs/Editor liegt
  useEffect(() => {
    const isTyping = (target) => {
      if (!target) return false;
      const tag = target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
      if (target.isContentEditable) return true;
      // TinyMCE rendert in iframe — Events bubblen nicht in den Parent, also irrelevant
      return false;
    };
    const handler = (e) => {
      // Modifier-basierte Shortcuts
      if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault(); setRoute({ page: 'search', query: '' }); return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key === ',') {
        e.preventDefault(); setTweaksOpen(o => !o); return;
      }
      // Modifier-frei: nur wenn nicht in Eingabefeld
      if (e.altKey || e.ctrlKey || e.metaKey) return;
      if (isTyping(e.target)) return;
      switch (e.key) {
        case '?':
          e.preventDefault(); setShortcutsOpen(true); break;
        case '/':
          e.preventDefault(); setRoute({ page: 'search', query: '' }); break;
        case 'g':
          e.preventDefault(); setRoute({ page: 'graph' }); break;
        case 'c':
          e.preventDefault(); setRoute({ page: 'calendar' }); break;
        case 'd':
          e.preventDefault(); setRoute({ page: 'dashboard' }); break;
        case 'n':
          // Nur in Section-View: neue Notiz im aktuellen Lernfeld
          if (route.page === 'section' && route.sectionId) {
            e.preventDefault(); setRoute({ page: 'note-new', sectionId: route.sectionId });
          }
          break;
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [route]);

  // Hash → State: initial nach Bootstrap, plus popstate (Back/Forward) und hashchange (manuelle URL-Edits).
  // pushState triggert hashchange nicht, daher kein Loop mit dem Write-Effect unten.
  useEffect(() => {
    if (!user || bootstrapping) return;
    const apply = () => {
      const decoded = window.decodeRoute(window.location.hash);
      setRoute(prev => window.routesEqual(prev, decoded) ? prev : decoded);
    };
    apply();
    window.addEventListener('popstate', apply);
    window.addEventListener('hashchange', apply);
    return () => {
      window.removeEventListener('popstate', apply);
      window.removeEventListener('hashchange', apply);
    };
  }, [user, bootstrapping]);

  // State → Hash: bei jeder route-Änderung URL synchronisieren. No-op-Guard verhindert
  // dass Re-Reads aus dem Hash-Listener neue History-Einträge erzeugen.
  useEffect(() => {
    if (!user || bootstrapping) return;
    const encoded = window.encodeRoute(route);
    const target = encoded ? '#' + encoded : '';
    const current = window.location.hash;
    if (current === target) return;
    const newUrl = window.location.pathname + window.location.search + target;
    window.history.pushState(null, '', newUrl);
  }, [route, user, bootstrapping]);

  // Auth gegen Backend-API
  // Wenn 2FA aktiv: erste Stufe (Passwort) liefert nur einen kurzlebigen totpToken;
  // LoginView ruft dann verifyTotp() mit dem TOTP- oder Backup-Code auf.
  const handleLogin = async (email, password) => {
    const { ok, data } = await apiFetch('/api/auth/login', { method: 'POST', body: { email, password } });
    if (!ok) return { ok: false, error: data.error || 'Login fehlgeschlagen.' };
    if (data.requireTotp) {
      return { ok: true, requireTotp: true, totpToken: data.totpToken, methods: data.methods };
    }
    setToken(data.token);
    setUser(data.user);
    return { ok: true };
  };

  const handleTotpVerify = async ({ totpToken, code, backupCode }) => {
    const body = { totpToken };
    if (code) body.code = code; else if (backupCode) body.backupCode = backupCode;
    const { ok, data } = await apiFetch('/api/auth/2fa/verify', { method: 'POST', body });
    if (!ok) return { ok: false, error: data.error || 'Code ungültig.' };
    setToken(data.token);
    setUser(data.user);
    return { ok: true };
  };

  const handleRegister = async (name, email, password) => {
    const { ok, data } = await apiFetch('/api/auth/register', { method: 'POST', body: { name, email, password } });
    if (!ok) return { ok: false, error: data.error || 'Registrierung fehlgeschlagen.' };
    setToken(data.token);
    setUser(data.user);
    return { ok: true };
  };

  const handleLogout = () => {
    // Fire-and-forget Cookie-Clear auf dem Server.
    apiFetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
    localStorage.removeItem('tb_token');
    if (HK_ENV === 'local') {
      document.cookie = 'hk_token=; Path=/; Max-Age=0; SameSite=Lax';
    } else {
      document.cookie = `hk_token=; Domain=${HK_COOKIE_DOMAIN}; Path=/; Max-Age=0; Secure; SameSite=Lax`;
    }
    // Direkt zum Hub-Root navigieren. KEIN setState davor, sonst feuert der
    // redirect-to-Hub-Login-Effekt synchron und bringt uns zur Login-Seite
    // statt zum anonymen Hub.
    window.location.assign(HK_HUB_URL);
  };

  const noteBody = (note) => ({
    sectionId: note.sectionId || null,
    title: note.title || 'Unbenannt',
    tags: Array.isArray(note.tags) ? note.tags : [],
    content: note.content || '',
    connections: Array.isArray(note.connections) ? note.connections : [],
    isPrivate: !!note.isPrivate,
  });

  const saveNote = async (note, opts = {}) => {
    const { silent = false, navigate = true } = opts;
    const isUpdate = notes.some(n => n.id === note.id);
    const path = isUpdate ? `/api/notes/${note.id}` : '/api/notes';
    const method = isUpdate ? 'PUT' : 'POST';
    const body = isUpdate ? noteBody(note) : { ...noteBody(note), id: note.id };
    const { ok, data, status } = await apiFetch(path, { method, body });
    if (!ok) {
      console.warn('saveNote fehlgeschlagen:', status, data);
      if (!silent) window.toast?.(data?.error || 'Speichern fehlgeschlagen.', 'error');
      return;
    }
    const saved = data.note;
    setNotes(prev => isUpdate
      ? prev.map(n => n.id === saved.id ? saved : n)
      : [...prev, saved]);
    if (!silent) window.toast?.(isUpdate ? 'Notiz gespeichert.' : 'Notiz angelegt.', 'success');
    if (navigate) setRoute({ page: 'note', noteId: saved.id });
  };

  const updateConnections = async (noteId, connections) => {
    const note = notes.find(n => n.id === noteId);
    if (!note) return;
    const { ok, data, status } = await apiFetch(`/api/notes/${noteId}`, {
      method: 'PUT', body: { ...noteBody(note), connections },
    });
    if (!ok) { console.warn('updateConnections fehlgeschlagen:', status, data); return; }
    setNotes(prev => prev.map(n => n.id === noteId ? data.note : n));
  };

  const togglePinNote = async (id) => {
    const current = notes.find(n => n.id === id);
    if (!current) return;
    const next = !current.pinned;
    const { ok, data, status } = await apiFetch(`/api/notes/${id}/pin`, {
      method: 'PATCH', body: { pinned: next },
    });
    if (!ok) { console.warn('togglePin fehlgeschlagen:', status, data); window.toast?.('Pin konnte nicht gesetzt werden.', 'error'); return; }
    setNotes(prev => prev.map(n => n.id === id ? data.note : n));
    window.toast?.(next ? 'Notiz angeheftet.' : 'Pin entfernt.', 'info');
  };

  const deleteNote = async (id) => {
    const { ok, status } = await apiFetch(`/api/notes/${id}`, { method: 'DELETE' });
    if (!ok) { console.warn('deleteNote fehlgeschlagen:', status); window.toast?.('Notiz konnte nicht gelöscht werden.', 'error'); return; }
    setNotes(prev => prev.filter(n => n.id !== id));
    window.toast?.('Notiz gelöscht.', 'success');
  };

  // Bulk-Operationen auf Notes — sequentielle PUT/DELETE, partial-failure-tolerant.
  // Backend hat kein Bulk-Endpoint; bei <50 Notes ist sequenziell schnell genug
  // und vermeidet, dass eine kaputte Note die anderen verhageln kann.
  const bulkDeleteNotes = async (ids) => {
    const deleted = [];
    for (const id of ids) {
      const { ok } = await apiFetch(`/api/notes/${id}`, { method: 'DELETE' });
      if (ok) deleted.push(id);
    }
    if (deleted.length > 0) {
      setNotes(prev => prev.filter(n => !deleted.includes(n.id)));
    }
    if (deleted.length === ids.length) {
      window.toast?.(`${deleted.length} ${deleted.length === 1 ? 'Notiz' : 'Notizen'} gelöscht.`, 'success');
    } else {
      window.toast?.(`${deleted.length} von ${ids.length} gelöscht — Rest fehlgeschlagen.`, 'error');
    }
  };

  const bulkPatchNotes = async (ids, patch) => {
    const updates = [];
    let failed = 0;
    for (const id of ids) {
      const note = notes.find(n => n.id === id);
      if (!note) { failed++; continue; }
      const body = noteBody(note);
      if (patch.sectionId) body.sectionId = patch.sectionId;
      if (patch.addTag) {
        const t = patch.addTag.trim();
        if (t && !body.tags.includes(t)) body.tags = [...body.tags, t];
      }
      const { ok, data } = await apiFetch(`/api/notes/${id}`, { method: 'PUT', body });
      if (ok && data.note) updates.push(data.note);
      else failed++;
    }
    if (updates.length > 0) {
      setNotes(prev => prev.map(n => updates.find(u => u.id === n.id) || n));
    }
    if (failed === 0) {
      const action = patch.sectionId ? 'verschoben' : patch.addTag ? 'getaggt' : 'aktualisiert';
      window.toast?.(`${updates.length} ${updates.length === 1 ? 'Notiz' : 'Notizen'} ${action}.`, 'success');
    } else {
      window.toast?.(`${updates.length} aktualisiert, ${failed} fehlgeschlagen.`, 'error');
    }
  };

  const addDeadline = async (d) => {
    const body = {
      sectionId: d.sectionId || null, title: d.title || 'Unbenannt',
      date: d.date, type: d.type || 'task',
      description: d.description || '', isPrivate: !!d.isPrivate,
      reminderLeadDays: d.reminderLeadDays === undefined ? 1 : d.reminderLeadDays,
    };
    const { ok, data, status } = await apiFetch('/api/deadlines', { method: 'POST', body });
    if (!ok) { console.warn('addDeadline fehlgeschlagen:', status, data); window.toast?.(data?.error || 'Termin konnte nicht angelegt werden.', 'error'); return; }
    setDeadlines(prev => [...prev, data.deadline]);
    window.toast?.('Termin angelegt.', 'success');
    refreshReminders();
  };

  const exportDeadlinesIcs = async () => {
    try {
      const res = await fetch('/api/deadlines/export.ics', {
        headers: { Authorization: 'Bearer ' + (localStorage.getItem('tb_token') || '') },
      });
      if (!res.ok) { window.toast?.('Export fehlgeschlagen.', 'error'); return; }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = 'techbase-termine.ics';
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      window.toast?.('Termine als .ics exportiert.', 'success');
    } catch (err) {
      console.warn('exportDeadlinesIcs:', err);
      window.toast?.('Export fehlgeschlagen.', 'error');
    }
  };

  const deleteDeadline = async (id) => {
    const { ok, status } = await apiFetch(`/api/deadlines/${id}`, { method: 'DELETE' });
    if (!ok) { console.warn('deleteDeadline fehlgeschlagen:', status); window.toast?.('Termin konnte nicht gelöscht werden.', 'error'); return; }
    setDeadlines(prev => prev.filter(d => d.id !== id));
    setReminderSubs(prev => prev.filter(s => s.deadlineId !== id));
    window.toast?.('Termin gelöscht.', 'success');
  };

  const refreshReminders = async () => {
    const { ok, data } = await apiFetch('/api/reminders');
    if (ok) setReminderSubs(data.subscriptions || []);
  };

  const addReminder = async (deadlineId, leadDays = 1) => {
    const { ok, data, status } = await apiFetch('/api/reminders', {
      method: 'POST', body: { deadlineId, leadDays },
    });
    if (!ok) { console.warn('addReminder fehlgeschlagen:', status, data); window.toast?.(data?.error || 'Reminder konnte nicht gesetzt werden.', 'error'); return null; }
    setReminderSubs(prev => [...prev.filter(s => s.id !== data.subscription.id), data.subscription]);
    window.toast?.('Reminder gesetzt.', 'success');
    return data.subscription;
  };

  const updateReminder = async (id, patch) => {
    const { ok, data, status } = await apiFetch(`/api/reminders/${id}`, {
      method: 'PATCH', body: patch,
    });
    if (!ok) { console.warn('updateReminder fehlgeschlagen:', status, data); return null; }
    setReminderSubs(prev => prev.map(s => s.id === data.subscription.id ? data.subscription : s));
    return data.subscription;
  };

  const deleteReminder = async (id) => {
    const { ok, status } = await apiFetch(`/api/reminders/${id}`, { method: 'DELETE' });
    if (!ok) { console.warn('deleteReminder fehlgeschlagen:', status); window.toast?.('Abo konnte nicht entfernt werden.', 'error'); return; }
    setReminderSubs(prev => prev.filter(s => s.id !== id));
    window.toast?.('Reminder entfernt.', 'info');
  };

  const acknowledgeReminder = (id) => updateReminder(id, { acknowledged: true });

  const setReleaseNotifications = (enabled) => {
    setTweaks(p => ({ ...p, releaseNotifications: !!enabled }));
  };

  const markReleaseSeen = (version) => {
    if (!version || tweaks.lastSeenReleaseVersion === version) return;
    setTweaks(p => ({ ...p, lastSeenReleaseVersion: version }));
  };

  const latestRelease = releases[0] || null;
  const hasUnseenRelease = !!latestRelease && tweaks.lastSeenReleaseVersion !== latestRelease.version;

  // "Was ist neu"-Popup einmalig nach Login öffnen, wenn neue Version + Popups nicht deaktiviert.
  const whatsNewShownRef = useRef(false);
  useEffect(() => {
    if (whatsNewShownRef.current) return;
    if (!user || !tweaksLoaded) return;
    if (!hasUnseenRelease) return;
    if (tweaks.releasePopupsDisabled === true) return;
    whatsNewShownRef.current = true;
    setWhatsNewOpen(true);
  }, [user, tweaksLoaded, hasUnseenRelease, tweaks.releasePopupsDisabled]);

  const acknowledgeWhatsNew = () => {
    if (latestRelease) markReleaseSeen(latestRelease.version);
    setWhatsNewOpen(false);
  };
  const suppressWhatsNew = () => {
    if (latestRelease) markReleaseSeen(latestRelease.version);
    setTweaks(p => ({ ...p, releasePopupsDisabled: true }));
    setWhatsNewOpen(false);
  };

  // Globaler MFA-Required-Handler: api.js ruft uns auf, wenn ein 403 mit
  // code=MFA_REQUIRED zurückkommt. UX: Toast + ab in den Security-Tab.
  useEffect(() => {
    window.onMfaRequired = (msg) => {
      window.toast?.(msg || '2FA erforderlich. Aktiviere TOTP, Telegram oder E-Mail.', 'warning');
      setRoute({ page: 'settings', tab: 'security' });
    };
    return () => { delete window.onMfaRequired; };
  }, []);

  // Onboarding-Modal: einmalig nach erstem Login eines neuen Accounts (oder Re-Open).
  // Bestandsuser haben onboardingCompleted=true via DB-Migration → kein Auto-Trigger.
  const onboardingShownRef = useRef(false);
  useEffect(() => {
    if (onboardingShownRef.current) return;
    if (!user || !tweaksLoaded) return;
    if (tweaks.onboardingCompleted === true) return;
    onboardingShownRef.current = true;
    setOnboardingOpen(true);
  }, [user, tweaksLoaded, tweaks.onboardingCompleted]);

  const completeOnboarding = () => {
    setTweaks(p => ({ ...p, onboardingCompleted: true }));
    setOnboardingOpen(false);
  };
  const reopenOnboarding = () => {
    onboardingShownRef.current = true; // Auto-Trigger einmalig deaktivieren
    setOnboardingOpen(true);
  };

  const flashcardBody = (c) => ({
    sectionId: c.sectionId || null,
    question: c.question || '',
    answer: c.answer || '',
    isPrivate: !!c.isPrivate,
  });

  const addFlashcard = async (c) => {
    const { ok, data, status } = await apiFetch('/api/flashcards', { method: 'POST', body: flashcardBody(c) });
    if (!ok) { console.warn('addFlashcard fehlgeschlagen:', status, data); return null; }
    setFlashcards(prev => [data.flashcard, ...prev]);
    return data.flashcard;
  };

  const updateFlashcard = async (c) => {
    const { ok, data, status } = await apiFetch(`/api/flashcards/${c.id}`, { method: 'PUT', body: flashcardBody(c) });
    if (!ok) { console.warn('updateFlashcard fehlgeschlagen:', status, data); return null; }
    setFlashcards(prev => prev.map(x => x.id === data.flashcard.id ? data.flashcard : x));
    return data.flashcard;
  };

  const deleteFlashcard = async (id) => {
    const { ok, status } = await apiFetch(`/api/flashcards/${id}`, { method: 'DELETE' });
    if (!ok) { console.warn('deleteFlashcard fehlgeschlagen:', status); return; }
    setFlashcards(prev => prev.filter(c => c.id !== id));
  };

  // Permission shorthand
  const userCan = (permission) => can(user, permission);

  if (bootstrapping || (token && (!tweaksLoaded || !notesLoaded || !deadlinesLoaded || !flashcardsLoaded || !rolesLoaded || !structureLoaded))) {
    return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg)', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 13 }}>...</div>;
  }

  // Wartungsmodus für eingeloggte User ohne Bypass-Permission. Anonyme User
  // werden vom oberen redirect-to-hub-Effekt sowieso direkt auf den Hub-Login
  // geschickt, dort kümmert sich Haikara um die MaintenanceView.
  const hasMaintenanceBypass = !!user && Array.isArray(user.permissions) && user.permissions.includes('system:bypass_maintenance');
  if (isHeronryGated(publicSettings) && user && !hasMaintenanceBypass) {
    const goToDevLogin = () => {
      localStorage.removeItem('tb_token');
      if (HK_ENV === 'local') document.cookie = 'hk_token=; Path=/; Max-Age=0; SameSite=Lax';
      else                    document.cookie = `hk_token=; Domain=${HK_COOKIE_DOMAIN}; Path=/; Max-Age=0; Secure; SameSite=Lax`;
      window.location.assign(`${HK_HUB_URL}?login=1&return=${encodeURIComponent(window.location.href)}`);
    };
    return <MaintenanceView publicSettings={publicSettings} onLoginClick={goToDevLogin} />;
  }

  if (!user) return <LoginView onLogin={handleLogin} onRegister={handleRegister} onTotpVerify={handleTotpVerify} registrationEnabled={publicSettings.registrationEnabled !== false} />;

  const renderContent = () => {
    switch (route.page) {
      case 'dashboard':
        return <Dashboard user={user} sections={sections} topicAreas={topicAreas} notes={notes} deadlines={deadlines} flashcards={flashcards} setRoute={setRoute} userCan={userCan}
                  reminderSubs={reminderSubs} onAcknowledgeReminder={acknowledgeReminder} />;
      case 'section':
        return <SectionView sectionId={route.sectionId} sections={sections} topicAreas={topicAreas} notes={notes} flashcards={flashcards} setRoute={setRoute} userCan={userCan} currentUser={user} onTogglePin={togglePinNote} onBulkDelete={bulkDeleteNotes} onBulkPatch={bulkPatchNotes} />;
      case 'note':
        return <NoteView noteId={route.noteId} notes={notes} sections={sections} setRoute={setRoute} onDelete={deleteNote} onUpdateConnections={updateConnections} userCan={userCan} />;
      case 'note-edit':
        return <MarkdownEditor noteId={route.noteId} notes={notes} sections={sections} onSave={saveNote} setRoute={setRoute} />;
      case 'note-new':
        return <MarkdownEditor sectionId={route.sectionId} notes={notes} sections={sections} onSave={saveNote} setRoute={setRoute} />;
      case 'flashcards':
        return <FlashcardsView sectionId={route.sectionId} sections={sections} flashcards={flashcards} setRoute={setRoute} userCan={userCan} />;
      case 'flashcards-manage':
        return <FlashcardsManageView sectionId={route.sectionId} sections={sections} flashcards={flashcards}
                  setRoute={setRoute} userCan={userCan} currentUser={user}
                  onAdd={addFlashcard} onUpdate={updateFlashcard} onDelete={deleteFlashcard} />;
      case 'search':
        return <SearchView query={route.query || ''} notes={notes} sections={sections} setRoute={setRoute} />;
      case 'calendar':
        return <CalendarView deadlines={deadlines} sections={sections} onAdd={addDeadline} onDelete={deleteDeadline} userCan={userCan}
                  view={tweaks.calendarView || 'list'}
                  onViewChange={(v) => setTweaks(p => ({ ...p, calendarView: v }))}
                  reminderSubs={reminderSubs} currentUserId={user.id}
                  defaultReminderLeadDays={Math.max(1, Math.min(365, Math.floor(Number(tweaks.reminderLeadDaysDefault) || 1)))}
                  onAddReminder={addReminder} onUpdateReminder={updateReminder} onDeleteReminder={deleteReminder}
                  onExportIcs={exportDeadlinesIcs} />;
      case 'graph':
        return <GraphView notes={notes} sections={sections} setRoute={setRoute} onUpdateConnections={updateConnections} />;
      case 'settings':
      case 'security':
      case 'admin': {
        // Alle drei Routen mappen jetzt auf den vereinheitlichten Settings-Hub.
        // /security und /admin sind Backward-Compat-Aliase: tab wird vom Pfad abgeleitet.
        const tab = route.tab || (route.page === 'security' ? 'security' : route.page === 'admin' ? 'admin' : undefined);
        return <SettingsView
          user={user} setRoute={setRoute} route={{ ...route, tab }} userCan={userCan}
          tweaks={tweaks} setTweaks={setTweaks}
          darkMode={darkMode} setDarkMode={setDarkMode}
          roles={roles} setRoles={setRoles}
          permissionCatalog={permissionCatalog}
          topicAreas={topicAreas} sections={sections}
          refreshStructure={refreshStructure}
          refreshUser={async () => {
            const me = await apiFetch('/api/auth/me');
            if (me.ok) setUser(me.data.user);
          }}
          onSettingsChange={refreshPublicSettings}
          onReopenOnboarding={reopenOnboarding}
        />;
      }
      case 'patchnotes':
        return <PatchNotesView releases={releases} focusVersion={route.version} currentUser={user}
                  releaseNotifications={!!tweaks.releaseNotifications}
                  onToggleSubscription={setReleaseNotifications}
                  onMarkSeen={markReleaseSeen} />;
      default:
        return <Dashboard user={user} sections={sections} topicAreas={topicAreas} notes={notes} deadlines={deadlines} flashcards={flashcards} setRoute={setRoute} userCan={userCan}
                  reminderSubs={reminderSubs} onAcknowledgeReminder={acknowledgeReminder} />;
    }
  };

  const isEditor = route.page === 'note-edit' || route.page === 'note-new';

  const sectionShortFor = (id) => {
    const s = sections.find(x => x.id === id);
    return s ? (s.short || s.label || id) : (id || '?');
  };

  return (
    <div style={{ display: 'flex', height: '100vh', overflow: 'hidden', fontFamily: 'var(--font-sans)' }}>
      <Sidebar
        route={route} setRoute={setRoute}
        user={user} onLogout={handleLogout}
        darkMode={darkMode} setDarkMode={setDarkMode}
        topicAreas={topicAreas} sections={sections} notes={notes}
        userCan={userCan}
        roles={roles}
        onOpenTweaks={() => setTweaksOpen(true)}
        hasUnseenRelease={hasUnseenRelease}
      />
      <main style={{ flex: 1, overflowY: isEditor ? 'hidden' : 'auto', scrollbarGutter: 'stable', background: 'var(--bg)', display: 'flex', flexDirection: 'column' }}>
        <MaintenancePreBanner publicSettings={publicSettings} />
        <MaintenanceBypassBanner publicSettings={publicSettings} />
        {user && user.emailVerified === false && (
          <div style={{
            flexShrink: 0,
            background: 'rgba(251, 191, 36, 0.12)',
            borderBottom: '1px solid rgba(251, 191, 36, 0.3)',
            color: '#fbbf24',
            padding: '10px 24px',
            display: 'flex', alignItems: 'center', gap: 12,
            fontSize: 13,
          }}>
            <span style={{ fontSize: 16 }}>⚠</span>
            <div style={{ flex: 1 }}>
              <strong>Email-Adresse noch nicht bestätigt.</strong>{' '}
              Wir haben dir einen Link an <span style={{ fontFamily: 'var(--font-mono)' }}>{user.email}</span> geschickt.
              {verifyResend.msg && <span style={{ marginLeft: 10, color: verifyResend.state === 'error' ? '#f87171' : '#4ade80' }}>· {verifyResend.msg}</span>}
            </div>
            <button onClick={handleResendVerify} disabled={verifyResend.state === 'sending'}
              style={{
                background: 'rgba(251, 191, 36, 0.18)',
                border: '1px solid rgba(251, 191, 36, 0.4)',
                color: '#fbbf24',
                borderRadius: 6, padding: '4px 10px',
                fontSize: 12, fontFamily: 'var(--font-mono)',
                cursor: verifyResend.state === 'sending' ? 'wait' : 'pointer',
              }}>
              {verifyResend.state === 'sending' ? 'Senden…' : 'Mail erneut senden'}
            </button>
          </div>
        )}
        {!isEditor && (
          <div style={{ height: 42, flexShrink: 0, borderBottom: '1px solid var(--border)', background: 'var(--sidebar-bg)', display: 'flex', alignItems: 'center', padding: '0 24px', gap: 12 }}>
            <div style={{ flex: 1 }}>
              {route.page === 'dashboard'  && <Breadcrumb parts={['dashboard']} />}
              {route.page === 'section'    && <Breadcrumb parts={['sections', sectionShortFor(route.sectionId).toLowerCase().replace(/ /g, '-')]} />}
              {route.page === 'search'     && <Breadcrumb parts={['suche']} />}
              {route.page === 'calendar'   && <Breadcrumb parts={['kalender']} />}
              {route.page === 'patchnotes' && <Breadcrumb parts={['patch-notes']} />}
              {route.page === 'graph'      && <Breadcrumb parts={['graph', 'mindmap']} />}
              {route.page === 'flashcards' && <Breadcrumb parts={['flashcards', sectionShortFor(route.sectionId).toLowerCase().replace(/ /g, '-')]} />}
              {route.page === 'flashcards-manage' && <Breadcrumb parts={['flashcards', sectionShortFor(route.sectionId).toLowerCase().replace(/ /g, '-'), 'verwalten']} />}
              {route.page === 'admin'      && <Breadcrumb parts={['admin']} />}
              {route.page === 'note' && (() => {
                const n = notes.find(x => x.id === route.noteId);
                return <Breadcrumb parts={['notizen', sectionShortFor(n?.sectionId).toLowerCase().replace(/ /g, '-'), n?.title?.toLowerCase().replace(/ /g, '-') || 'note']} />;
              })()}
            </div>
            {/* Quick actions — only for writers+ */}
            {userCan('notes:write') && (
              <button onClick={() => setRoute({ page: 'note-new', sectionId: null })}
                style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '4px 10px', background: 'var(--accent-dim)', border: '1px solid rgba(155,92,246,0.25)', borderRadius: 6, color: 'var(--accent)', cursor: 'pointer', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
                <Icon name="plus" size={12} /> neue notiz
              </button>
            )}
            {/* Role badge */}
            <RoleBadge roleId={user.role} roles={roles} />
          </div>
        )}
        <div style={{ flex: 1, overflow: isEditor ? 'hidden' : 'visible', display: 'flex', flexDirection: 'column' }}>
          {renderContent()}
        </div>
      </main>

      <ToastContainer />

      <WhatsNewModal
        open={whatsNewOpen}
        release={latestRelease}
        onAcknowledge={acknowledgeWhatsNew}
        onSuppress={suppressWhatsNew}
        onClose={() => setWhatsNewOpen(false)}
      />

      <OnboardingModal
        open={onboardingOpen}
        user={user}
        setRoute={setRoute}
        onComplete={completeOnboarding}
      />

      <TweaksPanel open={tweaksOpen} onClose={() => setTweaksOpen(false)}>
        <TweakSection label="Design">
          <TweakColor  label="Akzentfarbe"      value={tweaks.accentColor}  onChange={v => setTweaks(p => ({ ...p, accentColor: v }))} />
          <TweakSlider label="Sidebar-Breite"   value={tweaks.sidebarWidth} min={200} max={340} step={10} unit="px" onChange={v => setTweaks(p => ({ ...p, sidebarWidth: v }))} />
          <TweakSlider label="Schriftgröße"     value={tweaks.fontSize}     min={11}  max={16}  step={1}  unit="px" onChange={v => setTweaks(p => ({ ...p, fontSize: v }))} />
          <TweakToggle label="Kompakter Modus"  value={tweaks.compactMode}                                          onChange={v => setTweaks(p => ({ ...p, compactMode: v }))} />
        </TweakSection>
        <TweakSection label="Schrift">
          <TweakSelect label="Mono-Font" value={tweaks.fontMono} options={['JetBrains Mono', 'Fira Code', 'Source Code Pro', 'Cascadia Code', 'Courier New']} onChange={v => setTweaks(p => ({ ...p, fontMono: v }))} />
        </TweakSection>
        <TweakSection label="Benachrichtigungen">
          <TweakToggle label="Patch-Notes per Mail" value={!!tweaks.releaseNotifications} onChange={v => setTweaks(p => ({ ...p, releaseNotifications: v }))} />
          <TweakToggle label='"Was ist neu"-Popup beim Login' value={tweaks.releasePopupsDisabled !== true} onChange={v => setTweaks(p => ({ ...p, releasePopupsDisabled: !v }))} />
          <TweakSlider label="Reminder-Vorlaufzeit (Default)" value={Number(tweaks.reminderLeadDaysDefault) || 1} min={1} max={14} step={1} unit=" Tag(e)" onChange={v => setTweaks(p => ({ ...p, reminderLeadDaysDefault: v }))} />
        </TweakSection>
      </TweaksPanel>

      <ShortcutsCheatsheet open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
    </div>
  );
};

const SHORTCUTS = [
  { group: 'Navigation', keys: [
    { combo: ['?'],         desc: 'Diese Übersicht öffnen' },
    { combo: ['/'],         desc: 'Suche öffnen' },
    { combo: ['Ctrl', 'K'], desc: 'Suche öffnen' },
    { combo: ['d'],         desc: 'Dashboard' },
    { combo: ['g'],         desc: 'Graph-Ansicht' },
    { combo: ['c'],         desc: 'Kalender' },
  ]},
  { group: 'Aktionen', keys: [
    { combo: ['n'],         desc: 'Neue Notiz im aktuellen Lernfeld' },
    { combo: ['Ctrl', ','], desc: 'Tweaks-Panel umschalten' },
    { combo: ['Esc'],       desc: 'Modal / Panel schließen' },
  ]},
];

const ShortcutsCheatsheet = ({ open, onClose }) => (
  <Modal open={open} onClose={onClose} title="Tastatur-Shortcuts" width={420}>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
      {SHORTCUTS.map(({ group, keys }) => (
        <div key={group}>
          <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>{group}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {keys.map(({ combo, desc }, i) => (
              <div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
                <span style={{ fontSize: 13, color: 'var(--text)' }}>{desc}</span>
                <span style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
                  {combo.map((k, j) => (
                    <kbd key={j} style={{
                      padding: '2px 7px',
                      background: 'var(--surface-2)',
                      border: '1px solid var(--border)',
                      borderRadius: 4,
                      fontFamily: 'var(--font-mono)',
                      fontSize: 11,
                      color: 'var(--text)',
                      minWidth: 18,
                      textAlign: 'center',
                    }}>{k}</kbd>
                  ))}
                </span>
              </div>
            ))}
          </div>
        </div>
      ))}
      <div style={{ fontSize: 11, color: 'var(--text-dim)', borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 4 }}>
        Shortcuts ohne Modifier (n, g, c, …) greifen nur, wenn der Cursor nicht in einem Eingabefeld steht.
      </div>
    </div>
  </Modal>
);

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
