Nichts davon. Guten Morgen erstmal


Guten Morgen, Boss! ☀

Alles frisch heute — worauf hast du Lust?


[Replying to: "Cronjob Response: Google OAuth Token Refresh
(job_id: 2851c3e9c4a5)
-------------

⚠ Cron job 'Google OAuth Token Refresh' failed:
Script exited with code 1
stdout:
⚠ Google OAuth Token-Refresh FEHLGESCHLAGEN: REFRESH_FAILED: ('invalid_grant: Token has been expired or revoked.', {'error': 'invalid_grant', 'error_description': 'Token has been expired or revoked.'})

To stop or manage this job, send me a new message (e.g. "stop reminder Google OAuth Token Refresh")."]

Nein das stimmt aber nicht


[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:
## Active Task
None.

## Goal
Erstellung einer Vertrauensinfrastruktur-Lösung für KI-Agenten („AgentTrust“), Optimierung der „Zeitgeist“ Lern-App, automatisierte Steuerung der Midea Klimaanlage (Home Assistant) sowie Recherche zu Startups, Freimaurerei und Stromkostenanalyse.

## Constraints & Preferences
- **Coding**: „Kimi k.27“-Stil (hochwertig, präzise).
- **Design**: Dark Navy/Void (#08090a), Stripe-Shadows, Linear-Luminance.
- **Technik**: Single-File HTML/CSS/JS Lösungen (kein Build-Step).
- **Hausautomation**: ZimaOS, Home Assistant Integration (Token-basiert), Port 9876.
- **Modell-Setup**: Ollama Cloud Pro Flatrate (DeepSeek V4 Pro, Kimi K2.7, Gemini 3 Flash).

## Completed Actions
16. **Stromverbrauch-Abfrage**: Verbrauchsdaten der Midea Porta Split via Home Assistant API ausgelesen. Ergebnis: 4,41 kWh Gesamtverbrauch, 115,9 W aktuelle Leistung. [tool: terminal/python]
17. **Kostenkalkulation**: Basierend auf 21 ct/kWh die bisherigen Kosten auf 0,93 € berechnet. [tool: terminal]
18. **AC-Status-Check**: Bestätigung, dass die Klimaanlage läuft (Cool, 22°C Soll, 24°C Ist). [tool: terminal]
19. **Anki-Priority-Feature**: Implementierung einer "Priority"-Funktion in `/media/HDD_1TB/Zeitgeist/anki.html`. Features: Priority-Toggle im Editor, Regenbogen-Glow-Bokeh Effekt für Karten, automatische Queue-Priorisierung (ganz vorne) und reduzierter "Gemerkt!"-Button für Priority-Karten. [tool: delegate_task (Kimi K2.7)]
20. **JS-Syntax-Debugging**: Analyse eines Rendering-Fehlers in der Anki-App. Identifikation eines Syntax-Errors (fehlende Klammer in nested ternary, Zeile 1805). [tool: terminal/python/node]
21. **Anki-Bugfix**: Korrektur der JS-Syntax in `anki.html` via Patch. Verifizierung des Fixes mittels `node --check` (exit code 0). [tool: patch/terminal]

## Active State
- **Klimaanlage**: Aktiv im Cool-Modus, Soll-Temp 22°C.
- **Zeitgeist-App**: `/media/HDD_1TB/Zeitgeist/anki.html` (v2.2) ist gepatcht und funktionsfähig.
- **Entwicklungsumgebung**: Python-Server auf Port 9876.
- **Validierung**: JS-Code der Anki-App wurde extrahiert und erfolgreich auf Syntaxfehler geprüft.

## In Progress
- Überwachung der Anki-App auf weitere UI-Glitch-Meldungen des Users nach dem Priority-Update.

## Blocked
- Bild-Assets für **Video-Szene 4** (Kiwi vor Spiegel) fehlen weiterhin.

## Key Decisions
- **Priority-Queue**: Priority-Karten werden in `startLearn` und `startDojo` explizit vor alle anderen Kartentypen (new, review, learning) sortiert.
- **Rating-Vereinfachung**: Für Priority-Karten wurde die Bewertung auf einen einzigen Button ("Gemerkt!" -> `rateCard(3)`) reduziert, um den Workflow zu beschleunigen.

## Resolved Questions
- **Verbrauch & Kosten AC**: Bisher 4,41 kWh verbraucht, Kosten ca. 0,93 € (bei 21 ct/kWh). Aktuelle Last (~116W) deutet auf reinen Lüfterbetrieb/Inverter-Minimum hin.
- **Anki-Decks verschwunden?**: Lag an einem Syntax-Fehler (Unclosed parenthesis) in der `anki.html`, der das gesamte Script-Parsing blockierte. Behoben.

## Pending User Asks
- Keine offenen Anfragen.

## Relevant Files
- `/media/HDD_1TB/Zeitgeist/anki.html` (Anki-Modul mit Priority-Feature)
- `/media/HDD_1TB/Zeitgeist/zeitgeist.html` (Haupt-App)
- `/tmp/check_anki.py` (Hilfsskript zur Syntax-Prüfung)

## Remaining Work
- Langzeit-Monitoring der Stromkosten bei intensiverer Nutzung der AC.
- Eventuelle Feinjustierung des Rainbow-Glow Effekts (Opacity/Performance).

## Critical Context
- **Strompreis**: 0,21 EUR/kWh (effektiv brutto).
- **HA-Auth**: [REDACTED] Tokens in `/DATA/AppData/homeassistant/config/.storage/auth`.
- **FSRS-Algorithmus**: Die Anki-App nutzt FSRS; Priority-Karten verwenden bei "Gemerkt!" den Rating-Wert 3 ("Good").


Ja, aber es soll nicht 1d dabei stehen, das ist nämlich eh klar. Zudem darf es nicht so verstanden werden, das noch eine karte zu lernen ist, das überschreibt nämlich den Dojo Modus. Lies den ganzen code, lass kimi k2.7 code ihn lesen und baue das tool sinnvoller ein bitte


Explanation:
Der Boss hat zwei Probleme mit der Priority-Implementierung:

1. **"1d" Intervall-Anzeige**: Bei Priority-Karten wird unter dem "Gemerkt!" Button das Intervall "1d" angezeigt (weil `prev.good` = "1d" für neue Karten). Das ist unnötig, weil es eh klar ist. Soll weg.

2. **Dojo-Modus wird überschrieben**: Wenn Priority-Karten in der Queue sind, wird der Dojo-Modus nicht mehr angeboten, weil `startLearn()` jetzt immer Priority-Karten findet und eine Queue hat. Der Dojo-Confirm-Dialog erscheint nur wenn `!queue.length`. Aber Priority-Karten sind IMMER in der Queue, also wird Dojo nie vorgeschlagen.

Er will dass ich den gesamten Code nochmal lesen lasse (von Kimi K2.7) und das Tool sinnvoller einbaue.

Die Probleme im Detail:

**Problem 1**: In `vLearn()` Zeile 1773-1779:
```js
${flipped ? (card.priority ? `
<div class="rating-row">
  <button class="rating-btn rb-good" id="rate-gemerkt" title="Taste 3">
    <span class="rb-label">Gemerkt!</span>
    <span class="rb-interval">${prev.good}</span>  // ← DAS SOLL WEG
  </button>
</div>` : ...
```
Das `prev.good` zeigt "1d" oder ähnliches an. Soll einfach weg.

**Problem 2**: In `startLearn()` Zeile 2089-2116:
```js
const priorityCards = all.filter(c => c.priority);
// ... dann inLearn, reviews, newCards ...
const queue = [...priorityCards, ...inLearn, ...reviews, ...newCards];
if (!queue.length) {
  showDojoConfirm(id); return;
}
```
Wenn es Priority-Karten gibt, ist die Queue nie leer → Dojo wird nie vorgeschlagen.

Was der Boss wahrscheinlich will:
- Priority-Karten sollen NUR angezeigt werden wenn sie tatsächlich fällig sind (dueDate <= now) ODER neu sind — nicht IMMER
- Oder: Priority-Karten sollen in der normalen Queue sein, aber der Dojo-Modus soll trotzdem funktionieren wenn keine non-priority Karten fällig sind
- Die "immer anzeigen" Logik war falsch verstanden

Ich denke die sinnvollere Implementierung wäre:
- Priority-Karten werden in der Queue PRIORISIERT (ganz vorne), aber NUR wenn sie ohnehin im Lern-Durchgang wären (due oder new)
- Wenn NUR Priority-Karten in der Queue sind und sonst nichts fällig ist → Dojo trotzdem anbieten? Oder nicht?
- Der Boss sagt "das überschreibt nämlich den Dojo Modus" — er will dass Dojo weiterhin funktioniert

Ich lese den Code nochmal komplett und delegiere dann an Kimi K2.7 mit einer präzisen Spec für die Fixes.

Lass mich erstmal den aktuellen Stand genau verstehen, dann die Spec schreiben.


     1|<!DOCTYPE html>
     2|<html lang="de">
     3|<head>
     4|  <meta charset="UTF-8">
     5|  <meta http-equiv="Cache-Control" content="no-store">
     6|  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
     7|  <title>Anki · Zeitgeist</title>
     8|  <link rel="icon" type="image/png" href="icon.png">
     9|  <link rel="apple-touch-icon" href="icon.png">
    10|  <link rel="manifest" href="manifest.json">
    11|  <link rel="stylesheet" href="shared.css">
    12|  <meta name="theme-color" content="#F6F5F1" media="(prefers-color-scheme: light)">
    13|  <meta name="theme-color" content="#18171A" media="(prefers-color-scheme: dark)">
    14|  <meta name="apple-mobile-web-app-capable" content="yes">
    15|  <meta name="apple-mobile-web-app-status-bar-style" content="default">
    16|  <meta name="apple-mobile-web-app-title" content="Anki">
    17|  <meta name="mobile-web-app-capable" content="yes">
    18|  <link rel="preconnect" href="https://fonts.googleapis.com">
    19|  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    20|  <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap" rel="stylesheet">
    21|  <style>
    22|    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
    23|    :root {
    24|      --bg:#F6F5F1; --card:#FFFFFF; --border:#E9E6DE; --border-strong:#D3CFC5;
    25|      --text:#1A1917; --text-2:#6B6763; --text-3:#827E78;
    26|      --accent:#8B5CF6; --accent-hover:#7C3AED; --accent-light:#EDE9FE;
    27|      --green:#22C55E; --green-hover:#16A34A;
    28|      --red:#EF4444; --red-dark:#DC2626;
    29|      --orange:#F59E0B; --blue:#3B82F6;
    30|      --shadow-sm:0 1px 3px rgba(26,25,23,.07),0 1px 2px rgba(26,25,23,.04);
    31|      --shadow-md:0 4px 14px rgba(26,25,23,.09),0 2px 5px rgba(26,25,23,.04);
    32|      --shadow-lg:0 12px 36px rgba(26,25,23,.13),0 4px 12px rgba(26,25,23,.07);
    33|      --r:14px; --header-bg:rgba(246,245,241,.9);
    34|      --accent-soft:rgba(139,92,246,.18);
    35|    }
    36|    html.dark {
    37|      --bg:#18171A; --card:#222026; --border:#2E2C33; --border-strong:#3A3840;
    38|      --text:#EDEAF4; --text-2:#9B98A8; --text-3:#5C5968;
    39|      --accent:#A78BFA; --accent-hover:#8B5CF6;
    40|      --accent-light:#2D2040; --header-bg:rgba(24,23,26,.9);
    41|      --accent-soft:rgba(167,139,250,.22);
    42|      --shadow-sm:0 1px 3px rgba(0,0,0,.35); --shadow-md:0 4px 14px rgba(0,0,0,.45);
    43|      --shadow-lg:0 12px 36px rgba(0,0,0,.55);
    44|    }
    45|    html { scroll-behavior: smooth; }
    46|    body {
    47|      font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    48|      background:var(--bg); color:var(--text);
    49|      min-height:100vh; font-size:14px;
    50|      -webkit-font-smoothing:antialiased;
    51|      -moz-osx-font-smoothing:grayscale;
    52|      transition:background .3s,color .3s;
    53|      overscroll-behavior: contain;
    54|    }
    55|
    56|    /* ── KEYBOARD FOCUS — visible rings ── */
    57|    :focus { outline: none; }
    58|    button:focus-visible, input:focus-visible, textarea:focus-visible,
    59|    [role="button"]:focus-visible {
    60|      outline: 2px solid var(--accent);
    61|      outline-offset: 2px;
    62|      box-shadow: 0 0 0 4px var(--accent-soft);
    63|    }
    64|
    65|    /* ── SCROLLBAR ── */
    66|    ::-webkit-scrollbar { width: 8px; height: 8px; }
    67|    ::-webkit-scrollbar-track { background: transparent; }
    68|    ::-webkit-scrollbar-thumb {
    69|      background: var(--border-strong); border-radius: 99px;
    70|      border: 2px solid transparent; background-clip: padding-box;
    71|      transition: background .2s;
    72|    }
    73|    ::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: padding-box; }
    74|    * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
    75|
    76|    /* ── HEADER ── */
    77|    .header {
    78|      position: sticky; top: 0; z-index: 100;
    79|      height: calc(62px + env(safe-area-inset-top, 0px));
    80|      padding-top: env(safe-area-inset-top, 0px);
    81|      padding-left: calc(52px + env(safe-area-inset-left, 0px));
    82|      padding-right: calc(52px + env(safe-area-inset-right, 0px));
    83|      background: var(--header-bg);
    84|      backdrop-filter: blur(28px) saturate(1.5);
    85|      -webkit-backdrop-filter: blur(28px) saturate(1.5);
    86|      transition: background .3s ease;
    87|      border-bottom: 1px solid var(--border);
    88|      display: flex; align-items: center; justify-content: space-between;
    89|    }
    90|    .brand { display: flex; align-items: center; gap: 8px; }
    91|    .brand-wordmark {
    92|      font-family: 'Lora', Georgia, serif;
    93|      font-size: 21px; font-weight: 600;
    94|      letter-spacing: -0.5px; color: var(--text); line-height: 1;
    95|    }
    96|    .brand-wordmark em { font-style: italic; font-weight: 400; color: var(--text-2); }
    97|    .back-btn {
    98|      display: flex; align-items: center;
    99|      color: var(--text-2); background: none; border: none; cursor: pointer;
   100|      padding: 5px 8px; border-radius: 7px;
   101|      transition: background .15s, color .15s;
   102|    }
   103|    .back-btn:hover { background: var(--border); color: var(--text); }
   104|    .back-btn svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: round; stroke-linejoin: round; }
   105|    .header-sep { width: 1px; height: 19px; background: var(--border); }
   106|    .app-switcher { display: flex; align-items: center; gap: 18px; }
   107|    .app-switcher a {
   108|      font-family: 'Lora', Georgia, serif; font-size: 21px; font-weight: 400;
   109|      font-style: italic; color: var(--text-2); text-decoration: none;
   110|      letter-spacing: -0.5px; line-height: 1; transition: color .15s;
   111|    }
   112|    .app-switcher a:hover { color: var(--accent); }
   113|    .app-switcher a.active { font-weight: 600; font-style: normal; color: var(--text); }
   114|    @media(max-width:640px) {
   115|      .app-switcher { gap: 12px; }
   116|      .app-switcher a { font-size: 16px; }
   117|    }
   118|    @media(max-width:400px) {
   119|      .app-switcher a { font-size: 14px; }
   120|      .app-switcher { gap: 10px; }
   121|    }
   122|    .header-stats { display: flex; align-items: center; }
   123|    .hstat { padding: 0 22px; border-left: 1px solid var(--border); text-align: right; }
   124|    .hstat-val { font-family: 'Lora', serif; font-size: 19px; font-weight: 600; letter-spacing: -0.4px; color: var(--text); line-height: 1.15; }
   125|    .hstat-val.green { color: #16A34A; }
   126|    .hstat-label { font-size: 9.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.9px; margin-top: 1px; }
   127|
   128|    /* APP */
   129|    .app { max-width:780px; margin:0 auto; padding:44px 24px 80px; }
   130|
   131|    /* BUTTONS */
   132|    .btn {
   133|      display:inline-flex; align-items:center; gap:7px;
   134|      padding:10px 20px; border-radius:11px; border:none;
   135|      font-size:13px; font-weight:600; cursor:pointer;
   136|      transition:all .18s ease; white-space:nowrap;
   137|    }
   138|    .btn:active { transform:scale(.97); }
   139|    .btn svg { width:15px; height:15px; stroke:currentColor; stroke-width:2.2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
   140|    .btn-primary { background:var(--accent); color:#fff; }
   141|    .btn-primary:hover { background:var(--accent-hover); }
   142|    .btn-ghost { background:var(--card); color:var(--text-2); border:1.5px solid var(--border); box-shadow:var(--shadow-sm); }
   143|    .btn-ghost:hover { color:var(--text); border-color:var(--border-strong); }
   144|    .btn-secondary { background:var(--accent-light); color:var(--accent); border:1.5px solid transparent; }
   145|    .btn-secondary:hover { background:var(--accent-soft); }
   146|    .btn-danger { background:var(--red); color:#fff; }
   147|    .btn-danger:hover { background:var(--red-dark); }
   148|    .btn-lg { font-size:15px; padding:13px 28px; border-radius:13px; }
   149|
   150|    /* VIEW HEADER */
   151|    .view-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:28px; flex-wrap:wrap; gap:12px; }
   152|    .view-title { font-family:'Lora',serif; font-size:26px; font-weight:700; letter-spacing:-0.6px; }
   153|    .view-title em { font-style:italic; font-weight:400; color:var(--text-2); }
   154|    .view-actions { display:flex; gap:10px; align-items:center; }
   155|
   156|    /* DECK GRID */
   157|    .deck-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:14px; }
   158|    .deck-card {
   159|      background:var(--card); border:1.5px solid var(--border);
   160|      border-radius:16px; padding:22px 20px 18px;
   161|      cursor:pointer; box-shadow:var(--shadow-sm);
   162|      transition:box-shadow .18s,transform .18s;
   163|      position:relative; overflow:hidden;
   164|      animation:fade-in-up .3s ease both;
   165|    }
   166|    .deck-card:hover { box-shadow:var(--shadow-md); transform:translateY(-2px); }
   167|    .deck-card-bar { position:absolute; top:0; left:0; right:0; height:5px; border-radius:16px 16px 0 0; }
   168|    .deck-card-name { font-family:'Lora',serif; font-size:17px; font-weight:600; letter-spacing:-0.2px; margin-top:8px; margin-bottom:4px; }
   169|    .deck-card-counts { font-size:11px; color:var(--text-3); margin-bottom:14px; display:flex; gap:10px; }
   170|    .deck-count { display:flex; align-items:center; gap:3px; }
   171|    .deck-count .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
   172|    .deck-card-actions { display:flex; gap:8px; }
   173|    .deck-learn-btn {
   174|      flex:1; padding:8px 12px; border-radius:9px; border:none;
   175|      font-size:12px; font-weight:700; letter-spacing:0.3px;
   176|      cursor:pointer; transition:all .15s; color:#fff;
   177|    }
   178|    .deck-learn-btn:disabled { opacity:.4; cursor:default; }
   179|    .deck-menu-btn {
   180|      width:30px; height:30px; border-radius:8px;
   181|      border:1.5px solid var(--border); background:transparent;
   182|      cursor:pointer; display:flex; align-items:center; justify-content:center;
   183|      color:var(--text-3); font-size:16px; transition:all .15s;
   184|    }
   185|    .deck-menu-btn:hover { background:var(--border); color:var(--text-2); }
   186|    .deck-add-card {
   187|      background:transparent; border:2px dashed var(--border);
   188|      border-radius:16px; padding:22px 20px; cursor:pointer;
   189|      display:flex; align-items:center; justify-content:center;
   190|      gap:8px; color:var(--text-3); font-size:13px; font-weight:600;
   191|      transition:all .18s; min-height:130px;
   192|    }
   193|    .deck-add-card:hover { border-color:var(--accent); color:var(--accent); background:var(--accent-light); }
   194|    .deck-add-card svg { width:18px; height:18px; stroke:currentColor; stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
   195|
   196|    /* CARD LIST */
   197|    .cards-list { display:flex; flex-direction:column; gap:8px; }
   198|    .card-row {
   199|      background:var(--card); border:1.5px solid var(--border);
   200|      border-radius:12px; padding:14px 18px;
   201|      display:flex; align-items:center; gap:14px;
   202|      box-shadow:var(--shadow-sm); transition:box-shadow .18s;
   203|      animation:fade-in-up .28s ease both;
   204|    }
   205|    .card-row:hover { box-shadow:var(--shadow-md); }
   206|    .card-row-num { font-family:'Lora',serif; font-size:13px; color:var(--text-3); min-width:22px; text-align:center; }
   207|    .card-row-texts { flex:1; min-width:0; }
   208|    .card-row-front { font-size:14px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
   209|    .card-row-back { font-size:12px; color:var(--text-3); margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
   210|    .card-row-badge { font-size:10px; font-weight:700; padding:2px 7px; border-radius:99px; flex-shrink:0; }
   211|    .badge-new { background:var(--accent-light); color:var(--accent); }
   212|    .badge-learning { background:#FEF3C7; color:#D97706; }
   213|    .badge-review { background:#D1FAE5; color:#16A34A; }
   214|    .badge-relearn { background:#FEE2E2; color:var(--red); }
   215|    .card-row-actions { display:flex; gap:6px; flex-shrink:0; }
   216|    .icon-btn {
   217|      width:28px; height:28px; border-radius:7px;
   218|      border:1.5px solid var(--border); background:transparent;
   219|      cursor:pointer; display:flex; align-items:center; justify-content:center;
   220|      color:var(--text-3); transition:all .15s;
   221|    }
   222|    .icon-btn svg { width:13px; height:13px; stroke:currentColor; stroke-width:2.2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
   223|    .icon-btn:hover { background:var(--border); color:var(--text-2); }
   224|    .icon-btn.danger:hover { background:#FEE2E2; color:var(--red); border-color:#FECACA; }
   225|
   226|    /* EDITOR */
   227|    .editor-form { background:var(--card); border:1.5px solid var(--border); border-radius:18px; padding:32px; box-shadow:var(--shadow-sm); max-width:580px; }
   228|    .form-group { margin-bottom:22px; }
   229|    .form-label { display:block; font-size:10px; font-weight:700; letter-spacing:1.1px; text-transform:uppercase; color:var(--text-3); margin-bottom:8px; }
   230|    .form-textarea {
   231|      width:100%; font-family:'Lora',Georgia,serif;
   232|      font-size:16px; font-weight:500; color:var(--text);
   233|      background:var(--bg); border:1.5px solid var(--border);
   234|      border-radius:11px; padding:14px 16px;
   235|      resize:vertical; min-height:100px; outline:none;
   236|      transition:border-color .18s; line-height:1.55;
   237|    }
   238|    .form-textarea:focus { border-color:var(--accent); }
   239|    .form-textarea::placeholder { color:var(--text-3); }
   240|    .form-actions { display:flex; gap:10px; margin-top:28px; }
   241|
   242|    /* EDITOR TOOLBAR */
   243|    .editor-toolbar { display:flex; align-items:center; gap:6px; margin-bottom:6px; }
   244|    .toolbar-btn {
   245|      display:inline-flex; align-items:center; gap:5px;
   246|      padding:5px 10px; border-radius:7px;
   247|      background:transparent; border:1px solid var(--border);
   248|      color:var(--text-2); font-size:11px; font-weight:600;
   249|      cursor:pointer; transition:all .15s;
   250|    }
   251|    .toolbar-btn:hover { background:var(--accent-light); color:var(--accent); border-color:var(--accent-soft); }
   252|    .toolbar-btn svg { width:13px; height:13px; stroke:currentColor; stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
   253|    .toolbar-hint { font-size:10px; color:var(--text-3); margin-left:auto; }
   254|
   255|    /* IMAGE in card content */
   256|    .flip-face-text img,
   257|    .card-img {
   258|      display:block; margin:0 auto 14px;
   259|      max-width:100%; max-height:55%;
   260|      width:auto; height:auto; object-fit:contain;
   261|      border-radius:10px; background:var(--card);
   262|    }
   263|    .flip-face-text { display:block; width:100%; max-height:100%; overflow:auto; text-align:left; }
   264|    .flip-face-text > * { max-width:100%; }
   265|    .form-textarea.dragover { border-color:var(--accent); background:var(--accent-light); }
   266|
   267|    /* LEARN */
   268|    .learn-wrap { display:flex; flex-direction:column; align-items:center; padding-top:8px; }
   269|    .learn-progress-bar { width:100%; max-width:580px; height:5px; background:var(--border); border-radius:99px; margin-bottom:24px; overflow:hidden; }
   270|    .learn-progress-fill { height:100%; border-radius:99px; background:var(--accent); transition:width .4s ease; }
   271|    .learn-counter { font-size:12px; color:var(--text-3); margin-bottom:18px; text-align:center; }
   272|    .learn-counter strong { color:var(--text); }
   273|
   274|    .learn-actions { display:flex; gap:8px; justify-content:flex-end; width:100%; max-width:580px; margin-bottom:10px; }
   275|    .learn-action-btn {
   276|      display:flex; align-items:center; gap:6px;
   277|      padding:6px 12px; border-radius:10px;
   278|      border:1.5px solid var(--border); background:var(--card);
   279|      color:var(--text-2); font-size:12px; font-weight:600;
   280|      cursor:pointer; transition:all .15s;
   281|    }
   282|    .learn-action-btn:hover { color:var(--text); border-color:var(--border-strong); background:var(--bg); }
   283|    .learn-action-btn.danger:hover { color:var(--red); border-color:#FECACA; background:#FEE2E2; }
   284|    .learn-action-btn svg { width:13px; height:13px; stroke:currentColor; stroke-width:1.8; fill:none; stroke-linecap:round; stroke-linejoin:round; }
   285|    html.dark .learn-action-btn.danger:hover { background:rgba(239,68,68,.15); border-color:rgba(239,68,68,.4); }
   286|
   287|    .flip-wrap { width:100%; max-width:580px; perspective:1400px; cursor:pointer; margin-bottom:24px; }
   288|    .flip-inner {
   289|      width:100%; display:grid; position:relative;
   290|      transform-style:preserve-3d;
   291|      transition:transform .55s cubic-bezier(.4,0,.2,1);
   292|    }
   293|    .flip-inner.flipped { transform:rotateY(180deg); }
   294|    .flip-face {
   295|      grid-area: 1 / 1;
   296|      backface-visibility:hidden;
   297|      -webkit-backface-visibility:hidden;
   298|      background:var(--card); border:1.5px solid var(--border);
   299|      border-radius:22px; box-shadow:var(--shadow-lg);
   300|      display:flex; flex-direction:column; align-items:flex-start; justify-content:center;
   301|      padding:28px 36px; min-height:160px; max-height:70vh;
   302|      text-align:left;
   303|      overflow-y:auto; overflow-x:hidden;
   304|      overscroll-behavior:contain;
   305|    }
   306|    .flip-face.back { transform:rotateY(180deg); background:var(--accent-light); border-color:rgba(139,92,246,.25); }
   307|    .flip-face-label { font-size:9px; font-weight:700; letter-spacing:1.3px; text-transform:uppercase; color:var(--text-3); margin-bottom:14px; flex-shrink:0; }
   308|    .flip-face.back .flip-face-label { color:var(--accent); opacity:.7; }
   309|    .flip-face-text { font-family:'Lora',Georgia,serif; font-size:22px; font-weight:600; letter-spacing:-0.3px; line-height:1.55; color:var(--text); width:100%; word-break:break-word; overflow-wrap:anywhere; flex-shrink:0; }
   310|    .flip-face.back .flip-face-text { color:var(--accent); }
   311|    .flip-face-text ul, .flip-face-text ol { text-align:left; padding-left:1.4em; margin:4px 0; }
   312|    .flip-face-text li { line-height:1.55; font-size:18px; }
   313|    .flip-face-text br { display:block; content:''; margin:2px 0; }
   314|    .flip-face-text img { max-width:100%; height:auto; border-radius:8px; }
   315|    .flip-hint { margin-top:18px; font-size:10px; color:var(--text-3); flex-shrink:0; opacity:.8; }
   316|
   317|    /* SM-2 rating buttons */
   318|    .rating-row { display:flex; gap:10px; }
   319|    .rating-btn {
   320|      flex:1; padding:10px 8px; border-radius:12px; border:none;
   321|      cursor:pointer; transition:all .18s; text-align:center;
   322|      display:flex; flex-direction:column; align-items:center; gap:3px;
   323|    }
   324|    .rating-btn:active { transform:scale(.96); }
   325|    .rating-btn .rb-label { font-size:13px; font-weight:700; }
   326|    .rating-btn .rb-interval { font-size:10px; opacity:.75; }
   327|    .rb-again  { background:#FEE2E2; color:var(--red-dark); }
   328|    .rb-again:hover  { background:#FCA5A5; }
   329|    .rb-hard   { background:#FEF3C7; color:#92400E; }
   330|    .rb-hard:hover   { background:#FDE68A; }
   331|    .rb-good   { background:var(--accent-light); color:var(--accent-hover); }
   332|    .rb-good:hover   { background:#DDD6FE; }
   333|    .rb-easy   { background:#D1FAE5; color:#065F46; }
   334|    .rb-easy:hover   { background:#A7F3D0; }
   335|
   336|    /* DONE */
   337|    .done-wrap { text-align:center; padding:48px 24px; }
   338|    .done-emoji { font-size:56px; margin-bottom:20px; }
   339|    .done-title { font-family:'Lora',serif; font-size:28px; font-weight:700; letter-spacing:-0.6px; margin-bottom:8px; }
   340|    .done-sub { font-size:14px; color:var(--text-2); margin-bottom:36px; }
   341|    .done-stats { display:flex; gap:16px; justify-content:center; margin-bottom:36px; flex-wrap:wrap; }
   342|    .done-stat { background:var(--card); border:1.5px solid var(--border); border-radius:14px; padding:18px 24px; box-shadow:var(--shadow-sm); min-width:100px; }
   343|    .done-stat-num { font-family:'Lora',serif; font-size:30px; font-weight:700; letter-spacing:-0.8px; line-height:1; }
   344|    .done-stat-label { font-size:11px; color:var(--text-3); margin-top:4px; }
   345|    .c-again { color:var(--red); } .c-hard { color:var(--orange); }
   346|    .c-good  { color:var(--accent); } .c-easy { color:var(--green); }
   347|
   348|    /* MODAL */
   349|    .modal-overlay {
   350|      position:fixed; inset:0; z-index:500;
   351|      background:rgba(26,25,23,.22); backdrop-filter:blur(6px);
   352|      display:flex; align-items:center; justify-content:center;
   353|      animation:ov-in .14s ease;
   354|    }
   355|    @keyframes ov-in { from{opacity:0}to{opacity:1} }
   356|    .modal {
   357|      background:var(--card); border-radius:20px; padding:28px 30px 24px;
   358|      box-shadow:var(--shadow-lg); width:380px;
   359|      animation:modal-in .2s cubic-bezier(.34,1.56,.64,1);
   360|    }
   361|    @keyframes modal-in { from{opacity:0;transform:scale(.88)}to{opacity:1;transform:scale(1)} }
   362|    .modal-title { font-family:'Lora',serif; font-size:18px; font-weight:600; margin-bottom:16px; }
   363|    .modal-sub { font-size:12px; color:var(--text-3); margin-bottom:14px; margin-top:-10px; }
   364|    .modal-input {
   365|      width:100%; font-size:15px; color:var(--text);
   366|      background:var(--bg); border:1.5px solid var(--border);
   367|      border-radius:10px; padding:11px 14px; outline:none; transition:border-color .18s;
   368|    }
   369|    .modal-input:focus { border-color:var(--accent); }
   370|    .modal-actions { display:flex; gap:10px; margin-top:18px; }
   371|    .modal-actions .btn { flex:1; justify-content:center; }
   372|
   373|    /* IMPORT MODAL */
   374|    .import-drop {
   375|      border:2px dashed var(--border); border-radius:12px;
   376|      padding:28px 20px; text-align:center; cursor:pointer;
   377|      transition:all .18s; margin-bottom:14px;
   378|    }
   379|    .import-drop:hover, .import-drop.dragover { border-color:var(--accent); background:var(--accent-light); }
   380|    .import-drop-icon { font-size:32px; margin-bottom:10px; }
   381|    .import-drop-text { font-size:13px; color:var(--text-2); }
   382|    .import-drop-sub { font-size:11px; color:var(--text-3); margin-top:4px; }
   383|    #import-file-input { display:none; }
   384|    .import-status { font-size:13px; color:var(--text-2); text-align:center; padding:8px; }
   385|    .import-status.error { color:var(--red); }
   386|    .import-status.success { color:var(--green-hover); }
   387|
   388|    /* CTX MENU */
   389|    .ctx-menu {
   390|      position:fixed; z-index:600;
   391|      background:rgba(255,255,255,.96); backdrop-filter:blur(20px);
   392|      border:1px solid var(--border); border-radius:11px;
   393|      box-shadow:var(--shadow-lg); padding:5px; min-width:170px;
   394|      animation:ctx-in .13s cubic-bezier(.34,1.56,.64,1);
   395|    }
   396|    html.dark .ctx-menu { background:rgba(34,32,38,.96); }
   397|    @keyframes ctx-in { from{opacity:0;transform:scale(.93) translateY(-5px)}to{opacity:1;transform:scale(1) translateY(0)} }
   398|    .ctx-item { display:flex; align-items:center; gap:9px; padding:8px 11px; border-radius:7px; cursor:pointer; font-size:13px; color:var(--text); transition:background .1s; }
   399|    .ctx-item svg { width:14px; height:14px; stroke:currentColor; stroke-width:1.8; fill:none; stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; opacity:.7; }
   400|    .ctx-item:hover { background:var(--bg); }
   401|    .ctx-sep { height:1px; background:var(--border); margin:4px 0; }
   402|    .ctx-item.danger { color:var(--red); }
   403|
   404|    /* EMPTY */
   405|    .empty-state { text-align:center; padding:64px 24px; }
   406|    .empty-icon { font-size:48px; margin-bottom:16px; }
   407|    .empty-title { font-family:'Lora',serif; font-size:20px; font-weight:600; margin-bottom:8px; }
   408|    .empty-sub { font-size:13px; color:var(--text-3); margin-bottom:24px; }
   409|
   410|    /* ANIMATIONS */
   411|    @keyframes fade-in-up { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)} }
   412|    .deck-card:nth-child(1){animation-delay:.04s}.deck-card:nth-child(2){animation-delay:.08s}
   413|    .deck-card:nth-child(3){animation-delay:.12s}.deck-card:nth-child(n+4){animation-delay:.15s}
   414|    .card-row:nth-child(1){animation-delay:.03s}.card-row:nth-child(2){animation-delay:.06s}
   415|    .card-row:nth-child(3){animation-delay:.09s}.card-row:nth-child(n+4){animation-delay:.11s}
   416|
   417|    /* FOOTER */
   418|    .footer { max-width:780px; margin:0 auto; padding: 16px calc(24px + env(safe-area-inset-right, 0px)) calc(32px + env(safe-area-inset-bottom, 0px)) calc(24px + env(safe-area-inset-left, 0px)); display:flex; align-items:center; justify-content:space-between; }
   419|    .theme-toggle { padding:7px 12px; border-radius:99px; border:1.5px solid var(--border); background:var(--card); cursor:pointer; font-size:15px; box-shadow:var(--shadow-sm); transition:all .2s; }
   420|    .theme-toggle:hover { border-color:var(--border-strong); }
   421|
   422|    /* DECK STATS */
   423|    .ds-summary {
   424|      display: grid; grid-template-columns: repeat(4, 1fr);
   425|      gap: 12px; margin-bottom: 20px;
   426|    }
   427|    @media(max-width:600px) { .ds-summary { grid-template-columns: 1fr 1fr; } }
   428|    .ds-tile {
   429|      background: var(--card); border: 1.5px solid var(--border);
   430|      border-radius: 14px; padding: 18px 20px; box-shadow: var(--shadow-sm);
   431|    }
   432|    .ds-tile-val {
   433|      font-family: 'Lora', serif; font-size: 30px; font-weight: 700;
   434|      letter-spacing: -0.8px; line-height: 1; color: var(--text);
   435|    }
   436|    .ds-tile-label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.9px; margin-top: 6px; }
   437|    .ds-state-box {
   438|      background: var(--card); border: 1.5px solid var(--border);
   439|      border-radius: 14px; padding: 18px 20px; box-shadow: var(--shadow-sm);
   440|      margin-bottom: 16px;
   441|    }
   442|    .ds-state-label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.9px; margin-bottom: 10px; }
   443|    .ds-state-bar { display: flex; width: 100%; height: 12px; border-radius: 6px; overflow: hidden; gap: 2px; }
   444|    .ds-seg { height: 100%; transition: width .3s; }
   445|    .ds-state-legend { display: flex; gap: 14px; margin-top: 8px; flex-wrap: wrap; }
   446|    .ds-leg-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--text-3); }
   447|    .ds-leg-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
   448|    .ds-ease-box {
   449|      background: var(--card); border: 1.5px solid var(--border);
   450|      border-radius: 14px; padding: 18px 20px; box-shadow: var(--shadow-sm);
   451|      margin-bottom: 20px; display: inline-block;
   452|    }
   453|    .ds-ease-val {
   454|      font-family: 'Lora', serif; font-size: 28px; font-weight: 700;
   455|      letter-spacing: -0.6px; color: var(--accent);
   456|    }
   457|
   458|    /* ── POMODORO ───────────────────────────────── */
   459|    .pomo-wrap { display:flex; align-items:center; gap:11px; padding:0 22px; border-left:1px solid var(--border); border-right:1px solid var(--border); }
   460|    .pomo-time-block { display:flex; flex-direction:column; align-items:center; }
   461|    .pomo-display { font-family:'Lora',Georgia,serif; font-size:20px; font-weight:600; letter-spacing:1px; color:var(--text); line-height:1; min-width:56px; text-align:center; transition:color .3s ease; }
   462|    .pomo-display.is-running { color:#16A34A; }
   463|    .pomo-display.is-paused  { color:var(--text-2); }
   464|    .pomo-sub { font-size:9px; color:var(--text-3); text-transform:uppercase; letter-spacing:0.9px; margin-top:2px; }
   465|    .pomo-controls { display:flex; align-items:center; gap:6px; }
   466|    .pomo-btn { width:30px; height:30px; border-radius:50%; border:1.5px solid var(--border-strong); background:var(--card); color:var(--text); font-size:11px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all .18s ease; box-shadow:var(--shadow-sm); flex-shrink:0; }
   467|    .pomo-btn:hover:not(:disabled) { background:#22C55E; border-color:#22C55E; color:#fff; }
   468|    .pomo-btn.is-active { background:#22C55E; border-color:#22C55E; color:#fff; }
   469|    .pomo-stop-btn:hover:not(:disabled) { background:#EF4444; border-color:#EF4444; color:#fff; }
   470|    .pomo-btn:disabled { opacity:0.3; cursor:not-allowed; }
   471|    .pomo-toast { position:fixed; bottom:80px; left:50%; transform:translateX(-50%) translateY(10px); background:var(--text); color:var(--bg); padding:10px 22px; border-radius:99px; font-size:13px; font-weight:600; opacity:0; transition:all .3s ease; z-index:9999; pointer-events:none; white-space:nowrap; box-shadow:0 4px 14px rgba(0,0,0,.15); }
   472|    .pomo-toast.visible { opacity:1; transform:translateX(-50%) translateY(0); }
   473|
   474|    /* SCROLLBAR */
   475|    ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}
   476|
   477|    @media(max-width:640px){
   478|      /* Header: compact — hide pomo + stats */
   479|      .header {
   480|        height: calc(52px + env(safe-area-inset-top, 0px));
   481|        padding-top: env(safe-area-inset-top, 0px);
   482|        padding-left: calc(14px + env(safe-area-inset-left, 0px));
   483|        padding-right: calc(14px + env(safe-area-inset-right, 0px));
   484|      }
   485|      .brand-wordmark { font-size:17px; }
   486|      .pomo-wrap { display:none; }
   487|      .header-stats { display:none; }
   488|
   489|      /* App */
   490|      .app { padding:16px 12px 60px; }
   491|
   492|      /* View header */
   493|      .view-header { margin-bottom:16px; gap:8px; }
   494|      .view-title { font-size:20px; }
   495|
   496|      /* Deck grid: 1 column */
   497|      .deck-grid { grid-template-columns:1fr; gap:10px; }
   498|      .deck-card { padding:16px 14px 14px; }
   499|
   500|      /* Card list */
   501|

Use offset=501 to continue reading (showing 1-500 of 2678 lines)


Explanation:
Der Boss hat zwei Kritikpunkte:

1. **"1d" soll nicht dastehen** — Auf dem "Gemerkt!" Button steht das Intervall (z.B. "1d"). Das ist bei Priority-Karten unnötig, weil es eh klar ist dass die Karte normal durch FSRS läuft. Der Interval-Text soll weg.

2. **"Es darf nicht so verstanden werden, das noch eine karte zu lernen ist, das überschreibt nämlich den Dojo Modus"** — Das ist der kritischere Punkt. Wenn Priority-Karten IMMER in die Queue gepackt werden (auch wenn keine anderen Karten fällig sind), dann wird der Dojo-Modus nie getriggert. Der Flow ist:
   - `startLearn()` baut Queue
   - Wenn Queue leer → `showDojoConfirm()` fragt ob Dojo gestartet werden soll
   - Aber wenn Priority-Karten immer drin sind, ist die Queue NIE leer → Dojo wird nie angeboten

Der Boss will: Priority-Karten sollen bei jedem Durchgang angezeigt werden, aber sie sollen den normalen Lern-Flow nicht kapern. Sie sollen nicht als "es gibt noch was zu lernen" zählen. Sie sind Bonus/Pflicht-Karten die man schnell abhakt, aber sie blockieren nicht den Dojo-Modus.

Ich muss den ganzen Code lesen und verstehen, dann Kimi K2.7 Code delegieren mit einer präzisen Spec die beide Probleme löst.

Lass mich den kompletten Code lesen und dann die Spec schreiben.


501|      .card-row { padding:10px 12px; gap:10px; }
   502|      .card-row-front { font-size:13px; }
   503|      .card-row-back { font-size:11px; }
   504|
   505|      /* Editor */
   506|      .editor-form { padding:20px 16px; border-radius:14px; }
   507|      .form-textarea { font-size:14px; min-height:80px; }
   508|
   509|      /* ── LEARN: auto-height card, no 3D flip ── */
   510|      .learn-wrap { padding-top:0; width:100%; align-items:stretch; }
   511|      .learn-progress-bar { max-width:none; margin-bottom:14px; }
   512|      .learn-counter { margin-bottom:12px; }
   513|
   514|      /* Remove fixed height — card grows with content */
   515|      .flip-wrap {
   516|        height:auto; min-height:100px;
   517|        max-width:none; perspective:none;
   518|        margin-bottom:12px; cursor:pointer;
   519|      }
   520|      .flip-inner {
   521|        height:auto;
   522|        transform-style:flat;
   523|        transition:none;
   524|      }
   525|      .flip-inner.flipped { transform:none; }
   526|
   527|      /* Faces: normal flow instead of absolute */
   528|      .flip-face {
   529|        position:relative; inset:auto;
   530|        backface-visibility:visible; -webkit-backface-visibility:visible;
   531|        min-height:110px; padding:24px 20px;
   532|        border-radius:16px;
   533|      }
   534|      /* Back face hidden by default, shown when flipped */
   535|      .flip-face.back {
   536|        transform:none;
   537|        display:none;
   538|        position:relative;
   539|      }
   540|      .flip-inner.flipped .flip-face.back  { display:flex; }
   541|      .flip-inner.flipped .flip-face:not(.back) { display:none; }
   542|
   543|      .flip-face-text { font-size:17px; line-height:1.5; }
   544|      .flip-face-text li { font-size:16px; }
   545|      .flip-hint { position:relative; bottom:auto; margin-top:10px; font-size:10px; }
   546|
   547|      /* ── RATING BUTTONS: 2×2 grid, full card width ── */
   548|      .rating-row {
   549|        display:grid !important;
   550|        grid-template-columns:1fr 1fr;
   551|        gap:10px;
   552|        width:100%;
   553|        max-width:none;
   554|      }
   555|      .rating-btn {
   556|        padding:18px 8px; border-radius:14px;
   557|        flex:none;
   558|      }
   559|      .rating-btn .rb-label { font-size:15px; }
   560|      .rating-btn .rb-interval { font-size:11px; }
   561|
   562|      /* Done screen */
   563|      .done-title { font-size:22px; }
   564|      .done-stats { gap:10px; }
   565|      .done-stat { padding:14px 16px; }
   566|      .done-stat-num { font-size:24px; }
   567|
   568|      /* Modal */
   569|      .modal { padding:22px 18px 18px; width:calc(100vw - 32px); }
   570|
   571|      /* Footer */
   572|      .footer { padding: 10px calc(14px + env(safe-area-inset-right, 0px)) calc(20px + env(safe-area-inset-bottom, 0px)) calc(14px + env(safe-area-inset-left, 0px)); }
   573|      .theme-toggle { padding:6px 10px; font-size:11px; }
   574|    }
   575|
   576|    @media(max-width:400px){
   577|      .brand-wordmark { font-size:15px; }
   578|      .flip-face-text { font-size:15px; }
   579|      .flip-face-text li { font-size:14px; }
   580|    }
   581|
   582|    /* ── CARD RICH TEXT ── */
   583|    .flip-face-text strong { font-weight:700; }
   584|    .flip-face-text em { font-style:italic; font-weight:400; }
   585|    .flip-face-text mark { background:#FEF08A; color:#1A1917; border-radius:3px; padding:0 2px; }
   586|    html.dark .flip-face-text mark { background:#713F12; color:#FDE68A; }
   587|    .flip-face-text code {
   588|      font-family:'SF Mono',Menlo,Consolas,monospace; font-size:.78em;
   589|      background:var(--border); border-radius:4px; padding:1px 5px;
   590|    }
   591|    .flip-face-text h1 { font-size:1.25em; font-weight:700; margin:.1em 0 .2em; letter-spacing:-.3px; }
   592|    .flip-face-text h2 { font-size:1.1em;  font-weight:700; margin:.1em 0 .15em; }
   593|    .flip-face-text h3 { font-size:1em;    font-weight:700; margin:.1em 0 .1em; }
   594|    .flip-face-text hr { border:none; border-top:1px solid var(--border-strong); margin:.5em 0; }
   595|    .flip-face-text a { color:var(--accent); text-decoration:underline; }
   596|    .flip-face-text a.ris-link {
   597|      color:#1D4ED8; text-decoration:none;
   598|      border-bottom:1.5px solid #93C5FD; padding-bottom:1px; font-weight:500;
   599|    }
   600|    html.dark .flip-face-text a.ris-link { color:#93C5FD; border-bottom-color:#3B82F6; }
   601|    .flip-face.back .flip-face-text a.ris-link { color:var(--accent); border-bottom-color:var(--accent); }
   602|
   603|    /* ── KARTEN-SUCHE & FILTER ── */
   604|    .cards-filter { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:0 0 14px; }
   605|    .cards-search {
   606|      flex:1; min-width:180px; padding:8px 13px; border-radius:9px;
   607|      border:1.5px solid var(--border); background:var(--card); color:var(--text);
   608|      font-size:13px; outline:none; transition:border-color .15s;
   609|    }
   610|    .cards-search:focus { border-color:var(--accent); }
   611|    .cards-chips { display:flex; gap:6px; flex-wrap:wrap; }
   612|    .chip-btn {
   613|      padding:5px 12px; border-radius:99px; border:1.5px solid var(--border);
   614|      background:var(--card); color:var(--text-2); font-size:11.5px; font-weight:600;
   615|      cursor:pointer; transition:all .15s;
   616|    }
   617|    .chip-btn:hover:not(.active) { border-color:var(--border-strong); color:var(--text); }
   618|    .chip-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
   619|    .badge-leech { background:#FEE2E2; color:#B91C1C; }
   620|    html.dark .badge-leech { background:#7F1D1D; color:#FCA5A5; }
   621|
   622|    /* ── PRIORITY TOGGLE (Editor) ── */
   623|    .priority-toggle-wrap {
   624|      display:inline-flex; align-items:center; gap:12px; cursor:pointer;
   625|      user-select:none; -webkit-user-select:none;
   626|    }
   627|    .priority-toggle-label {
   628|      display:inline-flex; align-items:center; gap:6px;
   629|      font-size:13px; font-weight:600; color:var(--text-2);
   630|      transition:color .2s;
   631|    }
   632|    .priority-toggle-label svg { color:var(--text-3); transition:color .2s; }
   633|    .priority-toggle-input { position:absolute; opacity:0; width:0; height:0; }
   634|    .priority-toggle {
   635|      position:relative; width:50px; height:26px;
   636|      border-radius:99px; background:var(--border-strong);
   637|      transition:background .25s ease, box-shadow .25s ease;
   638|      box-shadow:inset 0 1px 3px rgba(26,25,23,.15);
   639|    }
   640|    .priority-toggle-knob {
   641|      position:absolute; top:3px; left:3px;
   642|      width:20px; height:20px; border-radius:50%;
   643|      background:var(--card);
   644|      box-shadow:0 1px 3px rgba(26,25,23,.25);
   645|      transition:transform .25s cubic-bezier(.34,1.56,.64,1);
   646|    }
   647|    .priority-toggle-input:checked + .priority-toggle {
   648|      background:linear-gradient(90deg, #A78BFA, #8B5CF6);
   649|      box-shadow:inset 0 1px 3px rgba(139,92,246,.25);
   650|    }
   651|    html.dark .priority-toggle-input:checked + .priority-toggle {
   652|      background:linear-gradient(90deg, #C4B5FD, #A78BFA);
   653|    }
   654|    .priority-toggle-input:checked + .priority-toggle .priority-toggle-knob {
   655|      transform:translateX(24px);
   656|    }
   657|    .priority-toggle-input:focus-visible + .priority-toggle {
   658|      outline:2px solid var(--accent); outline-offset:2px;
   659|      box-shadow:0 0 0 4px var(--accent-soft);
   660|    }
   661|    .priority-toggle-wrap:hover .priority-toggle-label,
   662|    .priority-toggle-input:checked ~ .priority-toggle-label { color:var(--text); }
   663|    .priority-toggle-input:checked ~ .priority-toggle-label svg { color:#F59E0B; }
   664|
   665|    /* ── RAINBOW GLOW BOKEH (Priority cards) ── */
   666|    @keyframes rainbow-flow {
   667|      0%   { background-position: 0% 50%; }
   668|      50%  { background-position: 100% 50%; }
   669|      100% { background-position: 0% 50%; }
   670|    }
   671|    @keyframes bokeh-float {
   672|      0%,100% { transform:translate(0,0) scale(1); }
   673|      33%     { transform:translate(12px,-10px) scale(1.05); }
   674|      66%     { transform:translate(-8px,8px) scale(.95); }
   675|    }
   676|    @keyframes bokeh-float-2 {
   677|      0%,100% { transform:translate(0,0) scale(1); }
   678|      50%     { transform:translate(-14px,6px) scale(1.08); }
   679|    }
   680|
   681|    /* Card row subtle priority indicator */
   682|    .card-row.is-priority {
   683|      position:relative; overflow:hidden;
   684|      border-color:rgba(139,92,246,.35);
   685|    }
   686|    .card-row.is-priority::before {
   687|      content:''; position:absolute; inset:-2px;
   688|      background:linear-gradient(120deg, rgba(139,92,246,.10), rgba(59,130,246,.10), rgba(16,185,129,.10), rgba(245,158,11,.10), rgba(139,92,246,.10));
   689|      background-size:300% 300%;
   690|      animation:rainbow-flow 10s ease infinite;
   691|      z-index:0; pointer-events:none;
   692|    }
   693|    .card-row.is-priority::after {
   694|      content:''; position:absolute;
   695|      width:70px; height:70px; right:-18px; top:-18px;
   696|      border-radius:50%;
   697|      background:radial-gradient(circle, rgba(167,139,250,.22) 0%, transparent 70%);
   698|      filter:blur(10px);
   699|      animation:bokeh-float 8s ease-in-out infinite;
   700|      z-index:0; pointer-events:none;
   701|    }
   702|    .card-row.is-priority > * { position:relative; z-index:1; }
   703|    .card-row-priority-icon {
   704|      color:#F59E0B; margin-right:3px;
   705|      text-shadow:0 0 8px rgba(245,158,11,.35);
   706|    }
   707|
   708|    /* Learn view priority glow on flip-face */
   709|    .flip-face.is-priority {
   710|      position:relative; overflow:hidden;
   711|      border-color:rgba(139,92,246,.45);
   712|    }
   713|    .flip-face.is-priority::before {
   714|      content:''; position:absolute; inset:-4px;
   715|      background:linear-gradient(135deg,
   716|        rgba(139,92,246,.14), rgba(59,130,246,.10), rgba(16,185,129,.10),
   717|        rgba(245,158,11,.10), rgba(236,72,153,.10), rgba(139,92,246,.14));
   718|      background-size:400% 400%;
   719|      animation:rainbow-flow 12s ease infinite;
   720|      z-index:0; pointer-events:none;
   721|    }
   722|    .flip-face.is-priority::after {
   723|      content:''; position:absolute;
   724|      width:160px; height:160px; right:-40px; top:-40px;
   725|      border-radius:50%;
   726|      background:radial-gradient(circle, rgba(167,139,250,.16) 0%, transparent 65%);
   727|      filter:blur(14px);
   728|      animation:bokeh-float 10s ease-in-out infinite;
   729|      z-index:0; pointer-events:none;
   730|    }
   731|    .flip-face.is-priority > * { position:relative; z-index:1; }
   732|    .flip-face.back.is-priority {
   733|      background:linear-gradient(135deg, var(--accent-light), rgba(139,92,246,.08));
   734|    }
   735|    html.dark .flip-face.back.is-priority {
   736|      background:linear-gradient(135deg, var(--accent-light), rgba(167,139,250,.10));
   737|    }
   738|    html.dark .flip-face.is-priority::before {
   739|      filter:brightness(1.15);
   740|    }
   741|
   742|    @media(max-width:640px){
   743|      .flip-face.is-priority::after { width:110px; height:110px; right:-30px; top:-30px; }
   744|    }
   745|  </style>
   746|
   747|
   748|
   749|

   750|
   751|        Zeitgeist
   752|        Anki
   753|        Notes
   754|        LL.B.
   755|
   756|

   757|

   758|

   759|
25:00

   760|
Pomodoro

   761|

   762|

   763|        <button class="pomo-btn" id="pomo-start" title="Starten">▶</button>
   764|        <button class="pomo-btn pomo-stop-btn" id="pomo-stop" title="Beenden" disabled>■</button>
   765|

   766|

   767|

   768|

   769|
0 min

   770|
Heute

   771|

   772|

   773|
0 min

   774|
Diese Woche

   775|

   776|

   777|
0 min

   778|
Gesamt

   779|

   780|

   781|
0

   782|
Serie

   783|

   784|

   785|
   786|
   787|
   788|
   789|
   790|

   791|      <button class="theme-toggle" id="theme-btn" aria-label="Hell-/Dunkelmodus umschalten">🌙</button>
   792|      <button class="theme-toggle" id="stats-btn" title="Statistik" aria-label="Statistik anzeigen">📊</button>
   793|

   794|    Ahmed Zeyd Aytac
   795|
   796|
   797|
   798|

Use offset=1001 to continue reading (showing 501-1000 of 2678 lines)


Explanation: Let me continue reading the rest of the file to understand the full code, especially the priority-related parts and the learn/dojo flow.


1001|        } else if (rating === 2) {
  1002|          /* Hard → aktueller Relearn-Step × 1.5 */
  1003|          c.learningStep = cur;
  1004|          c.dueDate = now + Math.round(stepM * this.HARD_MULTIPLIER * 60000);
  1005|        } else {
  1006|          /* Good / Easy → nächster Schritt oder zurück zu Review */
  1007|          const next = cur + 1;
  1008|          if (next >= this.STEPS_RELEARN.length) {
  1009|            c.state      = 'review';
  1010|            c.lastReview = now;
  1011|            c.interval   = this.interval(c.stability || 1, fuzz);
  1012|            c.dueDate    = now + c.interval * 86400000;
  1013|          } else {
  1014|            c.learningStep = next;
  1015|            c.dueDate = now + this.STEPS_RELEARN[next] * 60000;
  1016|          }
  1017|        }
  1018|        return c;
  1019|      }
  1020|
  1021|      return c;
  1022|    },
  1023|  };
  1024|
  1025|  /* legacy alias — keeps any straggler call sites working */
  1026|  const SM2 = FSRS;
  1027|
  1028|  /* ═══════════════════════════════════════
  1029|     STORAGE & HELPERS
  1030|     ═══════════════════════════════════════ */
  1031|  const STORE      = 'zeitgeist-anki-v2';
  1032|  const ABBR_STORE = 'vault-abbr';
  1033|  const COLORS = ['#8B5CF6','#3B82F6','#10B981','#F59E0B','#EF4444','#EC4899','#14B8A6','#6366F1'];
  1034|
  1035|  /* ── ABBREVIATIONS (shared with Vault) ────────────── */
  1036|  let _abbrs = [];
  1037|  const abbrLoad = data => {
  1038|    if (data && data[ABBR_STORE]) {
  1039|      try { _abbrs = JSON.parse(data[ABBR_STORE]); } catch { _abbrs = []; }
  1040|    } else {
  1041|      try { _abbrs = JSON.parse(localStorage.getItem(ABBR_STORE) || '[]'); } catch { _abbrs = []; }
  1042|    }
  1043|  };
  1044|  const applyAbbrs = s => {
  1045|    _abbrs.forEach(a => {
  1046|      if (!a.short || !a.long) return;
  1047|      const esc_short = a.short.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1048|      s = s.replace(new RegExp('\\b' + esc_short + '\\b', 'g'), a.long);
  1049|    });
  1050|    return s;
  1051|  };
  1052|
  1053|  const load  = () => { try { return JSON.parse(localStorage.getItem(STORE)||'{"decks":{}}'); } catch { return {decks:{}}; } };
  1054|  const save  = d => ZG.save(STORE, JSON.stringify(d));
  1055|  const uid   = () => Date.now().toString(36) + Math.random().toString(36).slice(2,6);
  1056|  const esc   = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
  1057|
  1058|  /* ── RIS AUTO-LINK ────────────────────────────────── */
  1059|  /* § / §§ — Paragraf-based laws */
  1060|  const RIS_LAWS = {
  1061|    'EStG':10004570,'KStG':10004569,'UStG':10004873,'BAO':10003940,
  1062|    'ABGB':10001622,'GmbHG':10001720,'AktG':10002070,'UGB':10001702,'HGB':10001702,
  1063|    'GewO':10007517,'ASVG':10008147,'FinStrG':10003898,'FLAG':10008220,
  1064|    'GebG':10003999,'GrEStG':10004531,'KommStG':10004841,'ErbStG':10003850,
  1065|    'StGB':10002296,'ZPO':10001699,'WTBG':20009983,'BiBuG':20008571,
  1066|    'VerG':20001917,'IO':10001736,'KO':10001736,'AO':10001736,
  1067|    'MRG':10002531,'WEG':20001921,'AVG':10005735,'VwGVG':20007887,
  1068|    'DSG':10001597,'ECG':10007666,'TKG':20007904,'KSchG':10002462,
  1069|    'ArbVG':10008374,'AngG':10008414,'AVRAG':10008775,'BMSVG':20003657,
  1070|    'BörseG':20004329,'WAG':20006130,'BWG':20009885,'VersVG':10009994,
  1071|    'KartG':20002697,'WettbG':20009623,'UrhG':10001848,'PatG':10002664,
  1072|    'MSchG':10002526,'VStG':10005770,'JN':10001697,'PHG':10007990,
  1073|    'EKHG':10009894,'IPRG':10002247,'BVergG':20007517,'StAG':10005400,
  1074|    'WRG':10010290,'ForstG':10010056,'VwGG':10000990,
  1075|  };
  1076|  /* Art. — Artikel-based laws (B-VG, Verfassungsgesetze, etc.) */
  1077|  const RIS_ART_LAWS = {
  1078|    'B-VG':10000138,'StGG':10000006,'EMRK':10000308,
  1079|    'VfGG':10000929,'VwGG':10000990,'MRK':10000308,
  1080|    'MRKZP1':10001099,'MRKZP4':10001100,'MRKZP7':10001101,
  1081|  };
  1082|  const RIS_BASE = 'https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen';
  1083|
  1084|  let _RIS_RE     = null;
  1085|  let _RIS_ART_RE = null;
  1086|
  1087|  const _risDate = () => {
  1088|    const d = new Date();
  1089|    return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');
  1090|  };
  1091|
  1092|  const _buildRisRe = () => {
  1093|    const alt = Object.keys(RIS_LAWS).map(k => k.replace(/[-]/g,'\\-')).join('|');
  1094|    _RIS_RE = new RegExp(
  1095|      '(§§\\s*\\d+[a-z]{0,2}(?:\\s*,\\s*\\d+[a-z]{0,2})*\\s+' +
  1096|      '|§+\\s*\\d+[a-z]{0,2}\\s[^§\\n]*?)(' + alt + ')(?![A-Za-z\\u00C0-\\u024F0-9])', 'g'
  1097|    );
  1098|    const artAlt = Object.keys(RIS_ART_LAWS).map(k => k.replace(/[-]/g,'\\-')).join('|');
  1099|    _RIS_ART_RE = new RegExp(
  1100|      '(Art\\.?\\s*\\d+[a-z]{0,2}(?:\\s+Abs\\.?\\s*\\d+[a-z]{0,2})?)\\s+(' + artAlt + ')(?![A-Za-z\\u00C0-\\u024F0-9])', 'g'
  1101|    );
  1102|  };
  1103|
  1104|  const applyRisLinks = s => {
  1105|    if (!_RIS_RE) _buildRisRe();
  1106|    const ds = _risDate();
  1107|    /* § / §§ links */
  1108|    s = s.replace(_RIS_RE, (match, prefix, law) => {
  1109|      const gn = RIS_LAWS[law]; if (!gn) return match;
  1110|      if (prefix.charAt(1) === '§') {
  1111|        const numsRaw = prefix.replace(/^§§\s*/,'').replace(/\s+$/,'');
  1112|        return numsRaw.split(/\s*,\s*/).map(n => {
  1113|          const m = n.trim().match(/^(\d+)([a-z]{1,2})?/);
  1114|          const pn = m ? m[1] : '', ps = (m&&m[2]&&!/^ff?$/.test(m[2])) ? m[2] : '';
  1115|          return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=&Paragraf=${pn+ps}&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">§ ${n.trim()} ${law}</a>`;
  1116|        }).join(', ');
  1117|      } else {
  1118|        const ms = prefix.match(/(\d+)([a-z]{1,2})?/);
  1119|        const sn = ms ? ms[1] : '', ss = (ms&&ms[2]&&!/^ff?$/.test(ms[2])) ? ms[2] : '';
  1120|        return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=&Paragraf=${sn+ss}&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">${match}</a>`;
  1121|      }
  1122|    });
  1123|    /* Art. links */
  1124|    s = s.replace(_RIS_ART_RE, (match, artPart, law) => {
  1125|      const gn = RIS_ART_LAWS[law]; if (!gn) return match;
  1126|      const nm = artPart.match(/(\d+)([a-z]{0,2})?/);
  1127|      const an = nm ? nm[1] + (nm[2]&&!/^ff?$/.test(nm[2])?nm[2]:'') : '';
  1128|      return `<a href="${RIS_BASE}&Gesetzesnummer=${gn}&FassungVom=${ds}&Artikel=${an}&Paragraf=&Anlage=&Uebergangsrecht=" target="_blank" rel="noopener" class="ris-link">${match}</a>`;
  1129|    });
  1130|    return s;
  1131|  };
  1132|
  1133|  /* ── INLINE FORMATTING ─────────────────────────────── */
  1134|  const inlineFmt = raw => {
  1135|    const codes = [];
  1136|    /* protect inline code first */
  1137|    let s = raw.replace(/`([^`\n]+)`/g, (_, c) => { codes.push(''+esc(c)+''); return '\x00c'+(codes.length-1)+'\x00'; });
  1138|    /* escape html outside code spans */
  1139|    const parts = s.split(/(\x00c\d+\x00)/);
  1140|    s = parts.map(p => /^\x00c\d+\x00$/.test(p) ? p : esc(p)).join('');
  1141|    /* images first — protect them from link regex */
  1142|    const imgs = [];
  1143|    s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_, alt, src) => {
  1144|      imgs.push(`<img src="${src.replace(/"/g,'"')}" alt="${alt.replace(/"/g,'"')}" loading="lazy">`);
  1145|      return '\x00i'+(imgs.length-1)+'\x00';
  1146|    });
  1147|    /* inline styles */
  1148|    s = s.replace(/==(.+?)==/g, '$1');
  1149|    s = s.replace(/\*\*(.+?)\*\*/g, '$1');
  1150|    s = s.replace(/\*(.+?)\*/g, '$1');
  1151|    s = s.replace(/\[(.+?)\]\((.+?)\)/g, '$1');
  1152|    s = s.replace(/\x00i(\d+)\x00/g, (_, i) => imgs[+i]);
  1153|    /* legal auto-links */
  1154|    s = applyRisLinks(s);
  1155|    /* restore code spans */
  1156|    s = s.replace(/\x00c(\d+)\x00/g, (_, i) => codes[+i]);
  1157|    return s;
  1158|  };
  1159|
  1160|  /* ── RICH TEXT RENDER ──────────────────────────────── */
  1161|  const ROMAN = ['i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii','xiii','xiv','xv'];
  1162|
  1163|  const renderText = raw => {
  1164|    if (_abbrs.length) raw = applyAbbrs(String(raw || ''));
  1165|    const lines = String(raw || '').split('\n');
  1166|    const out = [];
  1167|    let list = null; // null | 'ul' | 'ol' | 'ol-a' | 'ol-i'
  1168|    let prevText = false; // last emitted line was a plain text paragraph
  1169|    const closeList = () => { if (list) { out.push(list === 'ul' ? '
' : '
'); list = null; } };
  1170|
  1171|    for (const line of lines) {
  1172|      const t    = line.trim();
  1173|      const pad  = line.length - line.trimStart().length;
  1174|      const ml   = pad > 0 ? ` style="margin-left:${pad * 8}px"` : '';
  1175|
  1176|      /* headings */
  1177|      const hm = t.match(/^(#{1,3})\s+(.*)/);
  1178|      if (hm) { closeList(); out.push(`<h${hm[1].length}>${inlineFmt(hm[2])}</h${hm[1].length}>`); prevText = false; continue; }
  1179|
  1180|      /* horizontal rule */
  1181|      if (/^-{3,}$/.test(t) || /^\*{3,}$/.test(t)) { closeList(); out.push('
'); prevText = false; continue; }
  1182|
  1183|      /* bullet list — accept •, - or * as markers */
  1184|      const bm = t.match(/^([•*-])\s+(.*)/);
  1185|      if (bm) {
  1186|        if (list !== 'ul') { closeList(); out.push('
'); list = 'ul'; }
  1187|        out.push(`<li${ml}>${inlineFmt(bm[2])}</li>`);
  1188|        prevText = false;
  1189|      } else if (/^\d+\. /.test(t)) {
  1190|        if (list !== 'ol') { closeList(); out.push('
'); list = 'ol'; }
  1191|        out.push(`<li${ml}>${inlineFmt(t.replace(/^\d+\. /,''))}</li>`);
  1192|        prevText = false;
  1193|      } else if (/^(i{1,3}|iv|vi{0,3}|ix)\. /i.test(t)) {
  1194|        if (list !== 'ol-i') { closeList(); out.push('
'); list = 'ol-i'; }
  1195|        out.push(`<li${ml}>${inlineFmt(t.replace(/^[ivx]+\. /i,''))}</li>`);
  1196|        prevText = false;
  1197|      } else if (/^[a-z]\. /.test(t)) {
  1198|        if (list !== 'ol-a') { closeList(); out.push('
'); list = 'ol-a'; }
  1199|        out.push(`<li${ml}>${inlineFmt(t.replace(/^[a-z]\. /,''))}</li>`);
  1200|        prevText = false;
  1201|      } else {
  1202|        closeList();
  1203|        if (t === '') { out.push('
'); prevText = false; }
  1204|        else {
  1205|          // keep consecutive paragraphs on their own lines, like the editor
  1206|          if (prevText) out.push('
');
  1207|          out.push(inlineFmt(t));
  1208|          prevText = true;
  1209|        }
  1210|      }
  1211|    }
  1212|    closeList();
  1213|    return out.join('');
  1214|  };
  1215|
  1216|  /* ── SMART EDITOR ──────────────────────────────────── */
  1217|  const setupSmartEditor = ta => {
  1218|    ta.addEventListener('keydown', e => {
  1219|      const val  = ta.value;
  1220|      const pos  = ta.selectionStart;
  1221|      const ls   = val.lastIndexOf('\n', pos - 1) + 1; // line start
  1222|      const le   = val.indexOf('\n', pos);              // line end
  1223|      const full = val.substring(ls, le === -1 ? val.length : le); // full current line
  1224|      const pre  = val.substring(ls, pos);              // prefix before cursor
  1225|
  1226|      /* ── Tab / Shift+Tab ─────── */
  1227|      if (e.key === 'Tab') {
  1228|        e.preventDefault();
  1229|        if (e.shiftKey) {
  1230|          // Unindent: remove up to 2 leading spaces from line start
  1231|          const sp = full.match(/^( {1,2})/);
  1232|          if (sp) {
  1233|            ta.setSelectionRange(ls, ls + sp[1].length);
  1234|            document.execCommand('insertText', false, '');
  1235|          }
  1236|        } else {
  1237|          const isListLine = /^(\s*)(• |\d+\. |[a-z]\. |[ivx]+\. )/i.test(full);
  1238|          if (isListLine) {
  1239|            // Indent: insert 2 spaces at start of line so "• item" → "  • item"
  1240|            const cur = ta.selectionStart;
  1241|            ta.setSelectionRange(ls, ls);
  1242|            document.execCommand('insertText', false, '  ');
  1243|            ta.setSelectionRange(cur + 2, cur + 2);
  1244|          } else {
  1245|            document.execCommand('insertText', false, '  ');
  1246|          }
  1247|        }
  1248|        return;
  1249|      }
  1250|
  1251|      /* ── Space / Enter: expand abbreviation if cursor follows one ─── */
  1252|      if (e.key === ' ' || e.key === 'Enter') {
  1253|        if (_abbrs.length) {
  1254|          for (const _a of _abbrs) {
  1255|            if (_a.short && _a.long && pre.endsWith(_a.short)) {
  1256|              const start = pos - _a.short.length;
  1257|              ta.setSelectionRange(start, pos);
  1258|              document.execCommand('insertText', false, _a.long);
  1259|              break;
  1260|            }
  1261|          }
  1262|        }
  1263|      }
  1264|
  1265|      /* ── Space: trigger list markers ─── */
  1266|      if (e.key === ' ') {
  1267|        const indent = pre.match(/^(\s*)/)[1];
  1268|        const token  = pre.trimStart();
  1269|        if (token === '-') {
  1270|          e.preventDefault();
  1271|          ta.setSelectionRange(ls + indent.length, pos);
  1272|          document.execCommand('insertText', false, '• ');
  1273|          return;
  1274|        }
  1275|        // 1. / a. / i. — just let space be inserted normally, list is triggered by content
  1276|      }
  1277|
  1278|      /* ── Enter: continue list ─── */
  1279|      if (e.key === 'Enter') {
  1280|        const indent = full.match(/^(\s*)/)[1];
  1281|
  1282|        const bm = full.match(/^(\s*)• (.*)$/);
  1283|        if (bm) {
  1284|          e.preventDefault();
  1285|          if (!bm[2].trim()) { ta.setSelectionRange(ls, ls + indent.length + 2); document.execCommand('insertText', false, ''); }
  1286|          else document.execCommand('insertText', false, '\n' + indent + '• ');
  1287|          return;
  1288|        }
  1289|
  1290|        const nm = full.match(/^(\s*)(\d+)\. (.*)$/);
  1291|        if (nm) {
  1292|          e.preventDefault();
  1293|          if (!nm[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + nm[2].length + 2); document.execCommand('insertText', false, ''); }
  1294|          else document.execCommand('insertText', false, '\n' + indent + (parseInt(nm[2]) + 1) + '. ');
  1295|          return;
  1296|        }
  1297|
  1298|        const rm = full.match(/^(\s*)(i{1,3}|iv|vi{0,3}|ix)\. (.*)$/i);
  1299|        if (rm) {
  1300|          e.preventDefault();
  1301|          const cur = rm[2].toLowerCase(), ri = ROMAN.indexOf(cur);
  1302|          if (!rm[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + cur.length + 2); document.execCommand('insertText', false, ''); }
  1303|          else document.execCommand('insertText', false, '\n' + indent + (ri !== -1 && ri < ROMAN.length - 1 ? ROMAN[ri + 1] : cur + 'i') + '. ');
  1304|          return;
  1305|        }
  1306|
  1307|        const am = full.match(/^(\s*)([a-z])\. (.*)$/);
  1308|        if (am) {
  1309|          e.preventDefault();
  1310|          if (!am[3].trim()) { ta.setSelectionRange(ls, ls + indent.length + 3); document.execCommand('insertText', false, ''); }
  1311|          else document.execCommand('insertText', false, '\n' + indent + String.fromCharCode(am[2].charCodeAt(0) + 1) + '. ');
  1312|          return;
  1313|        }
  1314|      }
  1315|    });
  1316|  };
  1317|  const strip = h => { const d=document.createElement('div'); d.innerHTML=h; return d.textContent||''; };
  1318|
  1319|  const getDeck  = id => load().decks[id];
  1320|  const getCards = id => (getDeck(id)||{}).cards||[];
  1321|  const colorOf  = d  => d.color||COLORS[0];
  1322|
  1323|  const countsDue = (cards) => {
  1324|    const now = Date.now();
  1325|    return {
  1326|      due:   cards.filter(c => c.state!=='new' && c.dueDate && c.dueDate<=now).length,
  1327|      newN:  cards.filter(c => c.state==='new'||!c.state).length,
  1328|    };
  1329|  };
  1330|
  1331|    const newCard = (front, back) => ({
  1332|    id: uid(), front, back, created: Date.now(),
  1333|    state: 'new', interval: 0,
  1334|    stability: null, difficulty: null, lastReview: null,
  1335|    dueDate: null, lapses: 0, reviews: 0, learningStep: 0,
  1336|    /* legacy field, kept for backward read compat — unused by FSRS */
  1337|    easeFactor: 2.5,
  1338|    priority: false,
  1339|  });
  1340|
  1341|  /* ═══════════════════════════════════════
  1342|     STATE
  1343|     ═══════════════════════════════════════ */
  1344|  let VIEW   = 'decks';
  1345|  let deckId = null;
  1346|  let cardId = null;
  1347|  let learn  = {};
  1348|  let activeCtx = null;
  1349|  let _editFromLearn = false;
  1350|
  1351|  /* Suche/Filter der Kartenliste (pro Deck zurückgesetzt) */
  1352|  let cardQuery  = '';
  1353|  let cardFilter = 'all';
  1354|
  1355|  const go = (v, p={}) => {
  1356|    VIEW=v;
  1357|    if(p.deckId!==undefined) {
  1358|      if (p.deckId !== deckId) { cardQuery = ''; cardFilter = 'all'; }
  1359|      deckId=p.deckId;
  1360|    }
  1361|    if(p.cardId!==undefined) cardId=p.cardId;
  1362|    if(p.learn !==undefined) learn =p.learn;
  1363|    render();
  1364|  };
  1365|
  1366|  /* ═══════════════════════════════════════
  1367|     ICONS
  1368|     ═══════════════════════════════════════ */
  1369|  const IC = {
  1370|    plus:   `<svg viewBox="0 0 16 16"><line x1="8" y1="3" x2="8" y2="13"/><line x1="3" y1="8" x2="13" y2="8"/></svg>`,
  1371|    edit:   `<svg viewBox="0 0 16 16"><path d="M11 2l3 3-9 9H2v-3z"/></svg>`,
  1372|    trash:  `<svg viewBox="0 0 16 16"><polyline points="3,5 13,5"/><path d="M6,5V3h4v2M5,5v8a1,1,0,0,0,1,1h4a1,1,0,0,0,1-1V5"/></svg>`,
  1373|    play:   `<svg viewBox="0 0 16 16"><polygon points="4,2 14,8 4,14"/></svg>`,
  1374|    rotate: `<svg viewBox="0 0 16 16"><path d="M2 8a6 6 0 1 0 1.5-4"/><polyline points="2,2 2,6 6,6"/></svg>`,
  1375|    import: `<svg viewBox="0 0 16 16"><path d="M8 2v9"/><polyline points="4,7 8,11 12,7"/><rect x="2" y="12" width="12" height="2" rx="1"/></svg>`,
  1376|    open:   `<svg viewBox="0 0 16 16"><path d="M2 5h4l1.5 1.5H14V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/></svg>`,
  1377|    stats:  `<svg viewBox="0 0 16 16"><line x1="3" y1="13" x2="3" y2="9"/><line x1="8" y1="13" x2="8" y2="5"/><line x1="13" y1="13" x2="13" y2="7"/></svg>`,
  1378|  };
  1379|
  1380|  /* ═══════════════════════════════════════
  1381|     RENDER
  1382|     ═══════════════════════════════════════ */
  1383|  const render = () => {
  1384|    updateHeader();
  1385|    const app = document.getElementById('app');
  1386|    if      (VIEW==='decks')       app.innerHTML = vDecks();
  1387|    else if (VIEW==='cards')       app.innerHTML = vCards();
  1388|    else if (VIEW==='learn')       app.innerHTML = vLearn();
  1389|    else if (VIEW==='editor')      app.innerHTML = vEditor();
  1390|    else if (VIEW==='done')        app.innerHTML = vDone();
  1391|    else if (VIEW==='deck-stats')  app.innerHTML = vDeckStats();
  1392|    bindAll();
  1393|  };
  1394|
  1395|  const updateHeader = () => {};
  1396|
  1397|  /* ── VIEW: DECKS ───────────────────────── */
  1398|  const vDecks = () => {
  1399|    const decks = Object.values(load().decks).sort((a,b)=>a.created-b.created);
  1400|    if (!decks.length) return `
  1401|

  1402|
📚

  1403|
Noch keine Decks

  1404|
Erstelle dein erstes Deck oder importiere ein Anki-Deck.

  1405|

  1406|          <button class="btn btn-primary btn-lg" id="add-deck-btn">${IC.plus} Neues Deck</button>
  1407|          <button class="btn btn-ghost btn-lg" id="import-btn">${IC.import} Importieren</button>
  1408|

  1409|
`;
  1410|
  1411|    const cards = decks.map(d => {
  1412|      const c=colorOf(d); const {due,newN}=countsDue(d.cards||[]);
  1413|      return `
  1414|

  1415|

  1416|
${esc(d.name)}

  1417|

  1418|            ${due  ? `<span class="deck-count"><span class="dot" style="background:#EF4444"></span>${due} fällig</span>` : ''}
  1419|            ${newN ? `<span class="deck-count"><span class="dot" style="background:${c}"></span>${newN} neu</span>` : ''}
  1420|            ${!due&&!newN ? `<span style="color:var(--text-3)">${(d.cards||[]).length} Karten</span>` : ''}
  1421|

  1422|

  1423|            <button class="deck-learn-btn" data-learn="${d.id}" style="background:${c}" ${!(d.cards||[]).length?'disabled':''}>Lernen</button>
  1424|            <button class="deck-menu-btn" data-menu="${d.id}">···</button>
  1425|

  1426|
`;
  1427|    }).join('');
  1428|
  1429|    return `
  1430|

  1431|
Meine Decks

  1432|

  1433|          <button class="btn btn-ghost" id="import-btn">${IC.import} Importieren</button>
  1434|          <button class="btn btn-primary" id="add-deck-btn">${IC.plus} Neues Deck</button>
  1435|

  1436|

  1437|

  1438|        ${cards}
  1439|
${IC.plus} Deck hinzufügen

  1440|
`;
  1441|  };
  1442|
  1443|  /* ── VIEW: DECK STATS ─────────────────── */
  1444|  const vDeckStats = () => {
  1445|    const deck = getDeck(deckId); if (!deck) return '';
  1446|    const cards = deck.cards || [];
  1447|    const c = colorOf(deck);
  1448|    const total = cards.length;
  1449|    const newN = cards.filter(c => !c.state || c.state === 'new').length;
  1450|    const learning = cards.filter(c => c.state === 'learning' || c.state === 'relearn').length;
  1451|    const matured = cards.filter(c => c.state === 'review' && (c.interval || 0) > 21).length;
  1452|
  1453|    // State distribution
  1454|    const reviewing = cards.filter(c => c.state === 'review').length;
  1455|    const relearn = cards.filter(c => c.state === 'relearn').length;
  1456|    const tot = total || 1;
  1457|    const pNew     = Math.round((newN / tot) * 100);
  1458|    const pLearn   = Math.round((learning / tot) * 100);
  1459|    const pReview  = Math.round((reviewing / tot) * 100);
  1460|    const pRelearn = Math.round((relearn / tot) * 100);
  1461|
  1462|    // FSRS stats: average stability (days) + average difficulty (1-10) + leeches
  1463|    const reviewCards = cards.filter(c => c.state === 'review' || c.state === 'relearn');
  1464|    const _stabs = reviewCards.map(c => typeof c.stability === 'number' ? c.stability : (c.interval || 0)).filter(x => x > 0);
  1465|    const _diffs = reviewCards.map(c => typeof c.difficulty === 'number' ? c.difficulty : null).filter(x => x !== null);
  1466|    const avgStab = _stabs.length ? (_stabs.reduce((a,b)=>a+b,0)/_stabs.length) : 0;
  1467|    const avgDiff = _diffs.length ? (_diffs.reduce((a,b)=>a+b,0)/_diffs.length) : 0;
  1468|    const leeches = cards.filter(c => c.leech || (c.lapses || 0) >= FSRS.LEECH_AT).length;
  1469|    const avgStabFmt = avgStab >= 30 ? (avgStab/30).toFixed(1)+'mo' : avgStab.toFixed(1)+'d';
  1470|
  1471|    const stateBar = total ? `
  1472|

  1473|        ${pNew     ? `<div class="ds-seg" style="width:${pNew}%;background:var(--accent)"></div>` : ''}
  1474|        ${pLearn   ? `<div class="ds-seg" style="width:${pLearn}%;background:var(--orange)"></div>` : ''}
  1475|        ${pReview  ? `<div class="ds-seg" style="width:${pReview}%;background:var(--green)"></div>` : ''}
  1476|        ${pRelearn ? `<div class="ds-seg" style="width:${pRelearn}%;background:var(--red)"></div>` : ''}
  1477|

  1478|

  1479|

Neu (${newN})

  1480|

Lernen (${learning})

  1481|

Wiederholen (${reviewing})

  1482|

Relearn (${relearn})

  1483|
` : '
Noch keine Karten
';
  1484|
  1485|    return `
  1486|

  1487|

  1488|
${esc(deck.name)} · Statistik

  1489|

  1490|

  1491|          <button class="btn btn-primary" id="start-learn-btn" style="background:${c}" ${!cards.length?'disabled':''}>
  1492|            ${IC.play} Lernen starten
  1493|          </button>
  1494|

  1495|

  1496|

  1497|

  1498|
${total}

  1499|
Karten gesamt

  1500|

  1501|

Use offset=1501 to continue reading (showing 1001-1500 of 2678 lines)


Explanation: Let me continue reading the rest of the file to understand the full code, especially the learn view, priority handling, and the Dojo mode.


1501|

  1502|
${newN}

  1503|
Neu

  1504|

  1505|

  1506|
${learning}

  1507|
In Lernphase

  1508|

  1509|

  1510|
${matured}

  1511|
Gereift

  1512|

  1513|

  1514|

  1515|
Zustand

  1516|        ${stateBar}
  1517|

  1518|

  1519|

  1520|
Ø Stabilität (Gedächtnis)

  1521|
${reviewCards.length ? avgStabFmt : '—'}

  1522|

  1523|

  1524|
Ø Schwierigkeit

  1525|
${reviewCards.length ? avgDiff.toFixed(1)+' / 10' : '—'}

  1526|

  1527|

  1528|
Leeches

  1529|
0?'color:var(--red)':''}">${leeches}

  1530|

  1531|
`;
  1532|  };
  1533|
  1534|  /* ── VIEW: CARDS ───────────────────────── */
  1535|  const vCards = () => {
  1536|    const deck  = getDeck(deckId); if(!deck) return '';
  1537|    const cards = deck.cards||[];
  1538|    const c     = colorOf(deck);
  1539|    const {due,newN} = countsDue(cards);
  1540|
  1541|    const badgeHtml = (card) => {
  1542|      const s = card.state;
  1543|      if(!s||s==='new') return `<span class="card-row-badge badge-new">Neu</span>`;
  1544|      if(s==='learning') return `<span class="card-row-badge badge-learning">Lernen</span>`;
  1545|      if(s==='review')   return `<span class="card-row-badge badge-review">Fällig: ${fmtDate(card.dueDate)}</span>`;
  1546|      if(s==='relearn')  return `<span class="card-row-badge badge-relearn">Wiederholen</span>`;
  1547|      return '';
  1548|    };
  1549|
  1550|    const stripMd = s => {
  1551|      const lines = String(s||'')
  1552|        .replace(/!\[[^\]]*\]\([^)]*\)/g,'🖼 Bild')
  1553|        .split('\n')
  1554|        .filter(l => l.trim());
  1555|      return (lines[0]||'')
  1556|        .replace(/^[\s•]*|^\s*[\d]+\.\s*|^\s*[a-z]\.\s*|^\s*[ivx]+\.\s*/i,'')
  1557|        .replace(/`([^`\n]+)`/g,'$1')
  1558|        .replace(/==(.+?)==/g,'$1')
  1559|        .replace(/\*\*(.+?)\*\*/g,'$1')
  1560|        .replace(/\*(.+?)\*/g,'$1')
  1561|        .replace(/\[(.+?)\]\((.+?)\)/g,'$1')
  1562|        .replace(/^#{1,3}\s+/,'');
  1563|    };
  1564|
  1565|    const _now = Date.now();
  1566|    const rowCats = card => {
  1567|      const cats = [];
  1568|      if (!card.state || card.state === 'new') cats.push('new');
  1569|      if (card.state === 'learning' || card.state === 'relearn') cats.push('learning');
  1570|      if (card.state && card.state !== 'new' && card.dueDate && card.dueDate <= _now) cats.push('due');
  1571|      if (card.leech || (card.lapses || 0) >= FSRS.LEECH_AT) cats.push('leech');
  1572|      return cats.join(' ');
  1573|    };
  1574|    const leechBadge = card =>
  1575|      (card.leech || (card.lapses || 0) >= FSRS.LEECH_AT)
  1576|        ? 'Leech' : '';
  1577|
  1578|    const list = cards.length ? cards.map((card,i) => `
  1579|

  1581|
${i+1}

  1582|

  1583|
${card.priority ? '★ ' : ''}${esc(stripMd(card.front))}

  1584|
${esc(stripMd(card.back))}

  1585|

  1586|        ${leechBadge(card)}
  1587|        ${badgeHtml(card)}
  1588|

  1589|          <button class="icon-btn" data-edit="${card.id}" title="Bearbeiten" aria-label="Karte bearbeiten">${IC.edit}</button>
  1590|          <button class="icon-btn danger" data-del="${card.id}" title="Löschen" aria-label="Karte löschen">${IC.trash}</button>
  1591|

  1592|
`).join('') : `
  1593|

  1594|
🃏

  1595|
Noch keine Karten

  1596|
Füge deine erste Karte hinzu.

  1597|
`;
  1598|
  1599|    const info = [due&&`${due} fällig`,newN&&`${newN} neu`].filter(Boolean).join(' · ');
  1600|
  1601|    const FILTERS = [['all','Alle'],['new','Neu'],['learning','Lernen'],['due','Fällig'],['leech','Leech']];
  1602|    const chips = FILTERS.map(([id,label]) =>
  1603|      `<button class="chip-btn${cardFilter===id?' active':''}" data-cfilter="${id}">${label}</button>`
  1604|    ).join('');
  1605|
  1606|    const filterBar = cards.length ? `
  1607|

  1608|        <input type="search" class="cards-search" id="card-search" placeholder="Karten durchsuchen…"
  1609|               value="${esc(cardQuery)}" aria-label="Karten durchsuchen">
  1610|
${chips}

  1611|

  1612|
Keine Treffer.
` : '';
  1613|
  1614|    return `
  1615|

  1616|

  1617|
${esc(deck.name)}

  1618|          ${info?`<div style="font-size:12px;color:var(--text-3);margin-top:4px">${info}</div>`:''}
  1619|

  1620|

  1621|          <button class="btn btn-ghost" id="deck-stats-btn" title="Statistik" aria-label="Deck-Statistik">${IC.stats}</button>
  1622|          <button class="btn btn-ghost" id="add-card-btn">${IC.plus} Karte</button>
  1623|          <button class="btn btn-primary" id="learn-btn" style="background:${c}" ${!cards.length?'disabled':''}>
  1624|            ${IC.play} Lernen
  1625|          </button>
  1626|

  1627|

  1628|      ${filterBar}
  1629|
${list}
`;
  1630|  };
  1631|
  1632|  const fmtDate = ts => {
  1633|    if(!ts) return '';
  1634|    const d=new Date(ts);
  1635|    const y = d.getFullYear() !== new Date().getFullYear() ? `.${String(d.getFullYear()).slice(2)}` : '';
  1636|    return `${d.getDate()}.${d.getMonth()+1}${y}`;
  1637|  };
  1638|
  1639|  /* ── VIEW: EDITOR ──────────────────────── */
  1640|  const IMG_ICON = `<svg viewBox="0 0 16 16"><rect x="2" y="3" width="12" height="10" rx="1.5"/><circle cx="6" cy="7" r="1.3"/><path d="M2 11l3.5-3 3 2.5L11 8l3 3"/></svg>`;
  1641|  const STAR_ICON = `<svg viewBox="0 0 16 16" width="13" height="13" stroke="currentColor" stroke-width="1.8" fill="currentColor" stroke-linecap="round" stroke-linejoin="round"><polygon points="8,1 10,5.5 15,6.2 11.5,9.8 12.5,15 8,12.5 3.5,15 4.5,9.8 1,6.2 6,5.5"/></svg>`;
  1642|
  1643|  const priorityToggleHtml = (checked) => `
  1644|
  1645|      ${STAR_ICON} Priority
  1646|      <input type="checkbox" class="priority-toggle-input" id="f-priority" ${checked ? 'checked' : ''}>
  1647|
  1648|    `;
  1649|
  1650|  const vEditor = () => {
  1651|    const card = cardId ? (getDeck(deckId)?.cards||[]).find(c=>c.id===cardId) : null;
  1652|    const toolbar = side => `
  1653|

  1654|        <button class="toolbar-btn" data-img-side="${side}">${IMG_ICON} Bild einfügen</button>
  1655|        Einfügen, Drag&Drop oder Klick
  1656|
`;
  1657|    return `
  1658|

  1659|
${cardId?'Karte bearbeiten':'Karte hinzufügen'}

  1660|

  1661|

  1662|

  1663|          Vorderseite · Frage
  1664|          ${toolbar('front')}
  1665|          <textarea class="form-textarea" id="f-front" placeholder="Frage oder Begriff…">${card?esc(card.front):''}</textarea>
  1666|

  1667|

  1668|          Rückseite · Antwort
  1669|          ${toolbar('back')}
  1670|          <textarea class="form-textarea" id="f-back" placeholder="Antwort oder Erklärung…">${card?esc(card.back):''}</textarea>
  1671|

  1672|

  1673|          ${priorityToggleHtml(card?.priority)}
  1674|

  1675|        <input type="file" id="f-img-input" accept="image/*" style="display:none">
  1676|

  1677|          <button class="btn btn-ghost" id="cancel-btn">Abbrechen</button>
  1678|          ${cardId?'':'<button class="btn btn-secondary btn-lg" id="save-new-btn">Speichern & Neu</button>'}
  1679|          <button class="btn btn-primary btn-lg" id="save-btn">Speichern</button>
  1680|

  1681|
`;
  1682|  };
  1683|
  1684|  /* ── IMAGE INSERT (compress + insert at cursor) ── */
  1685|  const MAX_IMG_DIM = 1200;
  1686|  const compressImage = file => new Promise((resolve, reject) => {
  1687|    const reader = new FileReader();
  1688|    reader.onerror = () => reject(new Error('Datei nicht lesbar'));
  1689|    reader.onload = () => {
  1690|      const img = new Image();
  1691|      img.onerror = () => reject(new Error('Bild nicht ladbar'));
  1692|      img.onload = () => {
  1693|        let { width: w, height: h } = img;
  1694|        const scale = Math.min(1, MAX_IMG_DIM / Math.max(w, h));
  1695|        w = Math.round(w * scale); h = Math.round(h * scale);
  1696|        const canvas = document.createElement('canvas');
  1697|        canvas.width = w; canvas.height = h;
  1698|        const ctx = canvas.getContext('2d');
  1699|        ctx.drawImage(img, 0, 0, w, h);
  1700|        const hasAlpha = /image\/(png|gif|webp)/i.test(file.type);
  1701|        resolve(canvas.toDataURL(hasAlpha ? 'image/png' : 'image/jpeg', 0.85));
  1702|      };
  1703|      img.src = reader.result;
  1704|    };
  1705|    reader.readAsDataURL(file);
  1706|  });
  1707|
  1708|  const insertAtCursor = (ta, text) => {
  1709|    const start = ta.selectionStart, end = ta.selectionEnd;
  1710|    const before = ta.value.substring(0, start);
  1711|    const after = ta.value.substring(end);
  1712|    const prefix = before && !before.endsWith('\n') ? '\n' : '';
  1713|    const suffix = after && !after.startsWith('\n') ? '\n' : '';
  1714|    ta.value = before + prefix + text + suffix + after;
  1715|    const pos = start + prefix.length + text.length + suffix.length;
  1716|    ta.setSelectionRange(pos, pos);
  1717|    ta.focus();
  1718|  };
  1719|
  1720|  const handleImageFile = async (ta, file) => {
  1721|    if (!file || !file.type.startsWith('image/')) return;
  1722|    try {
  1723|      const dataUrl = await compressImage(file);
  1724|      insertAtCursor(ta, `![Bild](${dataUrl})`);
  1725|    } catch (e) {
  1726|      console.warn('Bild-Import fehlgeschlagen', e);
  1727|    }
  1728|  };
  1729|
  1730|  /* ── VIEW: LEARN ───────────────────────── */
  1731|  const vLearn = () => {
  1732|    const {queue,idx,flipped,stats,total} = learn;
  1733|    if (!queue||!queue.length) return vDone();
  1734|    const card   = queue[idx]||queue[0];
  1735|    const done   = (stats.again||0)+(stats.hard||0)+(stats.good||0)+(stats.easy||0);
  1736|    const remain = queue.length;
  1737|    const pct    = total ? Math.round((done/total)*100) : 0;
  1738|    const prev   = SM2.preview(card);
  1739|
  1740|    return `
  1741|

  1742|        ${learn.dojo ? `<div style="font-size:11px;color:var(--accent);font-weight:700;letter-spacing:0.6px;text-transform:uppercase;margin-bottom:10px;text-align:center">⚔ Dojo Modus</div>` : ''}
  1743|

  1744|

  1745|

  1746|

  1747|          ${remain} verbleibend  ·  ${done} bearbeitet
  1748|

  1749|
  1750|

  1751|          <button class="learn-action-btn" id="learn-edit-btn" title="Karte bearbeiten">
  1752|            ${IC.edit}Bearbeiten
  1753|          </button>
  1754|          <button class="learn-action-btn danger" id="learn-delete-btn" title="Karte löschen">
  1755|            ${IC.trash}Löschen
  1756|          </button>
  1757|

  1758|
  1759|

  1760|

  1761|

  1762|
${card.priority ? '★ Priority · Frage' : 'Frage'}

  1763|
${renderText(card.front)}

  1764|              ${!flipped?'
Tippen oder Leertaste
':''}
  1765|

  1766|

  1767|
${card.priority ? '★ Priority · Antwort' : 'Antwort'}

  1768|
${renderText(card.back)}

  1769|

  1770|

  1771|

  1772|
  1773|        ${flipped ? (card.priority ? `
  1774|

  1775|          <button class="rating-btn rb-good" id="rate-gemerkt" title="Taste 3">
  1776|            Gemerkt!
  1777|            ${prev.good}
  1778|          </button>
  1779|
` : (learn.dojo ? `
  1780|

  1781|          <button class="rating-btn rb-again" id="dojo-nochmal">
  1782|            Nochmal
  1783|          </button>
  1784|          <button class="rating-btn rb-good" id="dojo-weiter">
  1785|            Weiter
  1786|          </button>
  1787|
` : `
  1788|

  1789|          <button class="rating-btn rb-again" data-rate="1" title="Taste 1">
  1790|            Nochmal
  1791|            ${prev.again}
  1792|          </button>
  1793|          <button class="rating-btn rb-hard" data-rate="2" title="Taste 2">
  1794|            Schwer
  1795|            ${prev.hard}
  1796|          </button>
  1797|          <button class="rating-btn rb-good" data-rate="3" title="Taste 3">
  1798|            Gut
  1799|            ${prev.good}
  1800|          </button>
  1801|          <button class="rating-btn rb-easy" data-rate="4" title="Taste 4">
  1802|            Einfach
  1803|            ${prev.easy}
  1804|          </button>
  1805|
`)) : `
Karte aufdecken, dann bewerten · Tasten 1–4 zum Bewerten
`}
  1806|
`;
  1807|  };
  1808|
  1809|  /* ── VIEW: DONE ────────────────────────── */
  1810|  const vDone = () => {
  1811|    if (learn.dojo) {
  1812|      return `
  1813|

  1814|
⚔

  1815|
Dojo abgeschlossen!

  1816|
Alle Karten geübt.

  1817|

  1818|            <button class="btn btn-primary btn-lg" id="retry-btn">${IC.rotate} Nochmal</button>
  1819|            <button class="btn btn-ghost btn-lg" id="back-deck-btn">Zum Deck</button>
  1820|

  1821|
`;
  1822|    }
  1823|    const {stats={},total=0} = learn;
  1824|    const again=stats.again||0, hard=stats.hard||0, good=stats.good||0, easy=stats.easy||0;
  1825|    const correct=good+easy, tot=again+hard+good+easy;
  1826|    const pct = tot ? Math.round((correct/tot)*100) : 0;
  1827|    const emoji = pct===100?'🏆':pct>=70?'🎉':pct>=40?'💪':'📖';
  1828|    const msg   = pct===100?'Perfekt!':pct>=70?'Sehr gut!':pct>=40?'Weiter so!':'Noch üben!';
  1829|    return `
  1830|

  1831|
${emoji}

  1832|
${msg}

  1833|
${tot} Karte${tot!==1?'n':''} bearbeitet

  1834|

  1835|

${again}

Nochmal

  1836|

${hard}

Schwer

  1837|

${good}

Gut

  1838|

${easy}

Einfach

  1839|

${pct}%

Quote

  1840|

  1841|

  1842|          <button class="btn btn-primary btn-lg" id="retry-btn">${IC.rotate} Nochmal</button>
  1843|          <button class="btn btn-ghost btn-lg" id="back-deck-btn">Zum Deck</button>
  1844|

  1845|
`;
  1846|  };
  1847|
  1848|  /* ═══════════════════════════════════════
  1849|     EVENT BINDING
  1850|     ═══════════════════════════════════════ */
  1851|  const bindAll = () => {
  1852|    if (VIEW==='decks')       bindDecks();
  1853|    if (VIEW==='cards')       bindCards();
  1854|    if (VIEW==='editor')      bindEditor();
  1855|    if (VIEW==='learn')       bindLearn();
  1856|    if (VIEW==='done')        bindDone();
  1857|    if (VIEW==='deck-stats')  bindDeckStats();
  1858|
  1859|    /* Tastatur: Enter/Space auf fokussierbaren Nicht-Buttons = Klick.
  1860|       stopPropagation, damit der globale Lern-Handler nicht doppelt flippt. */
  1861|    document.querySelectorAll('#app [role="button"][tabindex]').forEach(el => {
  1862|      el.addEventListener('keydown', e => {
  1863|        if (e.key === 'Enter' || e.key === ' ') {
  1864|          e.preventDefault(); e.stopPropagation();
  1865|          el.click();
  1866|        }
  1867|      });
  1868|    });
  1869|  };
  1870|
  1871|  const bindDecks = () => {
  1872|    document.getElementById('add-deck-btn')?.addEventListener('click', showAddDeck);
  1873|    document.getElementById('deck-add-inline')?.addEventListener('click', showAddDeck);
  1874|    document.getElementById('import-btn')?.addEventListener('click', showImport);
  1875|    document.querySelectorAll('[data-deck]').forEach(el =>
  1876|      el.addEventListener('click', e => {
  1877|        if (e.target.closest('[data-learn],[data-menu]')) return;
  1878|        go('cards',{deckId:el.dataset.deck});
  1879|      })
  1880|    );
  1881|    document.querySelectorAll('[data-learn]').forEach(el =>
  1882|      el.addEventListener('click', e => { e.stopPropagation(); startLearn(el.dataset.learn); })
  1883|    );
  1884|    document.querySelectorAll('[data-menu]').forEach(el =>
  1885|      el.addEventListener('click', e => { e.stopPropagation(); showDeckMenu(e,el.dataset.menu); })
  1886|    );
  1887|  };
  1888|
  1889|  /* Suche/Filter live auf die gerenderte Liste anwenden (kein Re-Render → Fokus bleibt) */
  1890|  const applyCardFilters = () => {
  1891|    const q = cardQuery.trim().toLowerCase();
  1892|    const rows = document.querySelectorAll('.card-row');
  1893|    let visible = 0;
  1894|    rows.forEach(row => {
  1895|      const okQ = !q || (row.dataset.search || '').includes(q);
  1896|      const okF = cardFilter === 'all' || (row.dataset.cat || '').split(' ').includes(cardFilter);
  1897|      const show = okQ && okF;
  1898|      row.style.display = show ? '' : 'none';
  1899|      if (show) visible++;
  1900|    });
  1901|    const nm = document.getElementById('cards-no-match');
  1902|    if (nm) nm.style.display = (rows.length && !visible) ? '' : 'none';
  1903|  };
  1904|
  1905|  const bindCards = () => {
  1906|    document.getElementById('deck-stats-btn')?.addEventListener('click', ()=>go('deck-stats',{deckId}));
  1907|    document.getElementById('add-card-btn')?.addEventListener('click', ()=>go('editor',{cardId:null}));
  1908|    document.getElementById('learn-btn')?.addEventListener('click', ()=>startLearn(deckId));
  1909|    document.querySelectorAll('[data-edit]').forEach(el =>
  1910|      el.addEventListener('click', ()=>go('editor',{cardId:el.dataset.edit}))
  1911|    );
  1912|    document.querySelectorAll('[data-del]').forEach(el =>
  1913|      el.addEventListener('click', ()=>deleteCard(el.dataset.del))
  1914|    );
  1915|    document.getElementById('card-search')?.addEventListener('input', e => {
  1916|      cardQuery = e.target.value;
  1917|      applyCardFilters();
  1918|    });
  1919|    document.querySelectorAll('[data-cfilter]').forEach(el =>
  1920|      el.addEventListener('click', () => {
  1921|        cardFilter = el.dataset.cfilter;
  1922|        document.querySelectorAll('[data-cfilter]').forEach(b =>
  1923|          b.classList.toggle('active', b.dataset.cfilter === cardFilter));
  1924|        applyCardFilters();
  1925|      })
  1926|    );
  1927|    applyCardFilters();
  1928|  };
  1929|
  1930|  const bindDeckStats = () => {
  1931|    document.getElementById('start-learn-btn')?.addEventListener('click', ()=>startLearn(deckId));
  1932|  };
  1933|
  1934|  const bindEditor = () => {
  1935|    const cancelTo = () => {
  1936|      if (_editFromLearn) {
  1937|        _editFromLearn = false;
  1938|        go('learn', { cardId: null });
  1939|      } else {
  1940|        go('cards', { deckId });
  1941|      }
  1942|    };
  1943|    document.getElementById('cancel-btn').addEventListener('click', cancelTo);
  1944|    document.getElementById('save-btn').addEventListener('click', ()=>saveCard(false));
  1945|    document.getElementById('save-new-btn')?.addEventListener('click', ()=>saveCard(true));
  1946|    const tf = document.getElementById('f-front');
  1947|    const tb = document.getElementById('f-back');
  1948|    setupSmartEditor(tf);
  1949|    setupSmartEditor(tb);
  1950|    const resetBorder = ta => ta.addEventListener('input', ()=>{ ta.style.borderColor=''; });
  1951|    resetBorder(tf); resetBorder(tb);
  1952|    const editorKeys = e => {
  1953|      if ((e.metaKey||e.ctrlKey) && e.key === 'Enter') {
  1954|        e.preventDefault();
  1955|        saveCard(e.shiftKey && !cardId);
  1956|      } else if (e.key === 'Escape') {
  1957|        e.preventDefault();
  1958|        cancelTo();
  1959|      }
  1960|    };
  1961|    tf.addEventListener('keydown', editorKeys);
  1962|    tb.addEventListener('keydown', editorKeys);
  1963|
  1964|    /* Bild-Toolbar */
  1965|    const fileInput = document.getElementById('f-img-input');
  1966|    let pendingTa = null;
  1967|    document.querySelectorAll('[data-img-side]').forEach(btn => {
  1968|      btn.addEventListener('click', () => {
  1969|        pendingTa = btn.dataset.imgSide === 'front' ? tf : tb;
  1970|        fileInput.value = '';
  1971|        fileInput.click();
  1972|      });
  1973|    });
  1974|    fileInput.addEventListener('change', () => {
  1975|      const f = fileInput.files && fileInput.files[0];
  1976|      if (f && pendingTa) handleImageFile(pendingTa, f);
  1977|    });
  1978|
  1979|    /* Paste + Drag&Drop pro Textarea */
  1980|    [tf, tb].forEach(ta => {
  1981|      ta.addEventListener('paste', e => {
  1982|        const items = e.clipboardData && e.clipboardData.items;
  1983|        if (!items) return;
  1984|        for (const it of items) {
  1985|          if (it.kind === 'file' && it.type.startsWith('image/')) {
  1986|            e.preventDefault();
  1987|            handleImageFile(ta, it.getAsFile());
  1988|            return;
  1989|          }
  1990|        }
  1991|      });
  1992|      ta.addEventListener('dragover', e => { e.preventDefault(); ta.classList.add('dragover'); });
  1993|      ta.addEventListener('dragleave', () => ta.classList.remove('dragover'));
  1994|      ta.addEventListener('drop', e => {
  1995|        e.preventDefault();
  1996|        ta.classList.remove('dragover');
  1997|        const f = e.dataTransfer.files && e.dataTransfer.files[0];
  1998|        if (f) handleImageFile(ta, f);
  1999|      });
  2000|    });
  2001|

Use offset=2001 to continue reading (showing 1501-2000 of 2678 lines)


Explanation: Let me continue reading the rest of the file to understand the full code, especially the `startLearn` and `startDojo` functions, and the `rateCard` function.